diff --git a/CHANGELOG.md b/CHANGELOG.md index f0fd98cc2e02a..29044fee10b25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,69 @@ + +# 17.1.0-next.1 (2023-11-20) +### common +| Commit | Type | Description | +| -- | -- | -- | +| [29c5416d14](https://github.com/angular/angular/commit/29c5416d14638a05a894269aa5dbe67e98754418) | fix | remove `load` on image once it fails to load ([#52990](https://github.com/angular/angular/pull/52990)) | +| [7affa57754](https://github.com/angular/angular/commit/7affa5775427e92ef6e949c879765b7c8aa172da) | fix | scan images once page is loaded ([#52991](https://github.com/angular/angular/pull/52991)) | +### compiler +| Commit | Type | Description | +| -- | -- | -- | +| [ec2d6e7b9c](https://github.com/angular/angular/commit/ec2d6e7b9c2b386247d1320ee89f8e3ac5e5a0dd) | fix | changed after checked error in for loops ([#52935](https://github.com/angular/angular/pull/52935)) | +| [406049b95e](https://github.com/angular/angular/commit/406049b95e5234f17a7a18839ac848640f53fdde) | fix | generate i18n instructions for blocks ([#52958](https://github.com/angular/angular/pull/52958)) | +| [d9d566d315](https://github.com/angular/angular/commit/d9d566d31540582d73201675d0b8ed901261669e) | fix | nested for loops incorrectly calculating computed variables ([#52931](https://github.com/angular/angular/pull/52931)) | +| [5fb707f81a](https://github.com/angular/angular/commit/5fb707f81aee43751e61d2ed0861afc9b85bc85a) | fix | produce placeholder for blocks in i18n bundles ([#52958](https://github.com/angular/angular/pull/52958)) | +### compiler-cli +| Commit | Type | Description | +| -- | -- | -- | +| [b4d022e230](https://github.com/angular/angular/commit/b4d022e230ca141b12437949d11dc384bfe5c082) | fix | add diagnostic for control flow that prevents content projection ([#52726](https://github.com/angular/angular/pull/52726)) | +### core +| Commit | Type | Description | +| -- | -- | -- | +| [ed0fbd4071](https://github.com/angular/angular/commit/ed0fbd4071339b1af22d82bac07d51c6c41790cd) | fix | cleanup loading promise when no dependencies are defined ([#53031](https://github.com/angular/angular/pull/53031)) | +| [1ce31d819b](https://github.com/angular/angular/commit/1ce31d819b2e4f4425a41f07167a6edce98e77e1) | fix | handle local refs when `getDeferBlocks` is invoked in tests ([#52973](https://github.com/angular/angular/pull/52973)) | +### migrations +| Commit | Type | Description | +| -- | -- | -- | +| [e33f6e0f1a](https://github.com/angular/angular/commit/e33f6e0f1a483cad908fa6d7376d62332797499c) | fix | control flow migration fails for async pipe with unboxing of observable ([#52756](https://github.com/angular/angular/pull/52756)) ([#52972](https://github.com/angular/angular/pull/52972)) | +| [5564d020cd](https://github.com/angular/angular/commit/5564d020cdcea8273b65cf69c45c3f935195af66) | fix | Fixes control flow migration if then else case ([#53006](https://github.com/angular/angular/pull/53006)) | +| [28f6cbf9c9](https://github.com/angular/angular/commit/28f6cbf9c91f957b4926fe34610387e1f1919d4f) | fix | fixes migrations of nested switches in control flow ([#53010](https://github.com/angular/angular/pull/53010)) | +| [e090b48bf8](https://github.com/angular/angular/commit/e090b48bf8534761d46523be57a7889a325bcdec) | fix | tweaks to formatting in control flow migration ([#53058](https://github.com/angular/angular/pull/53058)) | + + + + +# 17.0.4 (2023-11-20) +### common +| Commit | Type | Description | +| -- | -- | -- | +| [7f1c55755d](https://github.com/angular/angular/commit/7f1c55755d94444aa2c07fc62c276bb158e69f24) | fix | remove `load` on image once it fails to load ([#52990](https://github.com/angular/angular/pull/52990)) | +| [fafcb0d23f](https://github.com/angular/angular/commit/fafcb0d23f1f687a2fe5c8349b916586ffadc375) | fix | scan images once page is loaded ([#52991](https://github.com/angular/angular/pull/52991)) | +### compiler +| Commit | Type | Description | +| -- | -- | -- | +| [98376f2c09](https://github.com/angular/angular/commit/98376f2c09e9c28d1473123a2a1f4fb1c9d1cb1e) | fix | changed after checked error in for loops ([#52935](https://github.com/angular/angular/pull/52935)) | +| [291deac663](https://github.com/angular/angular/commit/291deac6636a6f99a98dd0c9096ebe3b0547bb9e) | fix | generate i18n instructions for blocks ([#52958](https://github.com/angular/angular/pull/52958)) | +| [49dca36880](https://github.com/angular/angular/commit/49dca36880a1c1c394533e8a94db9c5ef412ebd2) | fix | nested for loops incorrectly calculating computed variables ([#52931](https://github.com/angular/angular/pull/52931)) | +| [f01b7183d2](https://github.com/angular/angular/commit/f01b7183d2064f41c0f5e30ee976cc91c15e06c5) | fix | produce placeholder for blocks in i18n bundles ([#52958](https://github.com/angular/angular/pull/52958)) | +### compiler-cli +| Commit | Type | Description | +| -- | -- | -- | +| [f671f86ac2](https://github.com/angular/angular/commit/f671f86ac28d434b2fd492ef005749fe0275ece9) | fix | add diagnostic for control flow that prevents content projection ([#52726](https://github.com/angular/angular/pull/52726)) | +### core +| Commit | Type | Description | +| -- | -- | -- | +| [db1a8ebdb4](https://github.com/angular/angular/commit/db1a8ebdb4da8673107ba4ba08c42d484b733c03) | fix | cleanup loading promise when no dependencies are defined ([#53031](https://github.com/angular/angular/pull/53031)) | +| [31a1575334](https://github.com/angular/angular/commit/31a1575334ef78822d947ed858d8365ca5665f2f) | fix | handle local refs when `getDeferBlocks` is invoked in tests ([#52973](https://github.com/angular/angular/pull/52973)) | +### migrations +| Commit | Type | Description | +| -- | -- | -- | +| [ac9cd6108f](https://github.com/angular/angular/commit/ac9cd6108f6fe25e9c7a11db9816c6e07d241515) | fix | control flow migration fails for async pipe with unboxing of observable ([#52756](https://github.com/angular/angular/pull/52756)) ([#52972](https://github.com/angular/angular/pull/52972)) | +| [13bf5b7007](https://github.com/angular/angular/commit/13bf5b700739aadb2e5a210441fb815a8501ad65) | fix | Fixes control flow migration if then else case ([#53006](https://github.com/angular/angular/pull/53006)) | +| [492ad4698a](https://github.com/angular/angular/commit/492ad4698aaef51a3d24ae90f617a2ba3fae901e) | fix | fixes migrations of nested switches in control flow ([#53010](https://github.com/angular/angular/pull/53010)) | +| [0fad36eff2](https://github.com/angular/angular/commit/0fad36eff2b228baa3b8868810d4ac86eb6db459) | fix | tweaks to formatting in control flow migration ([#53058](https://github.com/angular/angular/pull/53058)) | + + + # 17.1.0-next.0 (2023-11-15) ### compiler-cli diff --git a/adev/src/content/guide/components/lifecycle.md b/adev/src/content/guide/components/lifecycle.md index 8a9ce66da311c..7bf271e702f54 100644 --- a/adev/src/content/guide/components/lifecycle.md +++ b/adev/src/content/guide/components/lifecycle.md @@ -257,12 +257,12 @@ export class UserProfile { // Use the `Write` phase to write to a geometric property. afterNextRender(() => { nativeElement.style.padding = computePadding(); - }, AfterRenderPhase.Write); + }, {phase: AfterRenderPhase.Write}); // Use the `Read` phase to read geometric properties after all writes have occured. afterNextRender(() => { this.elementHeight = nativeElement.getBoundingClientRect().height; - }, AfterRenderPhase.Read); + }, {phase: AfterRenderPhase.Read}); } } ``` diff --git a/adev/src/content/guide/defer.md b/adev/src/content/guide/defer.md index 13e06b19bef65..be842d1ee7f2e 100644 --- a/adev/src/content/guide/defer.md +++ b/adev/src/content/guide/defer.md @@ -262,7 +262,7 @@ it('should render a defer block in different states', async () => { const componentFixture = TestBed.createComponent(ComponentA); // Retrieve the list of all defer block fixtures and get the first block. - const deferBlockFixture = async (componentFixture.getDeferBlocks())[0]; + const deferBlockFixture = (await componentFixture.getDeferBlocks())[0]; // Renders placeholder state by default. expect(componentFixture.nativeElement.innerHTML).toContain('Placeholder'); @@ -272,7 +272,7 @@ it('should render a defer block in different states', async () => { expect(componentFixture.nativeElement.innerHTML).toContain('Loading'); // Render final state and verify the output. - await deferBlockFixture.render(DeferBlockState.Completed); + await deferBlockFixture.render(DeferBlockState.Complete); expect(componentFixture.nativeElement.innerHTML).toContain('large works!'); }); ``` diff --git a/aio/content/guide/defer.md b/aio/content/guide/defer.md index 3d2ae23cd7aaf..0fed612e7dd67 100644 --- a/aio/content/guide/defer.md +++ b/aio/content/guide/defer.md @@ -262,7 +262,7 @@ it('should render a defer block in different states', async () => { const componentFixture = TestBed.createComponent(ComponentA); // Retrieve the list of all defer block fixtures and get the first block. - const deferBlockFixture = async (componentFixture.getDeferBlocks())[0]; + const deferBlockFixture = (await componentFixture.getDeferBlocks())[0]; // Renders placeholder state by default. expect(componentFixture.nativeElement.innerHTML).toContain('Placeholder'); @@ -272,7 +272,7 @@ it('should render a defer block in different states', async () => { expect(componentFixture.nativeElement.innerHTML).toContain('Loading'); // Render final state and verify the output. - await deferBlockFixture.render(DeferBlockState.Completed); + await deferBlockFixture.render(DeferBlockState.Complete); expect(componentFixture.nativeElement.innerHTML).toContain('large works!'); }); ``` diff --git a/package.json b/package.json index 2bd82c813761d..3c799c06f3e1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-srcs", - "version": "17.1.0-next.0", + "version": "17.1.0-next.1", "private": true, "description": "Angular - a web framework for modern web apps", "homepage": "https://github.com/angular/angular", diff --git a/packages/common/http/src/interceptor.ts b/packages/common/http/src/interceptor.ts index 8d83eb5f5a2fb..73ad2cf29d9f1 100644 --- a/packages/common/http/src/interceptor.ts +++ b/packages/common/http/src/interceptor.ts @@ -7,7 +7,7 @@ */ import {isPlatformServer} from '@angular/common'; -import {EnvironmentInjector, inject, Injectable, InjectionToken, PLATFORM_ID, ɵConsole as Console, ɵformatRuntimeError as formatRuntimeError, ɵInitialRenderPendingTasks as InitialRenderPendingTasks} from '@angular/core'; +import {EnvironmentInjector, inject, Injectable, InjectionToken, PLATFORM_ID, runInInjectionContext, ɵConsole as Console, ɵformatRuntimeError as formatRuntimeError, ɵInitialRenderPendingTasks as InitialRenderPendingTasks} from '@angular/core'; import {Observable} from 'rxjs'; import {finalize} from 'rxjs/operators'; @@ -162,7 +162,7 @@ function chainedInterceptorFn( chainTailFn: ChainedInterceptorFn, interceptorFn: HttpInterceptorFn, injector: EnvironmentInjector): ChainedInterceptorFn { // clang-format off - return (initialRequest, finalHandlerFn) => injector.runInContext(() => + return (initialRequest, finalHandlerFn) => runInInjectionContext(injector, () => interceptorFn( initialRequest, downstreamRequest => chainTailFn(downstreamRequest, finalHandlerFn) diff --git a/packages/common/http/src/jsonp.ts b/packages/common/http/src/jsonp.ts index 2ceaef1e4d3dd..9f09dd900c591 100644 --- a/packages/common/http/src/jsonp.ts +++ b/packages/common/http/src/jsonp.ts @@ -7,7 +7,7 @@ */ import {DOCUMENT} from '@angular/common'; -import {EnvironmentInjector, Inject, inject, Injectable} from '@angular/core'; +import {EnvironmentInjector, Inject, inject, Injectable, runInInjectionContext} from '@angular/core'; import {Observable, Observer} from 'rxjs'; import {HttpBackend, HttpHandler} from './backend'; @@ -278,7 +278,8 @@ export class JsonpInterceptor { * @returns An observable of the event stream. */ intercept(initialRequest: HttpRequest, next: HttpHandler): Observable> { - return this.injector.runInContext( + return runInInjectionContext( + this.injector, () => jsonpInterceptorFn( initialRequest, downstreamRequest => next.handle(downstreamRequest))); } diff --git a/packages/common/http/src/xsrf.ts b/packages/common/http/src/xsrf.ts index 67db8dcec9542..2ea3cb24a90b7 100644 --- a/packages/common/http/src/xsrf.ts +++ b/packages/common/http/src/xsrf.ts @@ -7,7 +7,7 @@ */ import {DOCUMENT, ɵparseCookieValue as parseCookieValue} from '@angular/common'; -import {EnvironmentInjector, Inject, inject, Injectable, InjectionToken, PLATFORM_ID} from '@angular/core'; +import {EnvironmentInjector, Inject, inject, Injectable, InjectionToken, PLATFORM_ID, runInInjectionContext} from '@angular/core'; import {Observable} from 'rxjs'; import {HttpHandler} from './backend'; @@ -104,7 +104,8 @@ export class HttpXsrfInterceptor implements HttpInterceptor { constructor(private injector: EnvironmentInjector) {} intercept(initialRequest: HttpRequest, next: HttpHandler): Observable> { - return this.injector.runInContext( + return runInInjectionContext( + this.injector, () => xsrfInterceptorFn(initialRequest, downstreamRequest => next.handle(downstreamRequest))); } diff --git a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts index 1e8aaabc7f093..27c2bf81cad11 100644 --- a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts +++ b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts @@ -746,8 +746,9 @@ function assertGreaterThanZero(dir: NgOptimizedImage, inputValue: unknown, input */ function assertNoImageDistortion( dir: NgOptimizedImage, img: HTMLImageElement, renderer: Renderer2) { - const removeListenerFn = renderer.listen(img, 'load', () => { - removeListenerFn(); + const removeLoadListenerFn = renderer.listen(img, 'load', () => { + removeLoadListenerFn(); + removeErrorListenerFn(); const computedStyle = window.getComputedStyle(img); let renderedWidth = parseFloat(computedStyle.getPropertyValue('width')); let renderedHeight = parseFloat(computedStyle.getPropertyValue('height')); @@ -828,6 +829,15 @@ function assertNoImageDistortion( } } }); + + // We only listen to the `error` event to remove the `load` event listener because it will not be + // fired if the image fails to load. This is done to prevent memory leaks in development mode + // because image elements aren't garbage-collected properly. It happens because zone.js stores the + // event listener directly on the element and closures capture `dir`. + const removeErrorListenerFn = renderer.listen(img, 'error', () => { + removeLoadListenerFn(); + removeErrorListenerFn(); + }); } /** @@ -870,8 +880,9 @@ function assertEmptyWidthAndHeight(dir: NgOptimizedImage) { */ function assertNonZeroRenderedHeight( dir: NgOptimizedImage, img: HTMLImageElement, renderer: Renderer2) { - const removeListenerFn = renderer.listen(img, 'load', () => { - removeListenerFn(); + const removeLoadListenerFn = renderer.listen(img, 'load', () => { + removeLoadListenerFn(); + removeErrorListenerFn(); const renderedHeight = img.clientHeight; if (dir.fill && renderedHeight === 0) { console.warn(formatRuntimeError( @@ -883,6 +894,12 @@ function assertNonZeroRenderedHeight( `property defined and the height of the element is not zero.`)); } }); + + // See comments in the `assertNoImageDistortion`. + const removeErrorListenerFn = renderer.listen(img, 'error', () => { + removeLoadListenerFn(); + removeErrorListenerFn(); + }); } /** diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts b/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts index 22e99ad52305c..6d3c845f71b19 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts @@ -9,7 +9,7 @@ import {Reference} from '../../imports'; import {ClassDeclaration} from '../../reflection'; -import {DirectiveMeta, InputMapping, MetadataReader} from './api'; +import {DirectiveMeta, HostDirectiveMeta, InputMapping, MetadataReader} from './api'; import {ClassPropertyMapping, ClassPropertyName} from './property_mapping'; /** @@ -34,6 +34,7 @@ export function flattenInheritedDirectiveMetadata( const undeclaredInputFields = new Set(); const restrictedInputFields = new Set(); const stringLiteralInputFields = new Set(); + let hostDirectives: HostDirectiveMeta[]|null = null; let isDynamic = false; let inputs = ClassPropertyMapping.empty(); let outputs = ClassPropertyMapping.empty(); @@ -69,6 +70,10 @@ export function flattenInheritedDirectiveMetadata( for (const field of meta.stringLiteralInputFields) { stringLiteralInputFields.add(field); } + if (meta.hostDirectives !== null && meta.hostDirectives.length > 0) { + hostDirectives ??= []; + hostDirectives.push(...meta.hostDirectives); + } }; addMetadata(topMeta); @@ -83,5 +88,6 @@ export function flattenInheritedDirectiveMetadata( stringLiteralInputFields, baseClass: isDynamic ? 'dynamic' : null, isStructural, + hostDirectives, }; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/GOLDEN_PARTIAL.js new file mode 100644 index 0000000000000..5225655ba735e --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/GOLDEN_PARTIAL.js @@ -0,0 +1,208 @@ +/**************************************************************************************************** + * PARTIAL FILE: conditional.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyApp { + constructor() { + this.count = 0; + } +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: ` +
+ Content: + @if (count === 0) { + beforezeroafter + } @else if (count === 1) { + before
one
after + } @else { + beforeafter + }! + + @if (count === 7) { + beforesevenafter + } +
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + template: ` +
+ Content: + @if (count === 0) { + beforezeroafter + } @else if (count === 1) { + before
one
after + } @else { + beforeafter + }! + + @if (count === 7) { + beforesevenafter + } +
+ ` + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: conditional.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyApp { + count: number; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: switch.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyApp { + constructor() { + this.count = 0; + } +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: ` +
+ Content: + @switch (count) { + @case (0) {beforezeroafter} + @case (1) {before
one
after} + @default {beforeafter} + } +
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + template: ` +
+ Content: + @switch (count) { + @case (0) {beforezeroafter} + @case (1) {before
one
after} + @default {beforeafter} + } +
+ ` + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: switch.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyApp { + count: number; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: for.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyApp { + constructor() { + this.items = [1, 2, 3]; + } +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: ` +
+ Content: + @for (item of items; track item) { + beforemiddleafter + } @empty { + before
empty
after + }! +
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + template: ` +
+ Content: + @for (item of items; track item) { + beforemiddleafter + } @empty { + before
empty
after + }! +
+ ` + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: for.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyApp { + items: number[]; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: defer.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyApp { + constructor() { + this.isLoaded = false; + } +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: ` +
+ Content: + @defer (when isLoaded) { + beforemiddleafter + } @placeholder { + before
placeholder
after + } @loading { + beforeafter + } @error { + before

error

after + } +
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + template: ` +
+ Content: + @defer (when isLoaded) { + beforemiddleafter + } @placeholder { + before
placeholder
after + } @loading { + beforeafter + } @error { + before

error

after + } +
+ ` + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: defer.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyApp { + isLoaded: boolean; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/TEST_CASES.json new file mode 100644 index 0000000000000..305e16408118e --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/TEST_CASES.json @@ -0,0 +1,89 @@ +{ + "$schema": "../../test_case_schema.json", + "cases": [ + { + "description": "should support @if blocks", + "skipForTemplatePipeline": true, + "inputFiles": [ + "conditional.ts" + ], + "expectations": [ + { + "files": [ + { + "generated": "conditional.js", + "expected": "conditional_template.js" + } + ], + "extraChecks": [ + "verifyPlaceholdersIntegrity", + "verifyUniqueConsts" + ] + } + ] + }, + { + "description": "should support @switch blocks", + "skipForTemplatePipeline": true, + "inputFiles": [ + "switch.ts" + ], + "expectations": [ + { + "files": [ + { + "generated": "switch.js", + "expected": "switch_template.js" + } + ], + "extraChecks": [ + "verifyPlaceholdersIntegrity", + "verifyUniqueConsts" + ] + } + ] + }, + { + "description": "should support @for blocks", + "skipForTemplatePipeline": true, + "inputFiles": [ + "for.ts" + ], + "expectations": [ + { + "files": [ + { + "generated": "for.js", + "expected": "for_template.js" + } + ], + "extraChecks": [ + "verifyPlaceholdersIntegrity", + "verifyUniqueConsts" + ] + } + ] + }, + { + "description": "should support @defer blocks", + "skipForTemplatePipeline": true, + "inputFiles": [ + "defer.ts" + ], + "expectations": [ + { + "files": [ + { + "generated": "defer.js", + "expected": "defer_template.js" + } + ], + "extraChecks": [ + "verifyPlaceholdersIntegrity", + "verifyUniqueConsts" + ] + } + ] + } + ] +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/conditional.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/conditional.ts new file mode 100644 index 0000000000000..5ac5bd3bc1996 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/conditional.ts @@ -0,0 +1,23 @@ +import {Component} from '@angular/core'; + +@Component({ + template: ` +
+ Content: + @if (count === 0) { + beforezeroafter + } @else if (count === 1) { + before
one
after + } @else { + beforeafter + }! + + @if (count === 7) { + beforesevenafter + } +
+ ` +}) +export class MyApp { + count = 0; +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/conditional_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/conditional_template.js new file mode 100644 index 0000000000000..a159da1c7e9ea --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/conditional_template.js @@ -0,0 +1,97 @@ +function MyApp_Conditional_2_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵi18nStart(0, 0, 1); + $r3$.ɵɵelement(1, "span"); + $r3$.ɵɵi18nEnd(); + } +} + +function MyApp_Conditional_3_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵi18nStart(0, 0, 2); + $r3$.ɵɵelement(1, "div"); + $r3$.ɵɵi18nEnd(); + } +} + +function MyApp_Conditional_4_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵi18nStart(0, 0, 3); + $r3$.ɵɵelement(1, "button"); + $r3$.ɵɵi18nEnd(); + } +} + +function MyApp_Conditional_5_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵi18nStart(0, 0, 4); + $r3$.ɵɵelement(1, "span"); + $r3$.ɵɵi18nEnd(); + } +} + +… + +MyApp.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({ + … + consts: () => { + let i18n_0; + if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) { + /** + * @suppress {msgDescriptions} + */ + const MSG_EXTERNAL_4037030454066434177$$CONDITIONAL_TS__1 = goog.getMsg(" Content: {$startBlockIf} before{$startTagSpan}zero{$closeTagSpan}after {$closeBlockIf}{$startBlockElseIf} before{$startTagDiv}one{$closeTagDiv}after {$closeBlockElseIf}{$startBlockElse} before{$startTagButton}otherwise{$closeTagButton}after {$closeBlockElse}! {$startBlockIf_1} before{$startTagSpan}seven{$closeTagSpan}after {$closeBlockIf}", { + "closeBlockElse": "\uFFFD/*4:3\uFFFD", + "closeBlockElseIf": "\uFFFD/*3:2\uFFFD", + "closeBlockIf": "[\uFFFD/*2:1\uFFFD|\uFFFD/*5:4\uFFFD]", + "closeTagButton": "\uFFFD/#1:3\uFFFD", + "closeTagDiv": "\uFFFD/#1:2\uFFFD", + "closeTagSpan": "[\uFFFD/#1:1\uFFFD|\uFFFD/#1:4\uFFFD]", + "startBlockElse": "\uFFFD*4:3\uFFFD", + "startBlockElseIf": "\uFFFD*3:2\uFFFD", + "startBlockIf": "\uFFFD*2:1\uFFFD", + "startBlockIf_1": "\uFFFD*5:4\uFFFD", + "startTagButton": "\uFFFD#1:3\uFFFD", + "startTagDiv": "\uFFFD#1:2\uFFFD", + "startTagSpan": "[\uFFFD#1:1\uFFFD|\uFFFD#1:4\uFFFD]" + }, { + original_code: { + "closeBlockElse": "}", + "closeBlockElseIf": "}", + "closeBlockIf": "}", + "closeTagButton": "", + "closeTagDiv": "", + "closeTagSpan": "", + "startBlockElse": "@else {", + "startBlockElseIf": "@else if (count === 1) {", + "startBlockIf": "@if (count === 0) {", + "startBlockIf_1": "@if (count === 7) {", + "startTagButton": "after + } @error { + before

error

after + } + + ` +}) +export class MyApp { + isLoaded = false; +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/defer_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/defer_template.js new file mode 100644 index 0000000000000..6bf70b8dfa5a5 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/defer_template.js @@ -0,0 +1,101 @@ +function MyApp_Defer_2_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵi18nStart(0, 0, 1); + $r3$.ɵɵelement(1, "span"); + $r3$.ɵɵi18nEnd(); + } +} + +function MyApp_DeferLoading_3_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵi18nStart(0, 0, 2); + $r3$.ɵɵelement(1, "button"); + $r3$.ɵɵi18nEnd(); + } +} + +function MyApp_DeferPlaceholder_4_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵi18nStart(0, 0, 3); + $r3$.ɵɵelement(1, "div"); + $r3$.ɵɵi18nEnd(); + } +} + +function MyApp_DeferError_5_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵi18nStart(0, 0, 4); + $r3$.ɵɵelement(1, "h1"); + $r3$.ɵɵi18nEnd(); + } +} + +… + +MyApp.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({ + … + consts: () => { + let i18n_0; + if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) { + /** + * @suppress {msgDescriptions} + */ + const MSG_EXTERNAL_7863738818382242773$$DEFER_TS__1 = goog.getMsg(" Content: {$startBlockDefer} before{$startTagSpan}middle{$closeTagSpan}after {$closeBlockDefer}{$startBlockPlaceholder} before{$startTagDiv}placeholder{$closeTagDiv}after {$closeBlockPlaceholder}{$startBlockLoading} before{$startTagButton}loading{$closeTagButton}after {$closeBlockLoading}{$startBlockError} before{$startHeadingLevel1}error{$closeHeadingLevel1}after {$closeBlockError}", { + "closeBlockDefer": "\uFFFD/*2:1\uFFFD", + "closeBlockError": "\uFFFD/*5:4\uFFFD", + "closeBlockLoading": "\uFFFD/*3:2\uFFFD", + "closeBlockPlaceholder": "\uFFFD/*4:3\uFFFD", + "closeHeadingLevel1": "\uFFFD/#1:4\uFFFD", + "closeTagButton": "\uFFFD/#1:2\uFFFD", + "closeTagDiv": "\uFFFD/#1:3\uFFFD", + "closeTagSpan": "\uFFFD/#1:1\uFFFD", + "startBlockDefer": "\uFFFD*2:1\uFFFD", + "startBlockError": "\uFFFD*5:4\uFFFD", + "startBlockLoading": "\uFFFD*3:2\uFFFD", + "startBlockPlaceholder": "\uFFFD*4:3\uFFFD", + "startHeadingLevel1": "\uFFFD#1:4\uFFFD", + "startTagButton": "\uFFFD#1:2\uFFFD", + "startTagDiv": "\uFFFD#1:3\uFFFD", + "startTagSpan": "\uFFFD#1:1\uFFFD" + }, { + original_code: { + "closeBlockDefer": "}", + "closeBlockError": "}", + "closeBlockLoading": "}", + "closeBlockPlaceholder": "}", + "closeHeadingLevel1": "", + "closeTagButton": "", + "closeTagDiv": "", + "closeTagSpan": "", + "startBlockDefer": "@defer (when isLoaded) {", + "startBlockError": "@error {", + "startBlockLoading": "@loading {", + "startBlockPlaceholder": "@placeholder {", + "startHeadingLevel1": "

", + "startTagButton": "after} + } + + ` +}) +export class MyApp { + count = 0; +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/switch_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/switch_template.js new file mode 100644 index 0000000000000..6581d336c8bfb --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/blocks/switch_template.js @@ -0,0 +1,84 @@ +function MyApp_Case_2_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵi18nStart(0, 0, 1); + $r3$.ɵɵelement(1, "span"); + $r3$.ɵɵi18nEnd(); + } +} + +function MyApp_Case_3_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵi18nStart(0, 0, 2); + $r3$.ɵɵelement(1, "div"); + $r3$.ɵɵi18nEnd(); + } +} + +function MyApp_Case_4_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵi18nStart(0, 0, 3); + $r3$.ɵɵelement(1, "button"); + $r3$.ɵɵi18nEnd(); + } +} + +… + +MyApp.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({ + … + consts: () => { + let i18n_0; + if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) { + /** + * @suppress {msgDescriptions} + */ + const MSG_EXTERNAL_532730339085995504$$SWITCH_TS__1 = goog.getMsg(" Content: {$startBlockCase}before{$startTagSpan}zero{$closeTagSpan}after{$closeBlockCase}{$startBlockCase_1}before{$startTagDiv}one{$closeTagDiv}after{$closeBlockCase}{$startBlockDefault}before{$startTagButton}otherwise{$closeTagButton}after{$closeBlockDefault}", { + "closeBlockCase": "[\uFFFD/*2:1\uFFFD|\uFFFD/*3:2\uFFFD]", + "closeBlockDefault": "\uFFFD/*4:3\uFFFD", + "closeTagButton": "\uFFFD/#1:3\uFFFD", + "closeTagDiv": "\uFFFD/#1:2\uFFFD", + "closeTagSpan": "\uFFFD/#1:1\uFFFD", + "startBlockCase": "\uFFFD*2:1\uFFFD", + "startBlockCase_1": "\uFFFD*3:2\uFFFD", + "startBlockDefault": "\uFFFD*4:3\uFFFD", + "startTagButton": "\uFFFD#1:3\uFFFD", + "startTagDiv": "\uFFFD#1:2\uFFFD", + "startTagSpan": "\uFFFD#1:1\uFFFD" + }, { + original_code: { + "closeBlockCase": "}", + "closeBlockDefault": "}", + "closeTagButton": "", + "closeTagDiv": "", + "closeTagSpan": "", + "startBlockCase": "@case (0) {", + "startBlockCase_1": "@case (1) {", + "startBlockDefault": "@default {", + "startTagButton": "'}) + class App { + spy = jasmine.createSpy('click spy'); + } + + TestBed.configureTestingModule({declarations: [App, Dir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + fixture.nativeElement.querySelector('button').click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.spy).toHaveBeenCalledOnceWith('hello'); + }); }); describe('inputs', () => { @@ -1874,6 +1997,36 @@ describe('host directives', () => { // The input on the button instance should not have been written to. expect(logs).toEqual(['spanValue']); }); + + it('should set the input of an inherited host directive that has been exposed', () => { + @Directive({standalone: true}) + class HostDir { + @Input() color?: string; + } + + @Directive({hostDirectives: [{directive: HostDir, inputs: ['color']}]}) + class Parent { + } + + @Directive({selector: '[dir]'}) + class Dir extends Parent { + } + + @Component({template: ''}) + class App { + @ViewChild(HostDir) hostDir!: HostDir; + color = 'red'; + } + + TestBed.configureTestingModule({declarations: [App, Dir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(fixture.componentInstance.hostDir.color).toBe('red'); + + fixture.componentInstance.color = 'green'; + fixture.detectChanges(); + expect(fixture.componentInstance.hostDir.color).toBe('green'); + }); }); describe('ngOnChanges', () => { @@ -3318,5 +3471,33 @@ describe('host directives', () => { fixture.detectChanges(); }).not.toThrow(); }); + + it('should throw an error if a duplicate directive is inherited', () => { + @Directive({standalone: true}) + class HostDir { + } + + @Directive({standalone: true, hostDirectives: [HostDir]}) + class Grandparent { + } + + @Directive({standalone: true}) + class Parent extends Grandparent { + } + + @Directive({selector: '[dir]', hostDirectives: [HostDir]}) + class Dir extends Parent { + } + + @Component({template: '
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Dir]}); + + expect(() => TestBed.createComponent(App)) + .toThrowError( + 'NG0309: Directive HostDir matches multiple times on the same element. Directives can only match an element once.'); + }); }); }); diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts index 45c601303c530..a40c527e9d6a4 100644 --- a/packages/core/test/acceptance/i18n_spec.ts +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -13,12 +13,12 @@ import {CommonModule, DOCUMENT, registerLocaleData} from '@angular/common'; import localeEs from '@angular/common/locales/es'; import localeRo from '@angular/common/locales/ro'; import {computeMsgId} from '@angular/compiler'; -import {Attribute, Component, ContentChild, ContentChildren, Directive, ElementRef, HostBinding, Input, LOCALE_ID, NO_ERRORS_SCHEMA, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, Type, ViewChild, ViewContainerRef, ɵsetDocument} from '@angular/core'; +import {Attribute, Component, ContentChild, ContentChildren, Directive, ElementRef, HostBinding, Input, LOCALE_ID, NO_ERRORS_SCHEMA, Pipe, PipeTransform, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ɵsetDocument} from '@angular/core'; import {HEADER_OFFSET} from '@angular/core/src/render3/interfaces/view'; import {getComponentLView} from '@angular/core/src/render3/util/discovery_utils'; -import {TestBed} from '@angular/core/testing'; +import {DeferBlockBehavior, DeferBlockState, TestBed} from '@angular/core/testing'; import {clearTranslations, loadTranslations} from '@angular/localize'; -import {By, ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser'; +import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {BehaviorSubject} from 'rxjs'; @@ -361,6 +361,163 @@ describe('runtime i18n', () => { expect(fixture.nativeElement.textContent).toBe(' Deux '); }); + it('should support conditional blocks', () => { + loadTranslations({ + [computeMsgId( + 'Content: {$START_BLOCK_IF}before{$START_TAG_SPAN}zero{$CLOSE_TAG_SPAN}after' + + '{$CLOSE_BLOCK_IF}{$START_BLOCK_ELSE_IF}before{$START_TAG_DIV}one{$CLOSE_TAG_DIV}' + + 'after{$CLOSE_BLOCK_ELSE_IF}{$START_BLOCK_ELSE}before{$START_TAG_BUTTON}' + + 'otherwise{$CLOSE_TAG_BUTTON}after{$CLOSE_BLOCK_ELSE}!', + '')]: 'Contenido: {$START_BLOCK_IF}antes{$START_TAG_SPAN}cero{$CLOSE_TAG_SPAN}después' + + '{$CLOSE_BLOCK_IF}{$START_BLOCK_ELSE_IF}antes{$START_TAG_DIV}uno{$CLOSE_TAG_DIV}' + + 'después{$CLOSE_BLOCK_ELSE_IF}{$START_BLOCK_ELSE}antes{$START_TAG_BUTTON}' + + 'si no{$CLOSE_TAG_BUTTON}después{$CLOSE_BLOCK_ELSE}!' + }); + + const fixture = initWithTemplate( + AppComp, + '
Content: @if (count === 0) {beforezeroafter} ' + + '@else if (count === 1) {before
one
after} ' + + '@else {beforeafter}!
'); + + expect(fixture.nativeElement.innerHTML) + .toEqual( + '
Contenido: antescerodespués' + + '!
'); + + fixture.componentInstance.count = 1; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + '
Contenido: antes
uno
después' + + '!
'); + + fixture.componentInstance.count = 2; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + '
Contenido: antesdespués' + + '!
'); + }); + + it('should support switch blocks', () => { + loadTranslations({ + [computeMsgId( + 'Content: {$START_BLOCK_CASE}before{$START_TAG_SPAN}zero{$CLOSE_TAG_SPAN}after' + + '{$CLOSE_BLOCK_CASE}{$START_BLOCK_CASE_1}before{$START_TAG_DIV}one' + + '{$CLOSE_TAG_DIV}after{$CLOSE_BLOCK_CASE}{$START_BLOCK_DEFAULT}before' + + '{$START_TAG_BUTTON}otherwise{$CLOSE_TAG_BUTTON}after{$CLOSE_BLOCK_DEFAULT}', + '')]: 'Contenido: {$START_BLOCK_CASE}antes{$START_TAG_SPAN}cero{$CLOSE_TAG_SPAN}después' + + '{$CLOSE_BLOCK_CASE}{$START_BLOCK_CASE_1}antes{$START_TAG_DIV}uno' + + '{$CLOSE_TAG_DIV}después{$CLOSE_BLOCK_CASE}{$START_BLOCK_DEFAULT}antes' + + '{$START_TAG_BUTTON}si no{$CLOSE_TAG_BUTTON}después{$CLOSE_BLOCK_DEFAULT}' + }); + + const fixture = initWithTemplate( + AppComp, + '
Content: @switch (count) {' + + '@case (0) {beforezeroafter}' + + '@case (1) {before
one
after}' + + '@default {beforeafter}' + + '}
'); + + expect(fixture.nativeElement.innerHTML) + .toEqual( + '
Contenido: antescerodespués' + + '
'); + + fixture.componentInstance.count = 1; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + '
Contenido: antes
uno
después' + + '
'); + + fixture.componentInstance.count = 2; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + '
Contenido: antesdespués' + + '
'); + }); + + it('should support for loop blocks', () => { + loadTranslations({ + [computeMsgId( + 'Content: {$START_BLOCK_FOR}before{$START_TAG_SPAN}' + + 'middle{$CLOSE_TAG_SPAN}after{$CLOSE_BLOCK_FOR}{$START_BLOCK_EMPTY}' + + 'before{$START_TAG_DIV}empty{$CLOSE_TAG_DIV}after{$CLOSE_BLOCK_EMPTY}!')]: + 'Contenido: {$START_BLOCK_FOR}antes{$START_TAG_SPAN}' + + 'medio{$CLOSE_TAG_SPAN}después{$CLOSE_BLOCK_FOR}{$START_BLOCK_EMPTY}' + + 'antes{$START_TAG_DIV}vacío{$CLOSE_TAG_DIV}después{$CLOSE_BLOCK_EMPTY}!' + }); + + const fixture = initWithTemplate( + AppComp, + '
Content: @for (item of items; track item) {beforemiddleafter}' + + '@empty {before
empty
after}!
'); + + expect(fixture.nativeElement.innerHTML) + .toEqual( + '
Contenido: antesmedio' + + 'despuésantesmediodespuésantesmedio' + + 'después!
'); + + fixture.componentInstance.items = []; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML) + .toEqual( + '
Contenido: antes
' + + 'vacío
después!
'); + }); + + it('should support deferred blocks', async () => { + loadTranslations({ + [computeMsgId( + 'Content: {$START_BLOCK_DEFER}before{$START_TAG_SPAN}middle' + + '{$CLOSE_TAG_SPAN}after{$CLOSE_BLOCK_DEFER}{$START_BLOCK_PLACEHOLDER}before' + + '{$START_TAG_DIV}placeholder{$CLOSE_TAG_DIV}after{$CLOSE_BLOCK_PLACEHOLDER}!', + '')]: 'Contenido: {$START_BLOCK_DEFER}before{$START_TAG_SPAN}medio' + + '{$CLOSE_TAG_SPAN}after{$CLOSE_BLOCK_DEFER}{$START_BLOCK_PLACEHOLDER}before' + + '{$START_TAG_DIV}marcador de posición{$CLOSE_TAG_DIV}after{$CLOSE_BLOCK_PLACEHOLDER}!' + }); + + @Component({ + selector: 'defer-comp', + standalone: true, + template: '
Content: @defer (when isLoaded) {beforemiddleafter} ' + + '@placeholder {before
placeholder
after}!
' + }) + class DeferComp { + isLoaded = false; + } + + TestBed.configureTestingModule({ + imports: [DeferComp], + deferBlockBehavior: DeferBlockBehavior.Manual, + teardown: {destroyAfterEach: true}, + }); + + const fixture = TestBed.createComponent(DeferComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML) + .toEqual( + '
Contenido: ' + + '!before
marcador de posición
after
'); + + const deferBlock = (await fixture.getDeferBlocks())[0]; + fixture.componentInstance.isLoaded = true; + fixture.detectChanges(); + await deferBlock.render(DeferBlockState.Complete); + + expect(fixture.nativeElement.innerHTML) + .toEqual( + '
Contenido: ' + + '!beforemedioafter
'); + }); + describe('ng-container and ng-template support', () => { it('should support ng-container', () => { loadTranslations({[computeMsgId('text')]: 'texte'}); @@ -3061,6 +3218,7 @@ class AppComp { description = `Web Framework`; visible = true; count = 0; + items = [1, 2, 3]; } @Component({ diff --git a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json index 01eaba391e910..1b0d113b526f9 100644 --- a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json @@ -1328,6 +1328,9 @@ { "name": "replacePostStylesAsPre" }, + { + "name": "requiresRefreshOrTraversal" + }, { "name": "resetPreOrderHookFlags" }, diff --git a/packages/core/test/bundling/animations/bundle.golden_symbols.json b/packages/core/test/bundling/animations/bundle.golden_symbols.json index 7c13020110545..89281f76307a7 100644 --- a/packages/core/test/bundling/animations/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations/bundle.golden_symbols.json @@ -1400,6 +1400,9 @@ { "name": "replacePostStylesAsPre" }, + { + "name": "requiresRefreshOrTraversal" + }, { "name": "resetPreOrderHookFlags" }, diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index db31761dc1b5f..e8dad0f93977a 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -1112,6 +1112,9 @@ { "name": "renderView" }, + { + "name": "requiresRefreshOrTraversal" + }, { "name": "resetPreOrderHookFlags" }, diff --git a/packages/core/test/bundling/defer/bundle.golden_symbols.json b/packages/core/test/bundling/defer/bundle.golden_symbols.json index f4e3b0a5e7916..75806d3e6f10d 100644 --- a/packages/core/test/bundling/defer/bundle.golden_symbols.json +++ b/packages/core/test/bundling/defer/bundle.golden_symbols.json @@ -2249,6 +2249,9 @@ { "name": "renderView" }, + { + "name": "requiresRefreshOrTraversal" + }, { "name": "resetPreOrderHookFlags" }, diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index e4313b3e406c4..c8b8cc508bc22 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -1553,6 +1553,9 @@ { "name": "renderView" }, + { + "name": "requiresRefreshOrTraversal" + }, { "name": "resetPreOrderHookFlags" }, diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index 195dff896bbf0..b1d84910400dd 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -1523,6 +1523,9 @@ { "name": "requiredValidator" }, + { + "name": "requiresRefreshOrTraversal" + }, { "name": "resetPreOrderHookFlags" }, diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 25d0847c0e3d5..74352ba37f0bf 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -881,6 +881,9 @@ { "name": "renderView" }, + { + "name": "requiresRefreshOrTraversal" + }, { "name": "resetPreOrderHookFlags" }, diff --git a/packages/core/test/bundling/hydration/bundle.golden_symbols.json b/packages/core/test/bundling/hydration/bundle.golden_symbols.json index 8d0425bdf8880..6b3c37c6e43f3 100644 --- a/packages/core/test/bundling/hydration/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hydration/bundle.golden_symbols.json @@ -1220,6 +1220,9 @@ { "name": "renderView" }, + { + "name": "requiresRefreshOrTraversal" + }, { "name": "resetPreOrderHookFlags" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index 3ca528e5ac3cb..9fc2d84eafbd8 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -1881,10 +1881,10 @@ "name": "replaceSegment" }, { - "name": "resetPreOrderHookFlags" + "name": "requiresRefreshOrTraversal" }, { - "name": "resolveData" + "name": "resetPreOrderHookFlags" }, { "name": "resolveForwardRef" @@ -1898,6 +1898,9 @@ { "name": "routes" }, + { + "name": "runInInjectionContext" + }, { "name": "rxSubscriber" }, diff --git a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json index 4f00f629567a6..ab1fddd2ac42b 100644 --- a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json +++ b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json @@ -977,6 +977,9 @@ { "name": "renderView" }, + { + "name": "requiresRefreshOrTraversal" + }, { "name": "resetPreOrderHookFlags" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 17dfc360fa33f..cf86883f5d0f0 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -1334,6 +1334,9 @@ { "name": "renderView" }, + { + "name": "requiresRefreshOrTraversal" + }, { "name": "resetPreOrderHookFlags" }, diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 711d935b3b385..a1f25f6c80e96 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -23,6 +23,7 @@ import { Pipe, PlatformRef, ProviderToken, + runInInjectionContext, Type, ɵconvertToBitFlags as convertToBitFlags, ɵDeferBlockBehavior as DeferBlockBehavior, @@ -557,7 +558,7 @@ export class TestBedImpl implements TestBed { } runInInjectionContext(fn: () => T): T { - return this.inject(EnvironmentInjector).runInContext(fn); + return runInInjectionContext(this.inject(EnvironmentInjector), fn); } execute(tokens: any[], fn: Function, context?: any): any { diff --git a/packages/router/src/operators/check_guards.ts b/packages/router/src/operators/check_guards.ts index 7a02d2be2e30e..d5107dbd21a4a 100644 --- a/packages/router/src/operators/check_guards.ts +++ b/packages/router/src/operators/check_guards.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {EnvironmentInjector, ProviderToken} from '@angular/core'; +import {EnvironmentInjector, ProviderToken, runInInjectionContext} from '@angular/core'; import {concat, defer, from, MonoTypeOperatorFunction, Observable, of, OperatorFunction, pipe} from 'rxjs'; import {concatMap, first, map, mergeMap, tap} from 'rxjs/operators'; @@ -116,7 +116,8 @@ function runCanActivate( const guard = getTokenOrFunctionIdentity(canActivate, closestInjector); const guardVal = isCanActivate(guard) ? guard.canActivate(futureARS, futureRSS) : - closestInjector.runInContext(() => (guard as CanActivateFn)(futureARS, futureRSS)); + runInInjectionContext( + closestInjector, () => (guard as CanActivateFn)(futureARS, futureRSS)); return wrapIntoObservable(guardVal).pipe(first()); }); }); @@ -142,8 +143,8 @@ function runCanActivateChild( canActivateChild, closestInjector); const guardVal = isCanActivateChild(guard) ? guard.canActivateChild(futureARS, futureRSS) : - closestInjector.runInContext( - () => (guard as CanActivateChildFn)(futureARS, futureRSS)); + runInInjectionContext( + closestInjector, () => (guard as CanActivateChildFn)(futureARS, futureRSS)); return wrapIntoObservable(guardVal).pipe(first()); }); return of(guardsMapped).pipe(prioritizedGuardValue()); @@ -162,7 +163,8 @@ function runCanDeactivate( const guard = getTokenOrFunctionIdentity(c, closestInjector); const guardVal = isCanDeactivate(guard) ? guard.canDeactivate(component, currARS, currRSS, futureRSS) : - closestInjector.runInContext( + runInInjectionContext( + closestInjector, () => (guard as CanDeactivateFn)(component, currARS, currRSS, futureRSS)); return wrapIntoObservable(guardVal).pipe(first()); }); @@ -181,7 +183,7 @@ export function runCanLoadGuards( const guard = getTokenOrFunctionIdentity(injectionToken, injector); const guardVal = isCanLoad(guard) ? guard.canLoad(route, segments) : - injector.runInContext(() => (guard as CanLoadFn)(route, segments)); + runInInjectionContext(injector, () => (guard as CanLoadFn)(route, segments)); return wrapIntoObservable(guardVal); }); @@ -214,7 +216,7 @@ export function runCanMatchGuards( const guard = getTokenOrFunctionIdentity(injectionToken, injector); const guardVal = isCanMatch(guard) ? guard.canMatch(route, segments) : - injector.runInContext(() => (guard as CanMatchFn)(route, segments)); + runInInjectionContext(injector, () => (guard as CanMatchFn)(route, segments)); return wrapIntoObservable(guardVal); }); diff --git a/packages/router/src/operators/resolve_data.ts b/packages/router/src/operators/resolve_data.ts index 38ca2c5484267..32279738d749e 100644 --- a/packages/router/src/operators/resolve_data.ts +++ b/packages/router/src/operators/resolve_data.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {EnvironmentInjector, ProviderToken} from '@angular/core'; +import {EnvironmentInjector, ProviderToken, runInInjectionContext} from '@angular/core'; import {EMPTY, from, MonoTypeOperatorFunction, Observable, of, throwError} from 'rxjs'; import {catchError, concatMap, first, map, mapTo, mergeMap, takeLast, tap} from 'rxjs/operators'; @@ -28,21 +28,25 @@ export function resolveData( if (!canActivateChecks.length) { return of(t); } - const routesWithResolversToRun = canActivateChecks.map(check => check.route); - const routesWithResolversSet = new Set(routesWithResolversToRun); - const routesNeedingDataUpdates = - // List all ActivatedRoutes in an array, starting from the parent of the first route to run - // resolvers. We go from the parent because the first route might have siblings that also - // run resolvers. - flattenRouteTree(routesWithResolversToRun[0].parent!) - // Remove the parent from the list -- we do not need to recompute its inherited data - // because no resolvers at or above it run. - .slice(1); + // Iterating a Set in javascript happens in insertion order so it is safe to use a `Set` to + // preserve the correct order that the resolvers should run in. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#description + const routesWithResolversToRun = new Set(canActivateChecks.map(check => check.route)); + const routesNeedingDataUpdates = new Set(); + for (const route of routesWithResolversToRun) { + if (routesNeedingDataUpdates.has(route)) { + continue; + } + // All children under the route with a resolver to run need to recompute inherited data. + for (const newRoute of flattenRouteTree(route)) { + routesNeedingDataUpdates.add(newRoute); + } + } let routesProcessed = 0; return from(routesNeedingDataUpdates) .pipe( concatMap(route => { - if (routesWithResolversSet.has(route)) { + if (routesWithResolversToRun.has(route)) { return runResolve(route, targetSnapshot!, paramsInheritanceStrategy, injector); } else { route.data = getInherited(route, route.parent, paramsInheritanceStrategy).resolve; @@ -51,7 +55,7 @@ export function resolveData( }), tap(() => routesProcessed++), takeLast(1), - mergeMap(_ => routesProcessed === routesNeedingDataUpdates.length ? of(t) : EMPTY), + mergeMap(_ => routesProcessed === routesNeedingDataUpdates.size ? of(t) : EMPTY), ); }); } @@ -106,6 +110,6 @@ function getResolver( const resolver = getTokenOrFunctionIdentity(injectionToken, closestInjector); const resolverValue = resolver.resolve ? resolver.resolve(futureARS, futureRSS) : - closestInjector.runInContext(() => resolver(futureARS, futureRSS)); + runInInjectionContext(closestInjector, () => resolver(futureARS, futureRSS)); return wrapIntoObservable(resolverValue); } diff --git a/packages/router/src/provide_router.ts b/packages/router/src/provide_router.ts index dcb7100eebab0..fc9c84c72d21c 100644 --- a/packages/router/src/provide_router.ts +++ b/packages/router/src/provide_router.ts @@ -7,7 +7,7 @@ */ import {HashLocationStrategy, LOCATION_INITIALIZED, LocationStrategy, ViewportScroller} from '@angular/common'; -import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Component, ComponentRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EnvironmentProviders, inject, InjectFlags, InjectionToken, Injector, makeEnvironmentProviders, NgZone, Provider, Type} from '@angular/core'; +import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Component, ComponentRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EnvironmentProviders, inject, InjectFlags, InjectionToken, Injector, makeEnvironmentProviders, NgZone, Provider, runInInjectionContext, Type} from '@angular/core'; import {of, Subject} from 'rxjs'; import {INPUT_BINDER, RoutedComponentInputBinder} from './directives/router_outlet'; @@ -643,7 +643,7 @@ export function withNavigationErrorHandler(fn: (error: NavigationError) => void) const injector = inject(EnvironmentInjector); inject(Router).events.subscribe((e) => { if (e instanceof NavigationError) { - injector.runInContext(() => fn(e)); + runInInjectionContext(injector, () => fn(e)); } }); } diff --git a/packages/router/test/operators/resolve_data.spec.ts b/packages/router/test/operators/resolve_data.spec.ts index 98e0564f1bf7b..305f9ac82b832 100644 --- a/packages/router/test/operators/resolve_data.spec.ts +++ b/packages/router/test/operators/resolve_data.spec.ts @@ -46,6 +46,44 @@ describe('resolveData operator', () => { expect(TestBed.inject(Router).url).toEqual('/'); }); + it('should run resolvers in different parts of the tree', async () => { + let value = 0; + let bValue = 0; + TestBed.configureTestingModule({ + providers: [provideRouter([ + { + path: 'a', + runGuardsAndResolvers: () => false, + children: [{ + path: '', + resolve: {d0: () => ++value}, + runGuardsAndResolvers: 'always', + children: [], + }], + }, + { + path: 'b', + outlet: 'aux', + runGuardsAndResolvers: () => false, + children: [{ + path: '', + resolve: {d1: () => ++bValue}, + runGuardsAndResolvers: 'always', + children: [], + }] + }, + ])] + }); + const router = TestBed.inject(Router); + const harness = await RouterTestingHarness.create('/a(aux:b)'); + expect(router.routerState.root.children[0]?.firstChild?.snapshot.data).toEqual({d0: 1}); + expect(router.routerState.root.children[1]?.firstChild?.snapshot.data).toEqual({d1: 1}); + + await harness.navigateByUrl('/a(aux:b)#new'); + expect(router.routerState.root.children[0]?.firstChild?.snapshot.data).toEqual({d0: 2}); + expect(router.routerState.root.children[1]?.firstChild?.snapshot.data).toEqual({d1: 2}); + }); + it('should update children inherited data when resolvers run', async () => { let value = 0; TestBed.configureTestingModule({