From 130c84ef7c041a2d0aadf0bbad0da2f9a3fe19f7 Mon Sep 17 00:00:00 2001
From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Mon, 20 Nov 2023 15:44:47 +0100
Subject: [PATCH 01/19] docs: update defer test snippet (#53056) PR Close
#53056
---
adev/src/content/guide/defer.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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!');
});
```
From 54823893c83f48ccf54a769ade2b4163f2e04fee Mon Sep 17 00:00:00 2001
From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Mon, 20 Nov 2023 15:47:59 +0100
Subject: [PATCH 02/19] docs: update defer testing snippet (#53056) PR Close
#53056
---
aio/content/guide/defer.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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!');
});
```
From ed0fbd4071339b1af22d82bac07d51c6c41790cd Mon Sep 17 00:00:00 2001
From: arturovt
Date: Sat, 18 Nov 2023 23:06:14 +0200
Subject: [PATCH 03/19] fix(core): cleanup loading promise when no dependencies
are defined (#53031)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This commit cleans up the `loadingPromise` when no `dependenciesFn` is defined,
as it's already cleaned up after the resolution of `Promise.allSettled`. This
occurs with `prefetch on` triggers, such as when `triggerResourceLoading` is called
from `ɵɵdeferPrefetchOnImmediate`, where there are no dependencies to load. The
`loadingPromise` should still be cleaned up because it typically involves the
`ZoneAwarePromise`, which isn't properly garbage collected when referenced elsewhere
(in this case, it would be referenced from the `tView` data).
PR Close #53031
---
packages/core/src/defer/instructions.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/core/src/defer/instructions.ts b/packages/core/src/defer/instructions.ts
index bb31ab813881f..c6759f3cfe8a1 100644
--- a/packages/core/src/defer/instructions.ts
+++ b/packages/core/src/defer/instructions.ts
@@ -655,10 +655,11 @@ export function triggerResourceLoading(tDetails: TDeferBlockDetails, lView: LVie
}
// The `dependenciesFn` might be `null` when all dependencies within
- // a given defer block were eagerly references elsewhere in a file,
+ // a given defer block were eagerly referenced elsewhere in a file,
// thus no dynamic `import()`s were produced.
if (!dependenciesFn) {
tDetails.loadingPromise = Promise.resolve().then(() => {
+ tDetails.loadingPromise = null;
tDetails.loadingState = DeferDependenciesLoadingState.COMPLETE;
});
return;
From f84057d366c07d2f3f4ab76abe1a0ff55236b9de Mon Sep 17 00:00:00 2001
From: Nicolas Frizzarin
Date: Sat, 18 Nov 2023 17:58:46 +0100
Subject: [PATCH 04/19] docs: example code afterNextRender phase (#53025)
Currently in the angular.dev documentation, afterNextRender function
take directly the phase as a second parameter which is not correct.
According to the api of this hook, the second parameter is an object
which contains the phase. This PR update the part of the documentation
PR Close #53025
---
adev/src/content/guide/components/lifecycle.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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});
}
}
```
From 5564d020cdcea8273b65cf69c45c3f935195af66 Mon Sep 17 00:00:00 2001
From: Jessica Janiuk
Date: Fri, 17 Nov 2023 11:54:20 -0500
Subject: [PATCH 05/19] fix(migrations): Fixes control flow migration if then
else case (#53006)
With if then else use cases, we now properly account for the length
of the original element's contents when tracking new offsets.
fixes: #52927
PR Close #53006
---
.../ng-generate/control-flow-migration/ifs.ts | 4 +-
.../control-flow-migration/util.ts | 11 ++--
.../test/control_flow_migration_spec.ts | 53 +++++++++++++++++++
3 files changed, 63 insertions(+), 5 deletions(-)
diff --git a/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts b/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts
index faa917dc8d1ad..34527b4a9b546 100644
--- a/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts
+++ b/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts
@@ -175,7 +175,9 @@ function buildIfThenElseBlock(
const updatedTmpl = tmplStart + ifThenElseBlock + tmplEnd;
- const pre = originals.start.length - startBlock.length;
+ // We ignore the contents of the element on if then else.
+ // If there's anything there, we need to account for the length in the offset.
+ const pre = originals.start.length + originals.childLength - startBlock.length;
const post = originals.end.length - postBlock.length;
return {tmpl: updatedTmpl, offsets: {pre, post}};
diff --git a/packages/core/schematics/ng-generate/control-flow-migration/util.ts b/packages/core/schematics/ng-generate/control-flow-migration/util.ts
index 35297ce780796..e925b8e0752b2 100644
--- a/packages/core/schematics/ng-generate/control-flow-migration/util.ts
+++ b/packages/core/schematics/ng-generate/control-flow-migration/util.ts
@@ -336,10 +336,12 @@ export function removeImports(
* retrieves the original block of text in the template for length comparison during migration
* processing
*/
-export function getOriginals(
- etm: ElementToMigrate, tmpl: string, offset: number): {start: string, end: string} {
+export function getOriginals(etm: ElementToMigrate, tmpl: string, offset: number):
+ {start: string, end: string, childLength: number} {
// original opening block
if (etm.el.children.length > 0) {
+ const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
+ const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
const start = tmpl.slice(
etm.el.sourceSpan.start.offset - offset,
etm.el.children[0].sourceSpan.start.offset - offset);
@@ -347,13 +349,14 @@ export function getOriginals(
const end = tmpl.slice(
etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset,
etm.el.sourceSpan.end.offset - offset);
- return {start, end};
+ const childLength = childEnd - childStart;
+ return {start, end, childLength};
}
// self closing or no children
const start =
tmpl.slice(etm.el.sourceSpan.start.offset - offset, etm.el.sourceSpan.end.offset - offset);
// original closing block
- return {start, end: ''};
+ return {start, end: '', childLength: 0};
}
function isI18nTemplate(etm: ElementToMigrate, i18nAttr: Attribute|undefined): boolean {
diff --git a/packages/core/schematics/test/control_flow_migration_spec.ts b/packages/core/schematics/test/control_flow_migration_spec.ts
index 6a67ad659141c..00107f3b9aca8 100644
--- a/packages/core/schematics/test/control_flow_migration_spec.ts
+++ b/packages/core/schematics/test/control_flow_migration_spec.ts
@@ -828,6 +828,59 @@ describe('control flow migration', () => {
].join('\n'));
});
+ it('should migrate a complex if then else case on ng-containers', async () => {
+ writeFile('/comp.ts', `
+ import {Component} from '@angular/core';
+ import {NgIf} from '@angular/common';
+
+ @Component({
+ templateUrl: './comp.html'
+ })
+ class Comp {
+ show = false;
+ }
+ `);
+
+ writeFile('/comp.html', [
+ ``,
+ ` `,
+ ` `,
+ ``,
+ ` `,
+ ` `,
+ ` `,
+ ``,
+ ``,
+ ` Empty`,
+ ``,
+ ``,
+ ``,
+ `
`,
+ ].join('\n'));
+ });
});
describe('nested structures', () => {
From 7affa5775427e92ef6e949c879765b7c8aa172da Mon Sep 17 00:00:00 2001
From: arturovt
Date: Fri, 17 Nov 2023 12:23:12 +0200
Subject: [PATCH 07/19] fix(common): scan images once page is loaded (#52991)
This commit updates the implementation of the `ImagePerformanceWarning` and
runs the image scan even if the page has already been loaded. The `window.load`
event would never fire if the page has already been loaded; that's why we're
checking for the document's ready state.
PR Close #52991
---
packages/core/src/image_performance_warning.ts | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/packages/core/src/image_performance_warning.ts b/packages/core/src/image_performance_warning.ts
index 7bc0c2194d91d..1a467b7661999 100644
--- a/packages/core/src/image_performance_warning.ts
+++ b/packages/core/src/image_performance_warning.ts
@@ -22,7 +22,6 @@ const SCAN_DELAY = 200;
const OVERSIZED_IMAGE_TOLERANCE = 1200;
-
@Injectable({providedIn: 'root'})
export class ImagePerformanceWarning implements OnDestroy {
// Map of full image URLs -> original `ngSrc` values.
@@ -38,7 +37,8 @@ export class ImagePerformanceWarning implements OnDestroy {
return;
}
this.observer = this.initPerformanceObserver();
- const win = getDocument().defaultView;
+ const doc = getDocument();
+ const win = doc.defaultView;
if (typeof win !== 'undefined') {
this.window = win;
// Wait to avoid race conditions where LCP image triggers
@@ -49,7 +49,16 @@ export class ImagePerformanceWarning implements OnDestroy {
// Angular doesn't have to run change detection whenever any asynchronous tasks are invoked in
// the scope of this functionality.
this.ngZone.runOutsideAngular(() => {
- this.window?.addEventListener('load', waitToScan, {once: true});
+ // Consider the case when the application is created and destroyed multiple times.
+ // Typically, applications are created instantly once the page is loaded, and the
+ // `window.load` listener is always triggered. However, the `window.load` event will never
+ // be fired if the page is loaded, and the application is created later. Checking for
+ // `readyState` is the easiest way to determine whether the page has been loaded or not.
+ if (doc.readyState === 'complete') {
+ waitToScan();
+ } else {
+ this.window?.addEventListener('load', waitToScan, {once: true});
+ }
});
}
}
From 29c5416d14638a05a894269aa5dbe67e98754418 Mon Sep 17 00:00:00 2001
From: arturovt
Date: Fri, 17 Nov 2023 11:53:39 +0200
Subject: [PATCH 08/19] fix(common): remove `load` on image once it fails to
load (#52990)
This commit adds an `error` listener to image elements and removes both
`load` and `error` listeners once the image loads or fails to load. The `load`
listener would never have been removed if the image failed to load.
PR Close #52990
---
.../ng_optimized_image/ng_optimized_image.ts | 25 ++++++++++++++++---
1 file changed, 21 insertions(+), 4 deletions(-)
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();
+ });
}
/**
From e33f6e0f1a483cad908fa6d7376d62332797499c Mon Sep 17 00:00:00 2001
From: anthonyfr75
Date: Sun, 19 Nov 2023 18:28:59 +0100
Subject: [PATCH 09/19] fix(migrations): control flow migration fails for async
pipe with unboxing of observable (#52756) (#52972)
Update control flow syntax to use 'as' for proper async pipe handling, accounting for variable whitespace before 'let'.
Fixes #52756
PR Close #52972
---
.../ng-generate/control-flow-migration/ifs.ts | 15 ++++-
.../test/control_flow_migration_spec.ts | 64 ++++++++++++++++++-
2 files changed, 75 insertions(+), 4 deletions(-)
diff --git a/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts b/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts
index 34527b4a9b546..959623a4820bf 100644
--- a/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts
+++ b/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts
@@ -84,7 +84,10 @@ function migrateNgIf(etm: ElementToMigrate, tmpl: string, offset: number): Resul
function buildIfBlock(etm: ElementToMigrate, tmpl: string, offset: number): Result {
// includes the mandatory semicolon before as
const lbString = etm.hasLineBreaks ? '\n' : '';
- const condition = etm.attr.value.replace(' as ', '; as ');
+ const condition = etm.attr.value
+ .replace(' as ', '; as ')
+ // replace 'let' with 'as' whatever spaces are between ; and 'let'
+ .replace(/;\s*let/g, '; as');
const originals = getOriginals(etm, tmpl, offset);
@@ -106,7 +109,10 @@ function buildIfBlock(etm: ElementToMigrate, tmpl: string, offset: number): Resu
function buildStandardIfElseBlock(
etm: ElementToMigrate, tmpl: string, elseString: string, offset: number): Result {
// includes the mandatory semicolon before as
- const condition = etm.getCondition(elseString).replace(' as ', '; as ');
+ const condition = etm.getCondition(elseString)
+ .replace(' as ', '; as ')
+ // replace 'let' with 'as' whatever spaces are between ; and 'let'
+ .replace(/;\s*let/g, '; as');
const elsePlaceholder = `#${etm.getTemplateName(elseString)}|`;
return buildIfElseBlock(etm, tmpl, condition, elsePlaceholder, offset);
}
@@ -151,7 +157,10 @@ function buildStandardIfThenElseBlock(
etm: ElementToMigrate, tmpl: string, thenString: string, elseString: string,
offset: number): Result {
// includes the mandatory semicolon before as
- const condition = etm.getCondition(thenString).replace(' as ', '; as ');
+ const condition = etm.getCondition(thenString)
+ .replace(' as ', '; as ')
+ // replace 'let' with 'as' whatever spaces are between ; and 'let'
+ .replace(/;\s*let/g, '; as');
const thenPlaceholder = `#${etm.getTemplateName(thenString, elseString)}|`;
const elsePlaceholder = `#${etm.getTemplateName(elseString)}|`;
return buildIfThenElseBlock(etm, tmpl, condition, thenPlaceholder, elsePlaceholder, offset);
diff --git a/packages/core/schematics/test/control_flow_migration_spec.ts b/packages/core/schematics/test/control_flow_migration_spec.ts
index 5452e32edae20..c06e74cd898ef 100644
--- a/packages/core/schematics/test/control_flow_migration_spec.ts
+++ b/packages/core/schematics/test/control_flow_migration_spec.ts
@@ -100,7 +100,6 @@ describe('control flow migration', () => {
} catch (e: any) {
error = e.message;
}
-
expect(error).toBe('Cannot run control flow migration outside of the current project.');
});
@@ -3513,5 +3512,68 @@ describe('control flow migration', () => {
expect(content).toContain(
'template: `
@if (toggle) {
@if (show) {shrug}
}
`');
});
+
+ it('should update let value in a build if block to as value for the new control flow',
+ async () => {
+ writeFile('/comp.ts', `
+ import {Component} from '@angular/core';
+ import {NgIf} from '@angular/common';
+
+ @Component({
+ imports: [NgIf],
+ template: \` {{value}} \`
+ })
+ class Comp {
+ value$ = of('Rica');
+ }
+ `);
+
+ await runMigration();
+ const content = tree.readContent('/comp.ts');
+ expect(content).toContain('template: `@if (value$ | async; as value) { {{value}} }`');
+ });
+
+ it('should update let value in a standard if else block to as value for the new control flow',
+ async () => {
+ writeFile('/comp.ts', `
+ import {Component} from '@angular/core';
+ import {CommonModule} from '@angular/common';
+
+ @Component({
+ imports: [CommonModule],
+ template: \`