diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e78124783414..f0fd98cc2e02a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ + +# 17.1.0-next.0 (2023-11-15) +### compiler-cli +| Commit | Type | Description | +| -- | -- | -- | +| [56a76d73e0](https://github.com/angular/angular/commit/56a76d73e037aeea1975808d5c51608fd23d4fa6) | fix | modify `getConstructorDependencies` helper to work with reflection host after the previous change ([#52215](https://github.com/angular/angular/pull/52215)) | +### core +| Commit | Type | Description | +| -- | -- | -- | +| [94096c6ede](https://github.com/angular/angular/commit/94096c6ede67436a349ae07901f2bb418bf9f461) | feat | support TypeScript 5.3 ([#52572](https://github.com/angular/angular/pull/52572)) | +| [bdd61c768a](https://github.com/angular/angular/commit/bdd61c768a28b56c68634b99c036986499829f45) | fix | replace assertion with more intentional error ([#52234](https://github.com/angular/angular/pull/52234)) | +### router +| Commit | Type | Description | +| -- | -- | -- | +| [726530a9af](https://github.com/angular/angular/commit/726530a9af9c8daf7295cc3548f24e70f380d70e) | feat | Allow `onSameUrlNavigation: 'ignore'` in `navigateByUrl` ([#52265](https://github.com/angular/angular/pull/52265)) | + + + + +# 17.0.3 (2023-11-15) +### animations +| Commit | Type | Description | +| -- | -- | -- | +| [f5872c9921](https://github.com/angular/angular/commit/f5872c992181a2c231890b83f92ec03ec9606802) | fix | prevent the AsyncAnimationRenderer from calling the delegate when there is no element. ([#52570](https://github.com/angular/angular/pull/52570)) | +### core +| Commit | Type | Description | +| -- | -- | -- | +| [6a1d4ed667](https://github.com/angular/angular/commit/6a1d4ed6670f5965a654e40997aa266a99925f50) | fix | handle non-container environment injector cases ([#52774](https://github.com/angular/angular/pull/52774)) | +| [5de7575be8](https://github.com/angular/angular/commit/5de7575be83b9829e65ad245034ee7ab1d966044) | fix | reset cached scope for components that were overridden using TestBed ([#52916](https://github.com/angular/angular/pull/52916)) | +### http +| Commit | Type | Description | +| -- | -- | -- | +| [7c066a4af4](https://github.com/angular/angular/commit/7c066a4af4faae25ee722c19576c63c3833066ee) | fix | Use the response `content-type` to set the blob `type`. ([#52840](https://github.com/angular/angular/pull/52840)) | +### migrations +| Commit | Type | Description | +| -- | -- | -- | +| [4e200bf13b](https://github.com/angular/angular/commit/4e200bf13b284fa89bbb0854cbb85dc8fe94d8bb) | fix | Add missing support for ngForOf ([#52903](https://github.com/angular/angular/pull/52903)) | +| [d033540d0f](https://github.com/angular/angular/commit/d033540d0f874a7a05b79c00e3151ed076fa71c3) | fix | Add support for bound versions of NgIfElse and NgIfThenElse ([#52869](https://github.com/angular/angular/pull/52869)) | +| [aa2d815648](https://github.com/angular/angular/commit/aa2d815648dbf3303cfe72bf976a4a87de406ee0) | fix | Add support for removing imports post migration ([#52763](https://github.com/angular/angular/pull/52763)) | +| [3831942771](https://github.com/angular/angular/commit/38319427711f4dab4e4d64ff48aecc7727085031) | fix | Fixes issue with multiple if elses with same template ([#52863](https://github.com/angular/angular/pull/52863)) | +| [e1f84a31dc](https://github.com/angular/angular/commit/e1f84a31dcac413251329c3b695a253234c6aae6) | fix | passed in paths will be respected in nx workspaces ([#52796](https://github.com/angular/angular/pull/52796)) | + + + # 17.0.2 (2023-11-09) ### compiler-cli diff --git a/devtools/projects/shell-browser/src/manifest/manifest.chrome.json b/devtools/projects/shell-browser/src/manifest/manifest.chrome.json index 68106d51a9b1a..f1b6e36cb52b8 100644 --- a/devtools/projects/shell-browser/src/manifest/manifest.chrome.json +++ b/devtools/projects/shell-browser/src/manifest/manifest.chrome.json @@ -3,8 +3,8 @@ "short_name": "Angular DevTools", "name": "Angular DevTools", "description": "Angular DevTools extends Chrome DevTools adding Angular specific debugging and profiling capabilities.", - "version": "1.0.8", - "version_name": "1.0.8", + "version": "1.0.9", + "version_name": "1.0.9", "minimum_chrome_version": "102", "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" diff --git a/devtools/projects/shell-browser/src/manifest/manifest.firefox.json b/devtools/projects/shell-browser/src/manifest/manifest.firefox.json index 70bf635d3e301..d0620740c8e73 100644 --- a/devtools/projects/shell-browser/src/manifest/manifest.firefox.json +++ b/devtools/projects/shell-browser/src/manifest/manifest.firefox.json @@ -3,7 +3,7 @@ "short_name": "Angular DevTools", "name": "Angular DevTools", "description": "Angular DevTools extends Firefox DevTools adding Angular specific debugging and profiling capabilities.", - "version": "1.0.8", + "version": "1.0.9", "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", "icons": { "16": "assets/icon16.png", diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for_template.js index fd44ceaa68f43..6967d3029f933 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_for_template.js @@ -18,6 +18,7 @@ function MyApp_Template(rf, ctx) { if (rf & 2) { $r3$.ɵɵadvance(1); $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); - $r3$.ɵɵrepeater(2, ctx.items); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater(ctx.items); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables_template.js index 4fca43229b52a..83ca7b4adf501 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_aliased_template_variables_template.js @@ -19,6 +19,7 @@ function MyApp_Template(rf, ctx) { if (rf & 2) { $r3$.ɵɵadvance(1); $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); - $r3$.ɵɵrepeater(2, ctx.items); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater(ctx.items); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots_template.js index 8a8fd335f5727..4787e36332a31 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_data_slots_template.js @@ -5,6 +5,7 @@ function MyApp_Template(rf, ctx) { $r3$.ɵɵtemplate(4, MyApp_ng_template_4_Template, 0, 0, "ng-template"); } if (rf & 2) { - $r3$.ɵɵrepeater(1, ctx.items); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater(ctx.items); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_impure_track_reuse_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_impure_track_reuse_template.js index 8c0e1e1172a56..7107664c2d911 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_impure_track_reuse_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_impure_track_reuse_template.js @@ -8,7 +8,8 @@ function MyApp_Template(rf, ctx) { $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $_forTrack0$, true); } if (rf & 2) { - $r3$.ɵɵrepeater(0, ctx.items); - $r3$.ɵɵrepeater(2, ctx.otherItems); + $r3$.ɵɵrepeater(ctx.items); + $r3$.ɵɵadvance(2); + $r3$.ɵɵrepeater(ctx.otherItems); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_pure_track_reuse_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_pure_track_reuse_template.js index 9591ce6e32a85..6d900b5c362c7 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_pure_track_reuse_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_pure_track_reuse_template.js @@ -6,7 +6,8 @@ function MyApp_Template(rf, ctx) { $r3$.ɵɵrepeaterCreate(2, MyApp_For_3_Template, 1, 1, null, null, $_forTrack0$); } if (rf & 2) { - $r3$.ɵɵrepeater(0, ctx.items); - $r3$.ɵɵrepeater(2, ctx.otherItems); + $r3$.ɵɵrepeater(ctx.items); + $r3$.ɵɵadvance(2); + $r3$.ɵɵrepeater(ctx.otherItems); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener_template.js index 45846900ac0b9..7b959f5a4b821 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_listener_template.js @@ -23,6 +23,7 @@ function MyApp_Template(rf, ctx) { if (rf & 2) { $r3$.ɵɵadvance(1); $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); - $r3$.ɵɵrepeater(2, ctx.items); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater(ctx.items); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_scope_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_scope_template.js index a94b4d6936ca8..b28a236753177 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_scope_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_scope_template.js @@ -17,8 +17,9 @@ function MyApp_Template(rf, ctx) { } if (rf & 2) { $r3$.ɵɵtextInterpolate4(" ", ctx.$index, " ", ctx.$count, " ", ctx.$first, " ", ctx.$last, " "); - $r3$.ɵɵrepeater(1, ctx.items); - $r3$.ɵɵadvance(3); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater(ctx.items); + $r3$.ɵɵadvance(2); $r3$.ɵɵtextInterpolate4(" ", ctx.$index, " ", ctx.$count, " ", ctx.$first, " ", ctx.$last, " "); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_template.js index 6399e6cddf7ef..f7fab8bfe8a28 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_template_variables_template.js @@ -19,6 +19,7 @@ function MyApp_Template(rf, ctx) { if (rf & 2) { $r3$.ɵɵadvance(1); $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); - $r3$.ɵɵrepeater(2, ctx.items); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater(ctx.items); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field_template.js index 52a63bffe47c9..77e263289aacb 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_field_template.js @@ -19,6 +19,7 @@ function MyApp_Template(rf, ctx) { if (rf & 2) { $r3$.ɵɵadvance(1); $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); - $r3$.ɵɵrepeater(2, ctx.items); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater(ctx.items); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index_template.js index adc50556651dc..a28345298fdde 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_by_index_template.js @@ -18,6 +18,7 @@ function MyApp_Template(rf, ctx) { if (rf & 2) { $r3$.ɵɵadvance(1); $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); - $r3$.ɵɵrepeater(2, ctx.items); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater(ctx.items); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_literals_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_literals_template.js index 83c18b778068a..43e0ccd8f8571 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_literals_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_track_literals_template.js @@ -10,6 +10,6 @@ function MyApp_Template(rf, ctx) { $r3$.ɵɵrepeaterCreate(0, MyApp_For_1_Template, 1, 1, null, null, $_forTrack0$, true); } if (rf & 2) { - $r3$.ɵɵrepeater(0, ctx.items); + $r3$.ɵɵrepeater(ctx.items); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty_template.js index 59273aaa1add2..05ce09f3d27f0 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_empty_template.js @@ -24,6 +24,7 @@ function MyApp_Template(rf, ctx) { if (rf & 2) { $r3$.ɵɵadvance(1); $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); - $r3$.ɵɵrepeater(2, ctx.items); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater(ctx.items); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_pipe_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_pipe_template.js index 11a64cfb7d004..4ccab1e98630b 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_pipe_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/for_with_pipe_template.js @@ -9,6 +9,7 @@ function MyApp_Template(rf, ctx) { if (rf & 2) { $r3$.ɵɵadvance(1); $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); - $r3$.ɵɵrepeater(2, $r3$.ɵɵpipeBind1(4, 1, ctx.items)); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater($r3$.ɵɵpipeBind1(4, 1, ctx.items)); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template.js index 0f82c2acdbe49..c94011a2e3da8 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template.js @@ -17,7 +17,8 @@ function MyApp_For_3_Template(rf, ctx) { if (rf & 2) { const $item_r1$ = ctx.$implicit; $r3$.ɵɵtextInterpolate1(" ", $item_r1$.name, " "); - $r3$.ɵɵrepeater(1, $item_r1$.subItems); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater($item_r1$.subItems); } } … @@ -31,6 +32,7 @@ function MyApp_Template(rf, ctx) { if (rf & 2) { $r3$.ɵɵadvance(1); $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); - $r3$.ɵɵrepeater(2, ctx.items); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater(ctx.items); } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables_template.js index 12bbc9d44a88c..c2ee7b180d1f8 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_for_template_variables_template.js @@ -17,7 +17,8 @@ function MyApp_For_3_Template(rf, ctx) { if (rf & 2) { const $item_r1$ = ctx.$implicit; $r3$.ɵɵtextInterpolate1(" ", $item_r1$.name, " "); - $r3$.ɵɵrepeater(1, $item_r1$.subItems); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater($item_r1$.subItems); } } … @@ -31,6 +32,7 @@ function MyApp_Template(rf, ctx) { if (rf & 2) { $r3$.ɵɵadvance(1); $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); - $r3$.ɵɵrepeater(2, ctx.items); + $r3$.ɵɵadvance(1); + $r3$.ɵɵrepeater(ctx.items); } } diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 2a89c7ddd7343..01da9d2fe8548 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -1574,10 +1574,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // because its value isn't stored in the LView. const value = block.expression.visit(this._valueConverter); - // `repeater(0, iterable)` - this.updateInstruction( - block.sourceSpan, R3.repeater, - () => [o.literal(blockIndex), this.convertPropertyBinding(value)]); + // `advance(x); repeater(iterable)` + this.updateInstructionWithAdvance( + blockIndex, block.sourceSpan, R3.repeater, () => [this.convertPropertyBinding(value)]); } private registerComputedLoopVariables(block: t.ForLoopBlock, bindingScope: BindingScope): void { diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/update.ts b/packages/compiler/src/template/pipeline/ir/src/ops/update.ts index b06533c05894b..a57aaf7f64efb 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/update.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/update.ts @@ -534,7 +534,7 @@ export function createConditionalOp( }; } -export interface RepeaterOp extends Op { +export interface RepeaterOp extends Op, DependsOnSlotContextOpTrait { kind: OpKind.Repeater; /** @@ -562,6 +562,7 @@ export function createRepeaterOp( collection, sourceSpan, ...NEW_OP, + ...TRAIT_DEPENDS_ON_SLOT_CONTEXT, }; } diff --git a/packages/compiler/src/template/pipeline/src/instruction.ts b/packages/compiler/src/template/pipeline/src/instruction.ts index 33c049ff45756..70ebf976f8797 100644 --- a/packages/compiler/src/template/pipeline/src/instruction.ts +++ b/packages/compiler/src/template/pipeline/src/instruction.ts @@ -293,9 +293,8 @@ export function repeaterCreate( return call(Identifiers.repeaterCreate, args, sourceSpan); } -export function repeater( - metadataSlot: number, collection: o.Expression, sourceSpan: ParseSourceSpan|null): ir.UpdateOp { - return call(Identifiers.repeater, [o.literal(metadataSlot), collection], sourceSpan); +export function repeater(collection: o.Expression, sourceSpan: ParseSourceSpan|null): ir.UpdateOp { + return call(Identifiers.repeater, [collection], sourceSpan); } export function deferWhen( diff --git a/packages/compiler/src/template/pipeline/src/phases/reify.ts b/packages/compiler/src/template/pipeline/src/phases/reify.ts index 60a66a8d4e379..38f77f0af173a 100644 --- a/packages/compiler/src/template/pipeline/src/phases/reify.ts +++ b/packages/compiler/src/template/pipeline/src/phases/reify.ts @@ -347,7 +347,7 @@ function reifyUpdateOperations(_unit: CompilationUnit, ops: ir.OpList 0) ? `;${aliases.join(';')}` : ''; + + let trackBy = forAttrs.item; + if (forAttrs.trackBy !== '') { + // build trackby value + trackBy = `${forAttrs.trackBy.trim()}(${aliasedIndex}, ${forAttrs.item})`; + } + + const {start, middle, end} = getMainBlock(etm, tmpl, offset); + const startBlock = `@for (${condition}; track ${trackBy}${aliasStr}) {\n ${start}`; + + const endBlock = `${end}\n}`; + const forBlock = startBlock + middle + endBlock; + + const updatedTmpl = tmpl.slice(0, etm.start(offset)) + forBlock + tmpl.slice(etm.end(offset)); + + const pre = originals.start.length - startBlock.length; + const post = originals.end.length - endBlock.length; + + return {tmpl: updatedTmpl, offsets: {pre, post}}; +} + function getNgForParts(expression: string): string[] { const parts: string[] = []; const commaSeparatedStack: string[] = []; 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 5bcdc0407eea8..faa917dc8d1ad 100644 --- a/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts +++ b/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts @@ -15,7 +15,7 @@ export const ngif = '*ngIf'; export const boundngif = '[ngIf]'; export const nakedngif = 'ngIf'; -export const ifs = [ +const ifs = [ ngif, nakedngif, boundngif, diff --git a/packages/core/schematics/ng-generate/control-flow-migration/switches.ts b/packages/core/schematics/ng-generate/control-flow-migration/switches.ts index 50ba91948a993..9647189cdd090 100644 --- a/packages/core/schematics/ng-generate/control-flow-migration/switches.ts +++ b/packages/core/schematics/ng-generate/control-flow-migration/switches.ts @@ -18,7 +18,7 @@ export const nakedcase = 'ngSwitchCase'; export const switchdefault = '*ngSwitchDefault'; export const nakeddefault = 'ngSwitchDefault'; -export const switches = [ +const switches = [ ngswitch, boundcase, switchcase, diff --git a/packages/core/schematics/ng-generate/control-flow-migration/types.ts b/packages/core/schematics/ng-generate/control-flow-migration/types.ts index 818416030cd2d..f4bfaaa41cb0b 100644 --- a/packages/core/schematics/ng-generate/control-flow-migration/types.ts +++ b/packages/core/schematics/ng-generate/control-flow-migration/types.ts @@ -12,6 +12,7 @@ import ts from 'typescript'; export const ngtemplate = 'ng-template'; export const boundngifelse = '[ngIfElse]'; export const boundngifthenelse = '[ngIfThenElse]'; +export const nakedngfor = 'ngFor'; function allFormsOf(selector: string): string[] { return [ @@ -30,6 +31,10 @@ const commonModuleDirectives = new Set([ ...allFormsOf('ngStyle'), ...allFormsOf('ngTemplateOutlet'), ...allFormsOf('ngComponentOutlet'), + '[NgForOf]', + '[NgForTrackBy]', + '[ngIfElse]', + '[ngIfThenElse]', ]); function pipeMatchRegExpFor(name: string): RegExp { @@ -72,6 +77,13 @@ export type Result = { offsets: Offsets, }; +export interface ForAttributes { + forOf: string; + trackBy: string; + item: string; + aliases: Map; +} + /** * Represents an error that happened during migration */ @@ -88,16 +100,18 @@ export class ElementToMigrate { attr: Attribute; elseAttr: Attribute|undefined; thenAttr: Attribute|undefined; + forAttrs: ForAttributes|undefined; nestCount = 0; hasLineBreaks = false; constructor( el: Element, attr: Attribute, elseAttr: Attribute|undefined = undefined, - thenAttr: Attribute|undefined = undefined) { + thenAttr: Attribute|undefined = undefined, forAttrs: ForAttributes|undefined = undefined) { this.el = el; this.attr = attr; this.elseAttr = elseAttr; this.thenAttr = thenAttr; + this.forAttrs = forAttrs; } getCondition(targetStr: string): string { @@ -236,12 +250,38 @@ export class ElementCollector extends RecursiveVisitor { if (this._attributes.includes(attr.name)) { const elseAttr = el.attrs.find(x => x.name === boundngifelse); const thenAttr = el.attrs.find(x => x.name === boundngifthenelse); - this.elements.push(new ElementToMigrate(el, attr, elseAttr, thenAttr)); + const forAttrs = attr.name === nakedngfor ? this.getForAttrs(el) : undefined; + this.elements.push(new ElementToMigrate(el, attr, elseAttr, thenAttr, forAttrs)); } } } super.visitElement(el, null); } + + private getForAttrs(el: Element): ForAttributes { + const aliases = new Map(); + let item = ''; + let trackBy = ''; + let forOf = ''; + for (const attr of el.attrs) { + if (attr.name === '[ngForTrackBy]') { + trackBy = attr.value; + } + if (attr.name === '[ngForOf]') { + forOf = attr.value; + } + if (attr.name.startsWith('let-')) { + if (attr.value === '') { + // item + item = attr.name.replace('let-', ''); + } else { + // alias + aliases.set(attr.name.replace('let-', ''), attr.value); + } + } + } + return {forOf, trackBy, item, aliases}; + } } /** Finds all elements with ngif structural directives. */ 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 e9fbe29be9f0f..a3379cc85bd06 100644 --- a/packages/core/schematics/ng-generate/control-flow-migration/util.ts +++ b/packages/core/schematics/ng-generate/control-flow-migration/util.ts @@ -12,7 +12,10 @@ import ts from 'typescript'; import {AnalyzedFile, CommonCollector, ElementCollector, ElementToMigrate, Template, TemplateCollector} from './types'; -const importRemovals = ['NgIf', 'NgFor', 'NgSwitch', 'NgSwitchCase', 'NgSwitchDefault']; +const importRemovals = [ + 'NgIf', 'NgIfElse', 'NgIfThenElse', 'NgFor', 'NgForOf', 'NgForTrackBy', 'NgSwitch', + 'NgSwitchCase', 'NgSwitchDefault' +]; const importWithCommonRemovals = [...importRemovals, 'CommonModule']; /** @@ -353,26 +356,34 @@ export function getOriginals( return {start, end: ''}; } +function isI18nTemplate(etm: ElementToMigrate, i18nAttr: Attribute|undefined): boolean { + return etm.el.name === 'ng-template' && i18nAttr !== undefined && + (etm.el.attrs.length === 2 || (etm.el.attrs.length === 3 && etm.elseAttr !== undefined)); +} + +function isRemovableContainer(etm: ElementToMigrate): boolean { + return (etm.el.name === 'ng-container' || etm.el.name === 'ng-template') && + (etm.el.attrs.length === 1 || etm.forAttrs !== undefined || + (etm.el.attrs.length === 2 && etm.elseAttr !== undefined) || + (etm.el.attrs.length === 3 && etm.elseAttr !== undefined && etm.thenAttr !== undefined)); +} + /** * builds the proper contents of what goes inside a given control flow block after migration */ export function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number): {start: string, middle: string, end: string} { const i18nAttr = etm.el.attrs.find(x => x.name === 'i18n'); - if ((etm.el.name === 'ng-container' || etm.el.name === 'ng-template') && - (etm.el.attrs.length === 1 || (etm.el.attrs.length === 2 && etm.elseAttr !== undefined) || - (etm.el.attrs.length === 3 && etm.elseAttr !== undefined && etm.thenAttr !== undefined))) { + if (isRemovableContainer(etm)) { // this is the case where we're migrating and there's no need to keep the ng-container 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 middle = tmpl.slice(childStart, childEnd); return {start: '', middle, end: ''}; - } else if ( - etm.el.name === 'ng-template' && i18nAttr !== undefined && - (etm.el.attrs.length === 2 || (etm.el.attrs.length === 3 && etm.elseAttr !== undefined))) { + } else if (isI18nTemplate(etm, i18nAttr)) { 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 middle = wrapIntoI18nContainer(i18nAttr, tmpl.slice(childStart, childEnd)); + const middle = wrapIntoI18nContainer(i18nAttr!, tmpl.slice(childStart, childEnd)); return {start: '', middle, end: ''}; } diff --git a/packages/core/schematics/test/control_flow_migration_spec.ts b/packages/core/schematics/test/control_flow_migration_spec.ts index f7ad20a42e6b3..d5f172b45c014 100644 --- a/packages/core/schematics/test/control_flow_migration_spec.ts +++ b/packages/core/schematics/test/control_flow_migration_spec.ts @@ -1585,6 +1585,208 @@ describe('control flow migration', () => { }); }); + describe('ngForOf', () => { + it('should migrate a basic ngForOf', async () => { + writeFile('/comp.ts', ` + import {Component} from '@angular/core'; + import {NgFor} from '@angular/common'; + interface Item { + id: number; + text: string; + } + + @Component({ + imports: [NgFor,NgForOf], + templateUrl: 'comp.html', + }) + class Comp { + items: Item[] = [{id: 1, text: 'blah'},{id: 2, text: 'stuff'}]; + } + `); + + writeFile('/comp.html', [ + ``, + ` `, + ` {{rowData}}`, + ` `, + ``, + ].join('\n')); + + await runMigration(); + const actual = tree.readContent('/comp.html'); + + const expected = [ + ``, + ` @for (rowData of things; track rowData) {\n `, + ` {{rowData}}\n `, + `}`, + ``, + ].join('\n'); + + expect(actual).toBe(expected); + }); + + it('should migrate ngForOf with an alias', async () => { + writeFile('/comp.ts', ` + import {Component} from '@angular/core'; + import {NgFor} from '@angular/common'; + interface Item { + id: number; + text: string; + } + + @Component({ + imports: [NgFor,NgForOf], + templateUrl: 'comp.html', + }) + class Comp { + items: Item[] = [{id: 1, text: 'blah'},{id: 2, text: 'stuff'}]; + } + `); + + writeFile('/comp.html', [ + ``, + ` `, + ` {{rowIndex}}{{rowData}}`, + ` `, + ``, + ].join('\n')); + + await runMigration(); + const actual = tree.readContent('/comp.html'); + + const expected = [ + ``, + ` @for (rowData of things; track rowData; let rowIndex = $index) {\n `, + ` {{rowIndex}}{{rowData}}\n `, + `}`, + ``, + ].join('\n'); + + expect(actual).toBe(expected); + }); + + it('should migrate ngForOf with track by', async () => { + writeFile('/comp.ts', ` + import {Component} from '@angular/core'; + import {NgFor} from '@angular/common'; + interface Item { + id: number; + text: string; + } + + @Component({ + imports: [NgFor,NgForOf], + templateUrl: 'comp.html', + }) + class Comp { + items: Item[] = [{id: 1, text: 'blah'},{id: 2, text: 'stuff'}]; + } + `); + + writeFile('/comp.html', [ + ``, + ` `, + ` {{rowData}}`, + ` `, + ``, + ].join('\n')); + + await runMigration(); + const actual = tree.readContent('/comp.html'); + + const expected = [ + ``, + ` @for (rowData of things; track trackMe($index, rowData)) {\n `, + ` {{rowData}}\n `, + `}`, + ``, + ].join('\n'); + + expect(actual).toBe(expected); + }); + + it('should migrate ngForOf with track by and alias', async () => { + writeFile('/comp.ts', ` + import {Component} from '@angular/core'; + import {NgFor} from '@angular/common'; + interface Item { + id: number; + text: string; + } + + @Component({ + imports: [NgFor,NgForOf], + templateUrl: 'comp.html', + }) + class Comp { + items: Item[] = [{id: 1, text: 'blah'},{id: 2, text: 'stuff'}]; + } + `); + + writeFile('/comp.html', [ + ``, + ` `, + ` {{rowIndex}}{{rowData}}`, + ` `, + ``, + ].join('\n')); + + await runMigration(); + const actual = tree.readContent('/comp.html'); + + const expected = [ + ``, + ` @for (rowData of things; track trackMe(rowIndex, rowData); let rowIndex = $index) {\n `, + ` {{rowIndex}}{{rowData}}\n `, + `}`, + ``, + ].join('\n'); + + expect(actual).toBe(expected); + }); + + it('should migrate ngForOf with track by and multiple aliases', async () => { + writeFile('/comp.ts', ` + import {Component} from '@angular/core'; + import {NgFor} from '@angular/common'; + interface Item { + id: number; + text: string; + } + + @Component({ + imports: [NgFor,NgForOf], + templateUrl: 'comp.html', + }) + class Comp { + items: Item[] = [{id: 1, text: 'blah'},{id: 2, text: 'stuff'}]; + } + `); + + writeFile('/comp.html', [ + ``, + ` `, + ` {{rowIndex}}{{rowData}}`, + ` `, + ``, + ].join('\n')); + + await runMigration(); + const actual = tree.readContent('/comp.html'); + + const expected = [ + ``, + ` @for (rowData of things; track trackMe(rowIndex, rowData); let rowIndex = $index; let rCount = $count) {\n `, + ` {{rowIndex}}{{rowData}}\n `, + `}`, + ``, + ].join('\n'); + + expect(actual).toBe(expected); + }); + }); + describe('ngSwitch', () => { it('should migrate an inline template', async () => { writeFile( diff --git a/packages/core/src/render3/instructions/control_flow.ts b/packages/core/src/render3/instructions/control_flow.ts index 11acac874342c..5d32b5ef3f2dc 100644 --- a/packages/core/src/render3/instructions/control_flow.ts +++ b/packages/core/src/render3/instructions/control_flow.ts @@ -21,7 +21,7 @@ import {TNode} from '../interfaces/node'; import {CONTEXT, DECLARATION_COMPONENT_VIEW, HEADER_OFFSET, HYDRATION, LView, TVIEW, TView} from '../interfaces/view'; import {LiveCollection, reconcile} from '../list_reconciliation'; import {destroyLView, detachView} from '../node_manipulation'; -import {getLView, nextBindingIndex} from '../state'; +import {getLView, getSelectedIndex, nextBindingIndex} from '../state'; import {getTNode} from '../util/view_utils'; import {addLViewToLContainer, createAndRenderEmbeddedLView, getLViewFromLContainer, removeLViewFromLContainer, shouldAddViewToDom} from '../view_manipulation'; @@ -56,7 +56,8 @@ export function ɵɵconditional(containerIndex: number, matchingTemplateIndex // Index -1 is a special case where none of the conditions evaluates to // a truthy value and as the consequence we've got no view to show. if (matchingTemplateIndex !== -1) { - const templateTNode = getExistingTNode(hostLView[TVIEW], matchingTemplateIndex); + const templateTNode = + getExistingTNode(hostLView[TVIEW], HEADER_OFFSET + matchingTemplateIndex); const dehydratedView = findMatchingDehydratedView(lContainer, templateTNode.tView!.ssrId); const embeddedLView = @@ -234,23 +235,20 @@ class LiveCollectionLContainerImpl extends * The repeater instruction does update-time diffing of a provided collection (against the * collection seen previously) and maps changes in the collection to views structure (by adding, * removing or moving views as needed). - * @param metadataSlotIdx - index in data where we can find an instance of RepeaterMetadata with - * additional information (ex. differ) needed to process collection diffing and view - * manipulation * @param collection - the collection instance to be checked for changes * @codeGenApi */ -export function ɵɵrepeater( - metadataSlotIdx: number, collection: Iterable|undefined|null): void { +export function ɵɵrepeater(collection: Iterable|undefined|null): void { const prevConsumer = setActiveConsumer(null); + const metadataSlotIdx = getSelectedIndex(); try { const hostLView = getLView(); const hostTView = hostLView[TVIEW]; - const metadata = hostLView[HEADER_OFFSET + metadataSlotIdx] as RepeaterMetadata; + const metadata = hostLView[metadataSlotIdx] as RepeaterMetadata; if (metadata.liveCollection === undefined) { const containerIndex = metadataSlotIdx + 1; - const lContainer = getLContainer(hostLView, HEADER_OFFSET + containerIndex); + const lContainer = getLContainer(hostLView, containerIndex); const itemTemplateTNode = getExistingTNode(hostTView, containerIndex); metadata.liveCollection = new LiveCollectionLContainerImpl(lContainer, hostLView, itemTemplateTNode); @@ -270,7 +268,7 @@ export function ɵɵrepeater( const isCollectionEmpty = liveCollection.length === 0; if (bindingUpdated(hostLView, bindingIndex, isCollectionEmpty)) { const emptyTemplateIndex = metadataSlotIdx + 2; - const lContainerForEmpty = getLContainer(hostLView, HEADER_OFFSET + emptyTemplateIndex); + const lContainerForEmpty = getLContainer(hostLView, emptyTemplateIndex); if (isCollectionEmpty) { const emptyTemplateTNode = getExistingTNode(hostTView, emptyTemplateIndex); const dehydratedView = @@ -312,7 +310,7 @@ function getExistingLViewFromLContainer(lContainer: LContainer, index: number } function getExistingTNode(tView: TView, index: number): TNode { - const tNode = getTNode(tView, index + HEADER_OFFSET); + const tNode = getTNode(tView, index); ngDevMode && assertTNode(tNode); return tNode; diff --git a/packages/core/test/acceptance/control_flow_for_spec.ts b/packages/core/test/acceptance/control_flow_for_spec.ts index 0b007c22cacc1..c993d2f4366ed 100644 --- a/packages/core/test/acceptance/control_flow_for_spec.ts +++ b/packages/core/test/acceptance/control_flow_for_spec.ts @@ -121,6 +121,40 @@ describe('control flow - for', () => { expect(fixture.nativeElement.textContent).toBe('1|2|3|'); }); + it('should be able to access a directive property that is reassigned in a lifecycle hook', () => { + @Directive({ + selector: '[dir]', + exportAs: 'dir', + standalone: true, + }) + class Dir { + data = [1]; + + ngDoCheck() { + this.data = [2]; + } + } + + @Component({ + selector: 'app-root', + standalone: true, + imports: [Dir], + template: ` +
+ + @for (x of dir.data; track $index) { + {{x}} + } + `, + }) + class TestComponent { + } + + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent.trim()).toBe('2'); + }); + describe('trackBy', () => { it('should have access to the host context in the track function', () => { let offsetReads = 0; diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts index 9fef0b46f5375..2dd3c1d9add87 100644 --- a/packages/core/test/test_bed_spec.ts +++ b/packages/core/test/test_bed_spec.ts @@ -175,6 +175,56 @@ describe('TestBed with Standalone types', () => { TestBed.resetTestingModule(); }); + it('should override dependencies of standalone components', () => { + @Component({ + selector: 'dep', + standalone: true, + template: 'main dep', + }) + class MainDep { + } + + @Component({ + selector: 'dep', + standalone: true, + template: 'mock dep', + }) + class MockDep { + } + + @Component({ + selector: 'app-root', + standalone: true, + imports: [MainDep], + template: '', + }) + class AppComponent { + } + + TestBed.configureTestingModule({imports: [AppComponent]}); + + let fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + // No overrides defined, expecting main dependency to be used. + expect(fixture.nativeElement.innerHTML).toBe('main dep'); + + // Emulate an end of a test. + TestBed.resetTestingModule(); + + // Emulate the start of a next test, make sure previous overrides + // are not persisted across tests. + TestBed.configureTestingModule({imports: [AppComponent]}); + TestBed.overrideComponent(AppComponent, {set: {imports: [MockDep]}}); + + fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + // Main dependency was overridden, expect to see a mock. + expect(fixture.nativeElement.innerHTML).toBe('mock dep'); + }); + + it('should override providers on standalone component itself', () => { const A = new InjectionToken('A'); diff --git a/packages/core/testing/src/test_bed_compiler.ts b/packages/core/testing/src/test_bed_compiler.ts index 1039a77822165..4c2a7c87b220e 100644 --- a/packages/core/testing/src/test_bed_compiler.ts +++ b/packages/core/testing/src/test_bed_compiler.ts @@ -396,6 +396,9 @@ export class TestBedCompiler { } this.maybeStoreNgDef(NG_COMP_DEF, declaration); + if (USE_RUNTIME_DEPS_TRACKER_FOR_JIT) { + depsTracker.clearScopeCacheFor(declaration); + } compileComponent(declaration, metadata); }); this.pendingComponents.clear();