From 02d613ef9cf16c6291e6966afe413168c22dc5e4 Mon Sep 17 00:00:00 2001 From: Jessica Janiuk Date: Wed, 7 Aug 2024 19:25:27 +0000 Subject: [PATCH 001/360] release: cut the v18.2.0-rc.0 release --- CHANGELOG.md | 23 +++++++++++++++++++++++ package.json | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b6c50d3de7..5779e2113f02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ + +# 18.2.0-rc.0 (2024-08-07) +## Breaking Changes +### zone.js +- `fakeAsync` will now flush pending timers at the end of + the given function by default. To opt-out of this, you can use `{flush: + false}` in options parameter of `fakeAsync` +### compiler +| Commit | Type | Description | +| -- | -- | -- | +| [d9d68e73d2](https://github.com/angular/angular/commit/d9d68e73d2b59b598d1f7de03ad5faa2b6d31ee2) | fix | reduce chance of conflicts between generated factory and local variables ([#57181](https://github.com/angular/angular/pull/57181)) | +### compiler-cli +| Commit | Type | Description | +| -- | -- | -- | +| [0b1dd39663](https://github.com/angular/angular/commit/0b1dd39663c290fcea9359d6faac91a01d2a9de1) | perf | improve performance of `interpolatedSignalNotInvoked` extended diagnostic ([#57291](https://github.com/angular/angular/pull/57291)) | +### core +| Commit | Type | Description | +| -- | -- | -- | +| [f7918f5272](https://github.com/angular/angular/commit/f7918f52720d3e903281154725cb257a952e8896) | feat | Add 'flush' parameter option to fakeAsync to flush after the test ([#57239](https://github.com/angular/angular/pull/57239)) | +| [7919982063](https://github.com/angular/angular/commit/7919982063e7638ffabe2127d4803bb930c791bc) | feat | Add whenStable helper on ApplicationRef ([#57190](https://github.com/angular/angular/pull/57190)) | + + + # 18.1.4 (2024-08-07) ### compiler diff --git a/package.json b/package.json index 97d67b94ba2e..c5945d0dc70c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-srcs", - "version": "18.2.0-next.4", + "version": "18.2.0-rc.0", "private": true, "description": "Angular - a web framework for modern web apps", "homepage": "https://github.com/angular/angular", From b16dd6d67fb60b76d2b12765b4cf08f3308fcad8 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 8 Aug 2024 13:11:37 +0200 Subject: [PATCH 002/360] fix(compiler-cli): generate valid TS 5.6 type checking code (#57303) Currently in some scenarios the compiler generates code like `null as any ? foo : bar` which will be invalid with [an upcoming TypeScript change](https://devblogs.microsoft.com/typescript/announcing-typescript-5-6-beta/#disallowed-nullish-and-truthy-checks). These changes switch to generating `0 as any` which is exempt from the change. **Note:** I'm not starting the work to fully get us on TS 5.6 until the 18.2 release comes out, but this change is necessary to unblock an internal team. PR Close #57303 --- .../src/ngtsc/typecheck/src/expression.ts | 45 +++++++++++++++---- .../ngtsc/typecheck/src/type_check_block.ts | 10 ++--- .../typecheck/test/span_comments_spec.ts | 8 ++-- .../typecheck/test/type_check_block_spec.ts | 18 ++++---- 4 files changed, 55 insertions(+), 26 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts index 29608617b27a..32d1444de71e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts @@ -40,8 +40,19 @@ import {TypeCheckingConfig} from '../api'; import {addParseSpanInfo, wrapForDiagnostics, wrapForTypeChecker} from './diagnostics'; import {tsCastToAny, tsNumericExpression} from './ts_util'; -export const NULL_AS_ANY = ts.factory.createAsExpression( - ts.factory.createNull(), +/** + * Expression that is cast to any. Currently represented as `0 as any`. + * + * Historically this expression was using `null as any`, but a newly-added check in TypeScript 5.6 + * (https://devblogs.microsoft.com/typescript/announcing-typescript-5-6-beta/#disallowed-nullish-and-truthy-checks) + * started flagging it as always being nullish. Other options that were considered: + * - `NaN as any` or `Infinity as any` - not used, because they don't work if the `noLib` compiler + * option is enabled. Also they require more characters. + * - Some flavor of function call, like `isNan(0) as any` - requires even more characters than the + * NaN option and has the same issue with `noLib`. + */ +export const ANY_EXPRESSION = ts.factory.createAsExpression( + ts.factory.createNumericLiteral('0'), ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), ); const UNDEFINED = ts.factory.createIdentifier('undefined'); @@ -306,7 +317,7 @@ class AstTranslator implements AstVisitor { if (this.config.strictSafeNavigationTypes) { // Basically, the return here is either the type of the complete expression with a null-safe // property read, or `undefined`. So a ternary is used to create an "or" type: - // "a?.b" becomes (null as any ? a!.b : undefined) + // "a?.b" becomes (0 as any ? a!.b : undefined) // The type of this expression is (typeof a!.b) | undefined, which is exactly as desired. const expr = ts.factory.createPropertyAccessExpression( ts.factory.createNonNullExpression(receiver), @@ -314,7 +325,13 @@ class AstTranslator implements AstVisitor { ); addParseSpanInfo(expr, ast.nameSpan); node = ts.factory.createParenthesizedExpression( - ts.factory.createConditionalExpression(NULL_AS_ANY, undefined, expr, undefined, UNDEFINED), + ts.factory.createConditionalExpression( + ANY_EXPRESSION, + undefined, + expr, + undefined, + UNDEFINED, + ), ); } else if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) { // Emulate a View Engine bug where 'any' is inferred for the left-hand side of the safe @@ -345,14 +362,20 @@ class AstTranslator implements AstVisitor { // The form of safe property reads depends on whether strictness is in use. if (this.config.strictSafeNavigationTypes) { - // "a?.[...]" becomes (null as any ? a![...] : undefined) + // "a?.[...]" becomes (0 as any ? a![...] : undefined) const expr = ts.factory.createElementAccessExpression( ts.factory.createNonNullExpression(receiver), key, ); addParseSpanInfo(expr, ast.sourceSpan); node = ts.factory.createParenthesizedExpression( - ts.factory.createConditionalExpression(NULL_AS_ANY, undefined, expr, undefined, UNDEFINED), + ts.factory.createConditionalExpression( + ANY_EXPRESSION, + undefined, + expr, + undefined, + UNDEFINED, + ), ); } else if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) { // "a?.[...]" becomes (a as any)[...] @@ -420,14 +443,20 @@ class AstTranslator implements AstVisitor { args: ts.Expression[], ): ts.Expression { if (this.config.strictSafeNavigationTypes) { - // "a?.method(...)" becomes (null as any ? a!.method(...) : undefined) + // "a?.method(...)" becomes (0 as any ? a!.method(...) : undefined) const call = ts.factory.createCallExpression( ts.factory.createNonNullExpression(expr), undefined, args, ); return ts.factory.createParenthesizedExpression( - ts.factory.createConditionalExpression(NULL_AS_ANY, undefined, call, undefined, UNDEFINED), + ts.factory.createConditionalExpression( + ANY_EXPRESSION, + undefined, + call, + undefined, + UNDEFINED, + ), ); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index d8f5084c21b4..b2da23437f17 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -69,7 +69,7 @@ import { } from './diagnostics'; import {DomSchemaChecker} from './dom'; import {Environment} from './environment'; -import {astToTypescript, NULL_AS_ANY} from './expression'; +import {astToTypescript, ANY_EXPRESSION} from './expression'; import {OutOfBandDiagnosticRecorder} from './oob'; import { tsCallMethod, @@ -763,7 +763,7 @@ class TcbInvalidReferenceOp extends TcbOp { override execute(): ts.Identifier { const id = this.tcb.allocateId(); - this.scope.addStatement(tsCreateVariable(id, NULL_AS_ANY)); + this.scope.addStatement(tsCreateVariable(id, ANY_EXPRESSION)); return id; } } @@ -2785,7 +2785,7 @@ class TcbExpressionTranslator { this.tcb.oobRecorder.missingPipe(this.tcb.id, ast); // Use an 'any' value to at least allow the rest of the expression to be checked. - pipe = NULL_AS_ANY; + pipe = ANY_EXPRESSION; } else if ( pipeMeta.isExplicitlyDeferred && this.tcb.boundTarget.getEagerlyUsedPipes().includes(ast.name) @@ -2795,7 +2795,7 @@ class TcbExpressionTranslator { this.tcb.oobRecorder.deferredPipeUsedEagerly(this.tcb.id, ast); // Use an 'any' value to at least allow the rest of the expression to be checked. - pipe = NULL_AS_ANY; + pipe = ANY_EXPRESSION; } else { // Use a variable declared as the pipe's type. pipe = this.tcb.env.pipeInst( @@ -2916,7 +2916,7 @@ function tcbCallTypeCtor( } else { // A type constructor is required to be called with all input properties, so any unset // inputs are simply assigned a value of type `any` to ignore them. - return ts.factory.createPropertyAssignment(propertyName, NULL_AS_ANY); + return ts.factory.createPropertyAssignment(propertyName, ANY_EXPRESSION); } }); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts index e4db7d93c51d..41edf86591de 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts @@ -85,7 +85,7 @@ describe('type check blocks diagnostics', () => { it('should annotate safe calls', () => { const TEMPLATE = `{{ method?.(a, b) }}`; expect(tcbWithSpans(TEMPLATE)).toContain( - '((null as any ? (((this).method /*3,9*/) /*3,9*/)!(((this).a /*12,13*/) /*12,13*/, ((this).b /*15,16*/) /*15,16*/) : undefined) /*3,17*/)', + '((0 as any ? (((this).method /*3,9*/) /*3,9*/)!(((this).a /*12,13*/) /*12,13*/, ((this).b /*15,16*/) /*15,16*/) : undefined) /*3,17*/)', ); }); @@ -141,21 +141,21 @@ describe('type check blocks diagnostics', () => { it('should annotate safe property access', () => { const TEMPLATE = `{{ a?.b }}`; expect(tcbWithSpans(TEMPLATE)).toContain( - '(null as any ? (((this).a /*3,4*/) /*3,4*/)!.b /*6,7*/ : undefined) /*3,7*/', + '(0 as any ? (((this).a /*3,4*/) /*3,4*/)!.b /*6,7*/ : undefined) /*3,7*/', ); }); it('should annotate safe method calls', () => { const TEMPLATE = `{{ a?.method(b) }}`; expect(tcbWithSpans(TEMPLATE)).toContain( - '((null as any ? (null as any ? (((this).a /*3,4*/) /*3,4*/)!.method /*6,12*/ : undefined) /*3,12*/!(((this).b /*13,14*/) /*13,14*/) : undefined) /*3,15*/)', + '((0 as any ? (0 as any ? (((this).a /*3,4*/) /*3,4*/)!.method /*6,12*/ : undefined) /*3,12*/!(((this).b /*13,14*/) /*13,14*/) : undefined) /*3,15*/)', ); }); it('should annotate safe keyed reads', () => { const TEMPLATE = `{{ a?.[0] }}`; expect(tcbWithSpans(TEMPLATE)).toContain( - '(null as any ? (((this).a /*3,4*/) /*3,4*/)![0 /*7,8*/] /*3,9*/ : undefined) /*3,9*/', + '(0 as any ? (((this).a /*3,4*/) /*3,4*/)![0 /*7,8*/] /*3,9*/ : undefined) /*3,9*/', ); }); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 710561a0f791..82fd517d0c64 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -157,7 +157,7 @@ describe('type check blocks', () => { 'const _ctor1: (init: Pick, "fieldA" | "fieldB">) => i0.Dir = null!;', ); expect(actual).toContain( - 'var _t1 = _ctor1({ "fieldA": (((this).foo)), "fieldB": null as any });', + 'var _t1 = _ctor1({ "fieldA": (((this).foo)), "fieldB": 0 as any });', ); }); @@ -1235,11 +1235,11 @@ describe('type check blocks', () => { it('should use undefined for safe navigation operations when enabled', () => { const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain( - '(null as any ? (null as any ? (((this).a))!.method : undefined)!() : undefined)', + '(0 as any ? (0 as any ? (((this).a))!.method : undefined)!() : undefined)', ); - expect(block).toContain('(null as any ? (((this).a))!.b : undefined)'); - expect(block).toContain('(null as any ? (((this).a))![0] : undefined)'); - expect(block).toContain('(null as any ? (((((this).a)).optionalMethod))!() : undefined)'); + expect(block).toContain('(0 as any ? (((this).a))!.b : undefined)'); + expect(block).toContain('(0 as any ? (((this).a))![0] : undefined)'); + expect(block).toContain('(0 as any ? (((((this).a)).optionalMethod))!() : undefined)'); }); it("should use an 'any' type for safe navigation operations when disabled", () => { const DISABLED_CONFIG: TypeCheckingConfig = { @@ -1258,13 +1258,13 @@ describe('type check blocks', () => { const TEMPLATE = `{{a.method()?.b}} {{a()?.method()}} {{a.method()?.[0]}} {{a.method()?.otherMethod?.()}}`; it('should check the presence of a property/method on the receiver when enabled', () => { const block = tcb(TEMPLATE, DIRECTIVES); - expect(block).toContain('(null as any ? ((((this).a)).method())!.b : undefined)'); + expect(block).toContain('(0 as any ? ((((this).a)).method())!.b : undefined)'); expect(block).toContain( - '(null as any ? (null as any ? ((this).a())!.method : undefined)!() : undefined)', + '(0 as any ? (0 as any ? ((this).a())!.method : undefined)!() : undefined)', ); - expect(block).toContain('(null as any ? ((((this).a)).method())![0] : undefined)'); + expect(block).toContain('(0 as any ? ((((this).a)).method())![0] : undefined)'); expect(block).toContain( - '(null as any ? ((null as any ? ((((this).a)).method())!.otherMethod : undefined))!() : undefined)', + '(0 as any ? ((0 as any ? ((((this).a)).method())!.otherMethod : undefined))!() : undefined)', ); }); it('should not check the presence of a property/method on the receiver when disabled', () => { From 9af760eb51adc29e8c4be241a42ebf7d9e8753f2 Mon Sep 17 00:00:00 2001 From: Thomas Nguyen Date: Tue, 6 Aug 2024 14:45:05 -0700 Subject: [PATCH 003/360] fix(core): Account for addEventListener to be passed a Window or Document. (#57282) This happened to work for other event listeners since both had a addEventListener method. PR Close #57282 --- packages/core/src/event_delegation_utils.ts | 22 ++++++++---- .../event_dispatch/event_dispatch_spec.ts | 34 ++++++++++++++++++- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/packages/core/src/event_delegation_utils.ts b/packages/core/src/event_delegation_utils.ts index 51a788cfc1b6..fcaae34addd0 100644 --- a/packages/core/src/event_delegation_utils.ts +++ b/packages/core/src/event_delegation_utils.ts @@ -93,15 +93,25 @@ export class GlobalEventDelegation implements OnDestroy { return isEarlyEventType(eventType); } - addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { - this.eventContractDetails.instance!.addEvent(eventName); - sharedStashFunction(element, eventName, handler); - getActionCache(element)[eventName] = ''; - return () => this.removeEventListener(element, eventName, handler); + addEventListener(element: HTMLElement, eventType: string, handler: Function): Function { + // Note: contrary to the type, Window and Document can be passed in + // as well. + if (element.nodeType === Node.ELEMENT_NODE) { + this.eventContractDetails.instance!.addEvent(eventType); + getActionCache(element)[eventType] = ''; + sharedStashFunction(element, eventType, handler); + } else { + element.addEventListener(eventType, handler as EventListener); + } + return () => this.removeEventListener(element, eventType, handler); } removeEventListener(element: HTMLElement, eventType: string, callback: Function): void { - getActionCache(element)[eventType] = undefined; + if (element.nodeType === Node.ELEMENT_NODE) { + getActionCache(element)[eventType] = undefined; + } else { + element.removeEventListener(eventType, callback as EventListener); + } } } diff --git a/packages/core/test/event_dispatch/event_dispatch_spec.ts b/packages/core/test/event_dispatch/event_dispatch_spec.ts index 81687241d8fb..df701a711753 100644 --- a/packages/core/test/event_dispatch/event_dispatch_spec.ts +++ b/packages/core/test/event_dispatch/event_dispatch_spec.ts @@ -6,7 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Renderer2, inject, ɵprovideGlobalEventDelegation} from '@angular/core'; +import { + Component, + HostListener, + Renderer2, + inject, + ɵprovideGlobalEventDelegation, +} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; function configureTestingModule(components: unknown[]) { @@ -216,5 +222,31 @@ describe('event dispatch', () => { bottomEl.click(); expect(onClickSpy).toHaveBeenCalledTimes(2); }); + it('should allow host listening on the window', async () => { + const onClickSpy = jasmine.createSpy(); + @Component({ + standalone: true, + selector: 'app', + template: ` +
+
+
+ `, + }) + class SimpleComponent { + renderer = inject(Renderer2); + destroy!: Function; + @HostListener('window:click', ['$event.target']) + listen(el: Element) { + onClickSpy(); + } + } + configureTestingModule([SimpleComponent]); + fixture = TestBed.createComponent(SimpleComponent); + const nativeElement = fixture.debugElement.nativeElement; + const bottomEl = nativeElement.querySelector('#bottom')!; + bottomEl.click(); + expect(onClickSpy).toHaveBeenCalledTimes(1); + }); }); }); From 296216cbe1c70822d4b444321d218d57c89621b2 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Mon, 5 Aug 2024 08:48:30 -0700 Subject: [PATCH 004/360] fix(core): Allow hybrid CD scheduling to support multiple "Angular zones" (#57267) This commit updates the inside/outside NgZone detection of the hybrid CD scheduling to track the actual instance of the NgZone being used rather than the name "Angular" (how `isInsideAngularZone` works). This allows the scheduling to work correctly when there are multiple versions of Angular running on the page. fixes #57261 PR Close #57267 --- .../scheduling/zoneless_scheduling_impl.ts | 11 +++- packages/core/src/zone/ng_zone.ts | 16 +++++- .../bundle.golden_symbols.json | 9 +++ .../animations/bundle.golden_symbols.json | 9 +++ .../cyclic_import/bundle.golden_symbols.json | 9 +++ .../bundling/defer/bundle.golden_symbols.json | 9 +++ .../forms_reactive/bundle.golden_symbols.json | 9 +++ .../bundle.golden_symbols.json | 9 +++ .../hello_world/bundle.golden_symbols.json | 9 +++ .../hydration/bundle.golden_symbols.json | 9 +++ .../router/bundle.golden_symbols.json | 9 +++ .../bundle.golden_symbols.json | 9 +++ .../bundling/todo/bundle.golden_symbols.json | 9 +++ .../test/change_detection_scheduler_spec.ts | 55 ++++++++++++++++++- 14 files changed, 175 insertions(+), 6 deletions(-) diff --git a/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts b/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts index 9133327e4ecc..dc25d6ba730e 100644 --- a/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts +++ b/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts @@ -20,7 +20,7 @@ import { scheduleCallbackWithRafRace, } from '../../util/callback_scheduler'; import {performanceMarkFeature} from '../../util/performance'; -import {NgZone, NoopNgZone} from '../../zone/ng_zone'; +import {NgZone, NgZonePrivate, NoopNgZone, angularZoneInstanceIdProperty} from '../../zone/ng_zone'; import { ChangeDetectionScheduler, @@ -64,6 +64,9 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler { private readonly zoneIsDefined = typeof Zone !== 'undefined' && !!Zone.root.run; private readonly schedulerTickApplyArgs = [{data: {'__scheduler_tick__': true}}]; private readonly subscriptions = new Subscription(); + private readonly angularZoneId = this.zoneIsDefined + ? (this.ngZone as NgZonePrivate)._inner?.get(angularZoneInstanceIdProperty) + : null; private cancelScheduledCallback: null | (() => void) = null; private shouldRefreshViews = false; @@ -176,7 +179,11 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler { } // If we're inside the zone don't bother with scheduler. Zone will stabilize // eventually and run change detection. - if (!this.zonelessEnabled && this.zoneIsDefined && NgZone.isInAngularZone()) { + if ( + !this.zonelessEnabled && + this.zoneIsDefined && + Zone.current.get(angularZoneInstanceIdProperty + this.angularZoneId) + ) { return false; } diff --git a/packages/core/src/zone/ng_zone.ts b/packages/core/src/zone/ng_zone.ts index 557d48824b99..cb74a4eba1a5 100644 --- a/packages/core/src/zone/ng_zone.ts +++ b/packages/core/src/zone/ng_zone.ts @@ -18,6 +18,11 @@ import {AsyncStackTaggingZoneSpec} from './async-stack-tagging'; // ERROR - [JSC_UNDEFINED_VARIABLE] variable Zone is undeclared declare const Zone: any; +const isAngularZoneProperty = 'isAngularZone'; +export const angularZoneInstanceIdProperty = isAngularZoneProperty + '_ID'; + +let ngZoneInstanceId = 0; + /** * An injectable service for executing work inside or outside of the Angular zone. * @@ -173,7 +178,7 @@ export class NgZone { */ static isInAngularZone(): boolean { // Zone needs to be checked, because this method might be called even when NoopNgZone is used. - return typeof Zone !== 'undefined' && Zone.current.get('isAngularZone') === true; + return typeof Zone !== 'undefined' && Zone.current.get(isAngularZoneProperty) === true; } /** @@ -266,7 +271,7 @@ export class NgZone { const EMPTY_PAYLOAD = {}; -interface NgZonePrivate extends NgZone { +export interface NgZonePrivate extends NgZone { _outer: Zone; _inner: Zone; _nesting: number; @@ -394,9 +399,14 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) { const delayChangeDetectionForEventsDelegate = () => { delayChangeDetectionForEvents(zone); }; + const instanceId = ngZoneInstanceId++; zone._inner = zone._inner.fork({ name: 'angular', - properties: {'isAngularZone': true}, + properties: { + [isAngularZoneProperty]: true, + [angularZoneInstanceIdProperty]: instanceId, + [angularZoneInstanceIdProperty + instanceId]: true, + }, onInvokeTask: ( delegate: ZoneDelegate, current: Zone, 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 bdf28033f4aa..caa1cb9631c6 100644 --- a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json @@ -653,6 +653,9 @@ { "name": "allocLFrame" }, + { + "name": "angularZoneInstanceIdProperty" + }, { "name": "animate" }, @@ -1118,6 +1121,9 @@ { "name": "invokeQuery" }, + { + "name": "isAngularZoneProperty" + }, { "name": "isApplicationBootstrapConfig" }, @@ -1259,6 +1265,9 @@ { "name": "ngOnChangesSetInput" }, + { + "name": "ngZoneInstanceId" + }, { "name": "nonNull" }, diff --git a/packages/core/test/bundling/animations/bundle.golden_symbols.json b/packages/core/test/bundling/animations/bundle.golden_symbols.json index a5066fab632d..c7846d06cc91 100644 --- a/packages/core/test/bundling/animations/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations/bundle.golden_symbols.json @@ -710,6 +710,9 @@ { "name": "allocLFrame" }, + { + "name": "angularZoneInstanceIdProperty" + }, { "name": "animate" }, @@ -1187,6 +1190,9 @@ { "name": "invokeQuery" }, + { + "name": "isAngularZoneProperty" + }, { "name": "isApplicationBootstrapConfig" }, @@ -1325,6 +1331,9 @@ { "name": "ngOnChangesSetInput" }, + { + "name": "ngZoneInstanceId" + }, { "name": "noSideEffects" }, 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 60b371852dec..0081a9013214 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -536,6 +536,9 @@ { "name": "allocLFrame" }, + { + "name": "angularZoneInstanceIdProperty" + }, { "name": "appendChild" }, @@ -941,6 +944,9 @@ { "name": "invokeHostBindingsInCreationMode" }, + { + "name": "isAngularZoneProperty" + }, { "name": "isApplicationBootstrapConfig" }, @@ -1067,6 +1073,9 @@ { "name": "ngOnChangesSetInput" }, + { + "name": "ngZoneInstanceId" + }, { "name": "noSideEffects" }, diff --git a/packages/core/test/bundling/defer/bundle.golden_symbols.json b/packages/core/test/bundling/defer/bundle.golden_symbols.json index e566ff25bb3e..1d498fb1c7c5 100644 --- a/packages/core/test/bundling/defer/bundle.golden_symbols.json +++ b/packages/core/test/bundling/defer/bundle.golden_symbols.json @@ -602,6 +602,9 @@ { "name": "allocLFrame" }, + { + "name": "angularZoneInstanceIdProperty" + }, { "name": "appendChild" }, @@ -2153,6 +2156,9 @@ { "name": "invokeTriggerCleanupFns" }, + { + "name": "isAngularZoneProperty" + }, { "name": "isApplicationBootstrapConfig" }, @@ -2291,6 +2297,9 @@ { "name": "ngOnChangesSetInput" }, + { + "name": "ngZoneInstanceId" + }, { "name": "nonNull" }, 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 b477e767ae53..1391e35ffaa7 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -758,6 +758,9 @@ { "name": "allocLFrame" }, + { + "name": "angularZoneInstanceIdProperty" + }, { "name": "appendChild" }, @@ -1364,6 +1367,9 @@ { "name": "isAbstractControlOptions" }, + { + "name": "isAngularZoneProperty" + }, { "name": "isApplicationBootstrapConfig" }, @@ -1589,6 +1595,9 @@ { "name": "ngOnChangesSetInput" }, + { + "name": "ngZoneInstanceId" + }, { "name": "noSideEffects" }, 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 3d048d165c8b..1523e6b2eade 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 @@ -740,6 +740,9 @@ { "name": "allocLFrame" }, + { + "name": "angularZoneInstanceIdProperty" + }, { "name": "appendChild" }, @@ -1322,6 +1325,9 @@ { "name": "invokeHostBindingsInCreationMode" }, + { + "name": "isAngularZoneProperty" + }, { "name": "isApplicationBootstrapConfig" }, @@ -1556,6 +1562,9 @@ { "name": "ngOnChangesSetInput" }, + { + "name": "ngZoneInstanceId" + }, { "name": "noSideEffects" }, 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 c0826b970edf..2a0732999b60 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -398,6 +398,9 @@ { "name": "allocLFrame" }, + { + "name": "angularZoneInstanceIdProperty" + }, { "name": "applyNodes" }, @@ -749,6 +752,9 @@ { "name": "invokeHostBindingsInCreationMode" }, + { + "name": "isAngularZoneProperty" + }, { "name": "isApplicationBootstrapConfig" }, @@ -839,6 +845,9 @@ { "name": "ngOnChangesSetInput" }, + { + "name": "ngZoneInstanceId" + }, { "name": "noop" }, diff --git a/packages/core/test/bundling/hydration/bundle.golden_symbols.json b/packages/core/test/bundling/hydration/bundle.golden_symbols.json index 9b9d4b76f61d..ed0ee787c94a 100644 --- a/packages/core/test/bundling/hydration/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hydration/bundle.golden_symbols.json @@ -587,6 +587,9 @@ { "name": "allocLFrame" }, + { + "name": "angularZoneInstanceIdProperty" + }, { "name": "appendChild" }, @@ -1019,6 +1022,9 @@ { "name": "invokeHostBindingsInCreationMode" }, + { + "name": "isAngularZoneProperty" + }, { "name": "isApplicationBootstrapConfig" }, @@ -1175,6 +1181,9 @@ { "name": "ngOnChangesSetInput" }, + { + "name": "ngZoneInstanceId" + }, { "name": "nonNull" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index d3914100ea19..a4a0f8cffba1 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -878,6 +878,9 @@ { "name": "allowSanitizationBypassAndThrow" }, + { + "name": "angularZoneInstanceIdProperty" + }, { "name": "appendChild" }, @@ -1553,6 +1556,9 @@ { "name": "invokeHostBindingsInCreationMode" }, + { + "name": "isAngularZoneProperty" + }, { "name": "isApplicationBootstrapConfig" }, @@ -1784,6 +1790,9 @@ { "name": "ngOnChangesSetInput" }, + { + "name": "ngZoneInstanceId" + }, { "name": "noMatch" }, 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 ac3a508ab502..67197b70a769 100644 --- a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json +++ b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json @@ -482,6 +482,9 @@ { "name": "allocLFrame" }, + { + "name": "angularZoneInstanceIdProperty" + }, { "name": "appendChild" }, @@ -839,6 +842,9 @@ { "name": "invokeHostBindingsInCreationMode" }, + { + "name": "isAngularZoneProperty" + }, { "name": "isApplicationBootstrapConfig" }, @@ -944,6 +950,9 @@ { "name": "ngOnChangesSetInput" }, + { + "name": "ngZoneInstanceId" + }, { "name": "nonNull" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index ac4a6f94fd83..ea74ff755b67 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -629,6 +629,9 @@ { "name": "allocLFrame" }, + { + "name": "angularZoneInstanceIdProperty" + }, { "name": "appendChild" }, @@ -1124,6 +1127,9 @@ { "name": "invokeHostBindingsInCreationMode" }, + { + "name": "isAngularZoneProperty" + }, { "name": "isApplicationBootstrapConfig" }, @@ -1283,6 +1289,9 @@ { "name": "ngOnChangesSetInput" }, + { + "name": "ngZoneInstanceId" + }, { "name": "noSideEffects" }, diff --git a/packages/core/test/change_detection_scheduler_spec.ts b/packages/core/test/change_detection_scheduler_spec.ts index 196cc7b60bb1..a81af495e66b 100644 --- a/packages/core/test/change_detection_scheduler_spec.ts +++ b/packages/core/test/change_detection_scheduler_spec.ts @@ -16,7 +16,6 @@ import { Component, createComponent, destroyPlatform, - Directive, ElementRef, EnvironmentInjector, ErrorHandler, @@ -43,6 +42,7 @@ import {filter, take, tap} from 'rxjs/operators'; import {RuntimeError, RuntimeErrorCode} from '../src/errors'; import {scheduleCallbackWithRafRace} from '../src/util/callback_scheduler'; +import {ChangeDetectionSchedulerImpl} from '../src/change_detection/scheduling/zoneless_scheduling_impl'; import {global} from '../src/util/global'; function isStable(injector = TestBed.inject(EnvironmentInjector)): boolean { @@ -737,6 +737,59 @@ describe('Angular with scheduler and ZoneJS', () => { expect(fixture.nativeElement.innerText).toContain('new'); }); + it('updating signal in another "Angular" zone schedules update when in hybrid mode', async () => { + @Component({template: '{{thing()}}', standalone: true}) + class App { + thing = signal('initial'); + } + + const fixture = TestBed.createComponent(App); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toContain('initial'); + const differentAngularZone: NgZone = Zone.root.run(() => new NgZone({})); + differentAngularZone.run(() => { + fixture.componentInstance.thing.set('new'); + }); + + expect(fixture.isStable()).toBe(false); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toContain('new'); + }); + + it('updating signal in a child zone of Angular does not schedule extra CD', async () => { + @Component({template: '{{thing()}}', standalone: true}) + class App { + thing = signal('initial'); + } + + const fixture = TestBed.createComponent(App); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toContain('initial'); + const childZone = TestBed.inject(NgZone).run(() => Zone.current.fork({name: 'child'})); + + childZone.run(() => { + fixture.componentInstance.thing.set('new'); + expect(TestBed.inject(ChangeDetectionSchedulerImpl)['cancelScheduledCallback']).toBeNull(); + }); + }); + + it('updating signal in a child Angular zone of Angular does not schedule extra CD', async () => { + @Component({template: '{{thing()}}', standalone: true}) + class App { + thing = signal('initial'); + } + + const fixture = TestBed.createComponent(App); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toContain('initial'); + const childAngularZone = TestBed.inject(NgZone).run(() => new NgZone({})); + + childAngularZone.run(() => { + fixture.componentInstance.thing.set('new'); + expect(TestBed.inject(ChangeDetectionSchedulerImpl)['cancelScheduledCallback']).toBeNull(); + }); + }); + it('should not run change detection twice if notified during AppRef.tick', async () => { TestBed.configureTestingModule({ providers: [ From cab6c23602c022093c348e1a5f3a0fcf8c19e592 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 9 Aug 2024 12:51:54 +0200 Subject: [PATCH 005/360] refactor(migrations): add internal cleanup logic (#57315) Expands the `inject` migration to add some cleanups that are only relevant internally. Externally this isn't exposed to users. PR Close #57315 --- .../ng-generate/inject-migration/analysis.ts | 23 +- .../ng-generate/inject-migration/internal.ts | 204 ++++++++++ .../ng-generate/inject-migration/migration.ts | 84 +++- .../schematics/test/inject_migration_spec.ts | 381 ++++++++++++++++++ 4 files changed, 676 insertions(+), 16 deletions(-) create mode 100644 packages/core/schematics/ng-generate/inject-migration/internal.ts diff --git a/packages/core/schematics/ng-generate/inject-migration/analysis.ts b/packages/core/schematics/ng-generate/inject-migration/analysis.ts index 0bc54011cd44..b380f06ea23b 100644 --- a/packages/core/schematics/ng-generate/inject-migration/analysis.ts +++ b/packages/core/schematics/ng-generate/inject-migration/analysis.ts @@ -9,7 +9,6 @@ import ts from 'typescript'; import {getAngularDecorators} from '../../utils/ng_decorators'; import {getNamedImports} from '../../utils/typescript/imports'; -import {isReferenceToImport} from '../../utils/typescript/symbol'; /** Names of decorators that enable DI on a class declaration. */ const DECORATORS_SUPPORTING_DI = new Set([ @@ -124,10 +123,12 @@ export function analyzeFile(sourceFile: ts.SourceFile, localTypeChecker: ts.Type * Returns the parameters of a function that aren't used within its body. * @param declaration Function in which to search for unused parameters. * @param localTypeChecker Type checker scoped to the file in which the function was declared. + * @param removedStatements Statements that were already removed from the constructor. */ export function getConstructorUnusedParameters( declaration: ts.ConstructorDeclaration, localTypeChecker: ts.TypeChecker, + removedStatements: Set | null, ): Set { const accessedTopLevelParameters = new Set(); const topLevelParameters = new Set(); @@ -147,6 +148,11 @@ export function getConstructorUnusedParameters( } declaration.body.forEachChild(function walk(node) { + // Don't descend into statements that were removed already. + if (removedStatements && ts.isStatement(node) && removedStatements.has(node)) { + return; + } + if (!ts.isIdentifier(node) || !topLevelParameterNames.has(node.text)) { node.forEachChild(walk); return; @@ -154,11 +160,7 @@ export function getConstructorUnusedParameters( // Don't consider `this.` accesses as being references to // parameters since they'll be moved to property declarations. - if ( - ts.isPropertyAccessExpression(node.parent) && - node.parent.expression.kind === ts.SyntaxKind.ThisKeyword && - node.parent.name === node - ) { + if (isAccessedViaThis(node)) { return; } @@ -287,6 +289,15 @@ export function hasGenerics(node: ts.TypeNode): boolean { return false; } +/** Checks whether an identifier is accessed through `this`, e.g. `this.`. */ +export function isAccessedViaThis(node: ts.Identifier): boolean { + return ( + ts.isPropertyAccessExpression(node.parent) && + node.parent.expression.kind === ts.SyntaxKind.ThisKeyword && + node.parent.name === node + ); +} + /** Finds a `super` call inside of a specific node. */ function findSuperCall(root: ts.Node): ts.CallExpression | null { let result: ts.CallExpression | null = null; diff --git a/packages/core/schematics/ng-generate/inject-migration/internal.ts b/packages/core/schematics/ng-generate/inject-migration/internal.ts new file mode 100644 index 000000000000..4a18d9d1493e --- /dev/null +++ b/packages/core/schematics/ng-generate/inject-migration/internal.ts @@ -0,0 +1,204 @@ +/*! + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import ts from 'typescript'; +import {isAccessedViaThis} from './analysis'; + +/** + * Finds class property declarations without initializers whose constructor-based initialization + * can be inlined into the declaration spot after migrating to `inject`. For example: + * + * ``` + * private foo: number; + * + * constructor(private service: MyService) { + * this.foo = this.service.getFoo(); + * } + * ``` + * + * The initializer of `foo` can be inlined, because `service` will be initialized + * before it after the `inject` migration has finished running. + * + * @param node Class declaration that is being migrated. + * @param constructor Constructor declaration of the class being migrated. + * @param localTypeChecker Type checker scoped to the current file. + */ +export function findUninitializedPropertiesToCombine( + node: ts.ClassDeclaration, + constructor: ts.ConstructorDeclaration, + localTypeChecker: ts.TypeChecker, +): Map | null { + let result: Map | null = null; + + const membersToDeclarations = new Map(); + for (const member of node.members) { + if ( + ts.isPropertyDeclaration(member) && + !member.initializer && + !ts.isComputedPropertyName(member.name) + ) { + membersToDeclarations.set(member.name.text, member); + } + } + + if (membersToDeclarations.size === 0) { + return result; + } + + const memberInitializers = getMemberInitializers(constructor); + if (memberInitializers === null) { + return result; + } + + for (const [name, initializer] of memberInitializers.entries()) { + if ( + membersToDeclarations.has(name) && + !hasLocalReferences(initializer, constructor, localTypeChecker) + ) { + result = result || new Map(); + result.set(membersToDeclarations.get(name)!, initializer); + } + } + + return result; +} + +/** + * Finds the expressions from the constructor that initialize class members, for example: + * + * ``` + * private foo: number; + * + * constructor() { + * this.foo = 123; + * } + * ``` + * + * @param constructor Constructor declaration being analyzed. + */ +function getMemberInitializers(constructor: ts.ConstructorDeclaration) { + let memberInitializers: Map | null = null; + + if (!constructor.body) { + return memberInitializers; + } + + // Only look at top-level constructor statements. + for (const node of constructor.body.statements) { + // Only look for statements in the form of `this. = ;` or `this[] = ;`. + if ( + !ts.isExpressionStatement(node) || + !ts.isBinaryExpression(node.expression) || + node.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + (!ts.isPropertyAccessExpression(node.expression.left) && + !ts.isElementAccessExpression(node.expression.left)) || + node.expression.left.expression.kind !== ts.SyntaxKind.ThisKeyword + ) { + continue; + } + + let name: string | undefined; + + if (ts.isPropertyAccessExpression(node.expression.left)) { + name = node.expression.left.name.text; + } else if (ts.isElementAccessExpression(node.expression.left)) { + name = ts.isStringLiteralLike(node.expression.left.argumentExpression) + ? node.expression.left.argumentExpression.text + : undefined; + } + + // If the member is initialized multiple times, take the first one. + if (name && (!memberInitializers || !memberInitializers.has(name))) { + memberInitializers = memberInitializers || new Map(); + memberInitializers.set(name, node.expression.right); + } + } + + return memberInitializers; +} + +/** + * Determines if a node has references to local symbols defined in the constructor. + * @param root Expression to check for local references. + * @param constructor Constructor within which the expression is used. + * @param localTypeChecker Type checker scoped to the current file. + */ +function hasLocalReferences( + root: ts.Expression, + constructor: ts.ConstructorDeclaration, + localTypeChecker: ts.TypeChecker, +): boolean { + const sourceFile = root.getSourceFile(); + let hasLocalRefs = false; + + root.forEachChild(function walk(node) { + // Stop searching if we know that it has local references. + if (hasLocalRefs) { + return; + } + + // Skip identifiers that are accessed via `this` since they're accessing class members + // that aren't local to the constructor. This is here primarily to catch cases like this + // where `foo` is defined inside the constructor, but is a class member: + // ``` + // constructor(private foo: Foo) { + // this.bar = this.foo.getFoo(); + // } + // ``` + if (ts.isIdentifier(node) && !isAccessedViaThis(node)) { + const declarations = localTypeChecker.getSymbolAtLocation(node)?.declarations; + const isReferencingLocalSymbol = declarations?.some( + (decl) => + // The source file check is a bit redundant since the type checker + // is local to the file, but it's inexpensive and it can prevent + // bugs in the future if we decide to use a full type checker. + decl.getSourceFile() === sourceFile && + decl.getStart() >= constructor.getStart() && + decl.getEnd() <= constructor.getEnd() && + !isInsideInlineFunction(decl, constructor), + ); + + if (isReferencingLocalSymbol) { + hasLocalRefs = true; + } + } + + if (!hasLocalRefs) { + node.forEachChild(walk); + } + }); + + return hasLocalRefs; +} + +/** + * Determines if a node is defined inside of an inline function. + * @param startNode Node from which to start checking for inline functions. + * @param boundary Node at which to stop searching. + */ +function isInsideInlineFunction(startNode: ts.Node, boundary: ts.Node): boolean { + let current = startNode; + + while (current) { + if (current === boundary) { + return false; + } + + if ( + ts.isFunctionDeclaration(current) || + ts.isFunctionExpression(current) || + ts.isArrowFunction(current) + ) { + return true; + } + + current = current.parent; + } + + return false; +} diff --git a/packages/core/schematics/ng-generate/inject-migration/migration.ts b/packages/core/schematics/ng-generate/inject-migration/migration.ts index 52e5a22081a6..f0c1f20e6abd 100644 --- a/packages/core/schematics/ng-generate/inject-migration/migration.ts +++ b/packages/core/schematics/ng-generate/inject-migration/migration.ts @@ -20,6 +20,8 @@ import { } from './analysis'; import {getAngularDecorators} from '../../utils/ng_decorators'; import {getImportOfIdentifier} from '../../utils/typescript/imports'; +import {closestNode} from '../../utils/typescript/nodes'; +import {findUninitializedPropertiesToCombine} from './internal'; /** * Placeholder used to represent expressions inside the AST. @@ -30,13 +32,32 @@ const PLACEHOLDER = 'ɵɵngGeneratePlaceholderɵɵ'; /** Options that can be used to configure the migration. */ export interface MigrationOptions { /** Whether to generate code that keeps injectors backwards compatible. */ - backwardsCompatibleConstructors: boolean; + backwardsCompatibleConstructors?: boolean; /** Whether to migrate abstract classes. */ - migrateAbstractClasses: boolean; + migrateAbstractClasses?: boolean; /** Whether to make the return type of `@Optinal()` parameters to be non-nullable. */ - nonNullableOptional: boolean; + nonNullableOptional?: boolean; + + /** + * Internal-only option that determines whether the migration should try to move the + * initializers of class members from the constructor back into the member itself. E.g. + * + * ``` + * // Before + * private foo; + * + * constructor(@Inject(BAR) private bar: Bar) { + * this.foo = this.bar.getValue(); + * } + * + * // After + * private bar = inject(BAR); + * private foo = this.bar.getValue(); + * ``` + */ + _internalCombineMemberInitializers?: boolean; } /** @@ -60,12 +81,45 @@ export function migrateFile(sourceFile: ts.SourceFile, options: MigrationOptions const printer = ts.createPrinter(); const tracker = new ChangeTracker(printer); - analysis.classes.forEach((result) => { + analysis.classes.forEach(({node, constructor, superCall}) => { + let removedStatements: Set | null = null; + + if (options._internalCombineMemberInitializers) { + findUninitializedPropertiesToCombine(node, constructor, localTypeChecker)?.forEach( + (initializer, property) => { + const statement = closestNode(initializer, ts.isStatement); + + if (!statement) { + return; + } + + const newProperty = ts.factory.updatePropertyDeclaration( + property, + property.modifiers, + property.name, + property.questionToken, + property.type, + initializer, + ); + tracker.replaceText( + statement.getSourceFile(), + statement.getFullStart(), + statement.getFullWidth(), + '', + ); + tracker.replaceNode(property, newProperty); + removedStatements = removedStatements || new Set(); + removedStatements.add(statement); + }, + ); + } + migrateClass( - result.node, - result.constructor, - result.superCall, + node, + constructor, + superCall, options, + removedStatements, localTypeChecker, printer, tracker, @@ -88,6 +142,7 @@ export function migrateFile(sourceFile: ts.SourceFile, options: MigrationOptions * @param constructor Reference to the class' constructor node. * @param superCall Reference to the constructor's `super()` call, if any. * @param options Options used to configure the migration. + * @param removedStatements Statements that have been removed from the constructor already. * @param localTypeChecker Type checker set up for the specific file. * @param printer Printer used to output AST nodes as strings. * @param tracker Object keeping track of the changes made to the file. @@ -97,6 +152,7 @@ function migrateClass( constructor: ts.ConstructorDeclaration, superCall: ts.CallExpression | null, options: MigrationOptions, + removedStatements: Set | null, localTypeChecker: ts.TypeChecker, printer: ts.Printer, tracker: ChangeTracker, @@ -110,12 +166,20 @@ function migrateClass( } const sourceFile = node.getSourceFile(); - const unusedParameters = getConstructorUnusedParameters(constructor, localTypeChecker); + const unusedParameters = getConstructorUnusedParameters( + constructor, + localTypeChecker, + removedStatements, + ); const superParameters = superCall ? getSuperParameters(constructor, superCall, localTypeChecker) : null; const memberIndentation = getNodeIndentation(node.members[0]); - const innerReference = superCall || constructor.body?.statements[0] || constructor; + const removedStatementCount = removedStatements?.size || 0; + const innerReference = + superCall || + constructor.body?.statements.find((statement) => !removedStatements?.has(statement)) || + constructor; const innerIndentation = getNodeIndentation(innerReference); const propsToAdd: string[] = []; const prependToConstructor: string[] = []; @@ -154,7 +218,7 @@ function migrateClass( if ( !options.backwardsCompatibleConstructors && - (!constructor.body || constructor.body.statements.length === 0) + (!constructor.body || constructor.body.statements.length - removedStatementCount === 0) ) { // Drop the constructor if it was empty. removedMembers.add(constructor); diff --git a/packages/core/schematics/test/inject_migration_spec.ts b/packages/core/schematics/test/inject_migration_spec.ts index 8c7fbd467561..d3e53df744d6 100644 --- a/packages/core/schematics/test/inject_migration_spec.ts +++ b/packages/core/schematics/test/inject_migration_spec.ts @@ -29,6 +29,7 @@ describe('inject migration', () => { backwardsCompatibleConstructors?: boolean; migrateAbstractClasses?: boolean; nonNullableOptional?: boolean; + _internalCombineMemberInitializers?: boolean; }) { return runner.runSchematic('inject-migration', options, tree); } @@ -1339,4 +1340,384 @@ describe('inject migration', () => { `}`, ]); }); + + describe('internal-only behavior', () => { + function runInternalMigration() { + return runMigration({_internalCombineMemberInitializers: true}); + } + + it('should inline initializers that depend on DI', async () => { + writeFile( + '/dir.ts', + [ + `import { Directive, Inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + `import { BAR_TOKEN, Bar } from './bar';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private value: number;`, + ` private otherValue: string;`, + ``, + ` constructor(private foo: Foo, @Inject(BAR_TOKEN) readonly bar: Bar) {`, + ` this.value = this.foo.getValue();`, + ` this.otherValue = this.bar.getOtherValue();`, + ` }`, + `}`, + ].join('\n'), + ); + + await runInternalMigration(); + + expect(tree.readContent('/dir.ts').split('\n')).toEqual([ + `import { Directive, inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + `import { BAR_TOKEN, Bar } from './bar';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private foo = inject(Foo);`, + ` readonly bar = inject(BAR_TOKEN);`, + ``, + ` private value: number = this.foo.getValue();`, + ` private otherValue: string = this.bar.getOtherValue();`, + `}`, + ]); + }); + + it('should not inline initializers that access injected parameters without `this`', async () => { + writeFile( + '/dir.ts', + [ + `import { Directive, Inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + `import { BAR_TOKEN, Bar } from './bar';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private value: number;`, + ` private otherValue: string;`, + ``, + ` constructor(private foo: Foo, @Inject(BAR_TOKEN) readonly bar: Bar) {`, + ` this.value = this.foo.getValue();`, + ` this.otherValue = bar.getOtherValue();`, + ` }`, + `}`, + ].join('\n'), + ); + + await runInternalMigration(); + + expect(tree.readContent('/dir.ts').split('\n')).toEqual([ + `import { Directive, inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + `import { BAR_TOKEN, Bar } from './bar';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private foo = inject(Foo);`, + ` readonly bar = inject(BAR_TOKEN);`, + ``, + ` private value: number = this.foo.getValue();`, + ` private otherValue: string;`, + ``, + ` constructor() {`, + ` const bar = this.bar;`, + ``, + ` this.otherValue = bar.getOtherValue();`, + ` }`, + `}`, + ]); + }); + + it('should not inline initializers that depend on local symbols from the constructor', async () => { + writeFile( + '/dir.ts', + [ + `import { Directive } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private value: number;`, + ``, + ` constructor(private foo: Foo) {`, + ` const val = 456;`, + ` this.value = this.foo.getValue([123, [val]]);`, + ` }`, + `}`, + ].join('\n'), + ); + + await runInternalMigration(); + + expect(tree.readContent('/dir.ts').split('\n')).toEqual([ + `import { Directive, inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private foo = inject(Foo);`, + ``, + ` private value: number;`, + ``, + ` constructor() {`, + ` const val = 456;`, + ` this.value = this.foo.getValue([123, [val]]);`, + ` }`, + `}`, + ]); + }); + + it('should inline initializers that depend on local symbols defined outside of the constructor', async () => { + writeFile( + '/dir.ts', + [ + `import { Directive } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `const val = 456;`, + ``, + `@Directive()`, + `class MyDir {`, + ` private value: number;`, + ``, + ` constructor(private foo: Foo) {`, + ` this.value = this.getValue(this.foo, extra);`, + ` }`, + ``, + ` private getValue(foo: Foo, extra: number) {`, + ` return foo.getValue([123, [extra]]);`, + ` }`, + `}`, + ].join('\n'), + ); + + await runInternalMigration(); + + expect(tree.readContent('/dir.ts').split('\n')).toEqual([ + `import { Directive, inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `const val = 456;`, + ``, + `@Directive()`, + `class MyDir {`, + ` private foo = inject(Foo);`, + ``, + ` private value: number = this.getValue(this.foo, extra);`, + ``, + ` private getValue(foo: Foo, extra: number) {`, + ` return foo.getValue([123, [extra]]);`, + ` }`, + `}`, + ]); + }); + + it('should inline initializers defined through an element access', async () => { + writeFile( + '/dir.ts', + [ + `import { Directive, Inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + `import { BAR_TOKEN, Bar } from './bar';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private 'my-value': number;`, + ` private 'my-other-value': string;`, + ``, + ` constructor(private foo: Foo, @Inject(BAR_TOKEN) readonly bar: Bar) {`, + ` this['my-value'] = this.foo.getValue();`, + ` this['my-other-value'] = this.bar.getOtherValue();`, + ` }`, + `}`, + ].join('\n'), + ); + + await runInternalMigration(); + + expect(tree.readContent('/dir.ts').split('\n')).toEqual([ + `import { Directive, inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + `import { BAR_TOKEN, Bar } from './bar';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private foo = inject(Foo);`, + ` readonly bar = inject(BAR_TOKEN);`, + ``, + ` private 'my-value': number = this.foo.getValue();`, + ` private 'my-other-value': string = this.bar.getOtherValue();`, + `}`, + ]); + }); + + it('should take the first initializer for properties initialized multiple times', async () => { + writeFile( + '/dir.ts', + [ + `import { Directive } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private value: number;`, + ``, + ` constructor(private foo: Foo) {`, + ` this.value = this.foo.getValue();`, + ``, + ` this.value = this.foo.getOtherValue();`, + ` }`, + `}`, + ].join('\n'), + ); + + await runInternalMigration(); + + expect(tree.readContent('/dir.ts').split('\n')).toEqual([ + `import { Directive, inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private foo = inject(Foo);`, + ``, + ` private value: number = this.foo.getValue();`, + ``, + ` constructor() {`, + ``, + ` this.value = this.foo.getOtherValue();`, + ` }`, + `}`, + ]); + }); + + it('should not inline initializers that are not at the top level', async () => { + writeFile( + '/dir.ts', + [ + `import { Directive, Optional } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private value: number;`, + ``, + ` constructor(@Optional() private foo: Foo | null) {`, + ` if (this.foo) {`, + ` this.value = this.foo.getValue();`, + ` }`, + ` }`, + `}`, + ].join('\n'), + ); + + await runInternalMigration(); + + expect(tree.readContent('/dir.ts').split('\n')).toEqual([ + `import { Directive, inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private foo = inject(Foo, { optional: true });`, + ``, + ` private value: number;`, + ``, + ` constructor() {`, + ` if (this.foo) {`, + ` this.value = this.foo.getValue();`, + ` }`, + ` }`, + `}`, + ]); + }); + + it('should inline initializers that have expressions using local parameters', async () => { + writeFile( + '/dir.ts', + [ + `import { Directive } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private ids: number[];`, + ` private names: string[];`, + ``, + ` constructor(private foo: Foo) {`, + ` this.ids = this.foo.getValue().map(val => val.id);`, + ` this.names = this.foo.getValue().map(function(current) { return current.name; });`, + ` }`, + `}`, + ].join('\n'), + ); + + await runInternalMigration(); + + expect(tree.readContent('/dir.ts').split('\n')).toEqual([ + `import { Directive, inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private foo = inject(Foo);`, + ``, + ` private ids: number[] = this.foo.getValue().map(val => val.id);`, + ` private names: string[] = this.foo.getValue().map(function (current) { return current.name; });`, + `}`, + ]); + }); + + it('should inline initializers that have expressions using local variables', async () => { + writeFile( + '/dir.ts', + [ + `import { Directive } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private ids: number[];`, + ` private names: string[];`, + ``, + ` constructor(private foo: Foo) {`, + ` this.ids = this.foo.getValue().map(val => {`, + ` const id = val.id;`, + ` return id;`, + ` });`, + ` this.names = this.foo.getValue().map(function(current) {`, + ` const name = current.name;`, + ` return name;`, + ` });`, + ` }`, + `}`, + ].join('\n'), + ); + + await runInternalMigration(); + + expect(tree.readContent('/dir.ts').split('\n')).toEqual([ + `import { Directive, inject } from '@angular/core';`, + `import { Foo } from 'foo';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private foo = inject(Foo);`, + ``, + // The indentation of the closing braces here is slightly off, + // but it's not a problem because this code is internal-only. + ` private ids: number[] = this.foo.getValue().map(val => {`, + ` const id = val.id;`, + ` return id;`, + `});`, + ` private names: string[] = this.foo.getValue().map(function (current) {`, + ` const name = current.name;`, + ` return name;`, + `});`, + `}`, + ]); + }); + }); }); From 86216792fd765d399cb15f91d7f7aace6e051597 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Wed, 7 Aug 2024 21:55:36 -0700 Subject: [PATCH 006/360] fix(core): complete post-hydration cleanup in components that use ViewContainerRef (#57300) Previously, if a component injects a `ViewContainerRef`, the post-hydration cleanup process doesn't visit inner views to cleanup dehydrated views in nested LContainers. This commit updates the logic to recognize this situation and enter host LView to complete cleanup. Resolves #56989. PR Close #57300 --- packages/core/src/hydration/cleanup.ts | 13 ++-- .../platform-server/test/hydration_spec.ts | 64 +++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/core/src/hydration/cleanup.ts b/packages/core/src/hydration/cleanup.ts index d0b0664ef4ee..c5b2ea8718ae 100644 --- a/packages/core/src/hydration/cleanup.ts +++ b/packages/core/src/hydration/cleanup.ts @@ -76,6 +76,15 @@ function removeDehydratedView(dehydratedView: DehydratedContainerView, renderer: */ function cleanupLContainer(lContainer: LContainer) { removeDehydratedViews(lContainer); + + // The host could be an LView if this container is on a component node. + // In this case, descend into host LView for further cleanup. See also + // LContainer[HOST] docs for additional information. + const hostLView = lContainer[HOST]; + if (isLView(hostLView)) { + cleanupLView(hostLView); + } + for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) { cleanupLView(lContainer[i] as LView); } @@ -114,10 +123,6 @@ export function cleanupDehydratedViews(appRef: ApplicationRef) { if (isLView(lNode)) { cleanupLView(lNode); } else { - // Cleanup in the root component view - const componentLView = lNode[HOST] as LView; - cleanupLView(componentLView); - // Cleanup in all views within this view container cleanupLContainer(lNode); } diff --git a/packages/platform-server/test/hydration_spec.ts b/packages/platform-server/test/hydration_spec.ts index 7a538868cd4b..669f61646f8e 100644 --- a/packages/platform-server/test/hydration_spec.ts +++ b/packages/platform-server/test/hydration_spec.ts @@ -7771,6 +7771,70 @@ describe('platform-server hydration integration', () => { verifyAllNodesClaimedForHydration(clientRootNode); verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); }); + + it('should cleanup dehydrated views in routed components that use ViewContainerRef', async () => { + @Component({ + standalone: true, + selector: 'cmp-a', + template: ` + @if (isServer) { +

Server view

+ } @else { +

Client view

+ } + `, + }) + class CmpA { + isServer = isPlatformServer(inject(PLATFORM_ID)); + viewContainerRef = inject(ViewContainerRef); + } + + const routes: Routes = [ + { + path: '', + component: CmpA, + }, + ]; + + @Component({ + standalone: true, + selector: 'app', + imports: [RouterOutlet], + template: ` + + `, + }) + class SimpleComponent {} + + const envProviders = [ + {provide: PlatformLocation, useClass: MockPlatformLocation}, + provideRouter(routes), + ] as unknown as Provider[]; + const html = await ssr(SimpleComponent, {envProviders}); + const ssrContents = getAppContents(html); + + expect(ssrContents).toContain(`(appRef); + appRef.tick(); + + await whenStable(appRef); + + const clientRootNode = compRef.location.nativeElement; + + //

tag is used in a view that is different on a server and + // on a client, so it gets re-created (not hydrated) on a client + const p = clientRootNode.querySelector('p'); + verifyAllNodesClaimedForHydration(clientRootNode, [p]); + + expect(clientRootNode.innerHTML).not.toContain('Server view'); + expect(clientRootNode.innerHTML).toContain('Client view'); + }); }); }); }); From 5558e275ee2eb59708c56fb69aaa5aa6ae62160f Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Tue, 6 Aug 2024 19:02:49 -0700 Subject: [PATCH 007/360] fix(core): take skip hydration flag into account while hydrating i18n blocks (#57299) This commit updates serialization and hydration i18n logic to take into account situations when i18n blocks are located within "skip hydration" blocks. Resolves #57105. PR Close #57299 --- packages/core/src/hydration/annotate.ts | 1 - packages/core/src/hydration/i18n.ts | 11 ++- packages/core/src/hydration/skip_hydration.ts | 13 +++ .../core/src/linker/view_container_ref.ts | 2 +- packages/core/src/render3/i18n/i18n_parse.ts | 1 + packages/core/src/render3/interfaces/i18n.ts | 5 + packages/core/test/render3/i18n/i18n_spec.ts | 9 ++ packages/core/test/render3/is_shape_of.ts | 1 + .../platform-server/test/hydration_spec.ts | 95 +++++++++++++++++++ 9 files changed, 135 insertions(+), 3 deletions(-) diff --git a/packages/core/src/hydration/annotate.ts b/packages/core/src/hydration/annotate.ts index 9859556a2077..75258ca04d22 100644 --- a/packages/core/src/hydration/annotate.ts +++ b/packages/core/src/hydration/annotate.ts @@ -7,7 +7,6 @@ */ import {ApplicationRef} from '../application/application_ref'; -import {APP_ID} from '../application/application_tokens'; import {isDetachedByI18n} from '../i18n/utils'; import {ViewEncapsulation} from '../metadata'; import {Renderer2} from '../render'; diff --git a/packages/core/src/hydration/i18n.ts b/packages/core/src/hydration/i18n.ts index efdda0cc16a6..68e66e5216fa 100644 --- a/packages/core/src/hydration/i18n.ts +++ b/packages/core/src/hydration/i18n.ts @@ -21,6 +21,7 @@ import {assertDefined, assertNotEqual} from '../util/assert'; import type {HydrationContext} from './annotate'; import {DehydratedIcuData, DehydratedView, I18N_DATA} from './interfaces'; import {isDisconnectedRNode, locateNextRNode, tryLocateRNodeByPath} from './node_lookup_utils'; +import {isI18nInSkipHydrationBlock} from './skip_hydration'; import {IS_I18N_HYDRATION_ENABLED} from './tokens'; import { getNgContainerSize, @@ -187,6 +188,11 @@ export function trySerializeI18nBlock( return null; } + const parentTNode = tView.data[tI18n.parentTNodeIndex] as TNode; + if (parentTNode && isI18nInSkipHydrationBlock(parentTNode)) { + return null; + } + const serializedI18nBlock: SerializedI18nBlock = { caseQueue: [], disconnectedNodes: new Set(), @@ -401,7 +407,10 @@ function prepareI18nBlockForHydrationImpl( parentTNode: TNode | null, subTemplateIndex: number, ) { - if (!isI18nHydrationSupportEnabled()) { + if ( + !isI18nHydrationSupportEnabled() || + (parentTNode && isI18nInSkipHydrationBlock(parentTNode)) + ) { return; } diff --git a/packages/core/src/hydration/skip_hydration.ts b/packages/core/src/hydration/skip_hydration.ts index 184dfdb6544f..6ca8e17f9632 100644 --- a/packages/core/src/hydration/skip_hydration.ts +++ b/packages/core/src/hydration/skip_hydration.ts @@ -70,3 +70,16 @@ export function isInSkipHydrationBlock(tNode: TNode): boolean { } return false; } + +/** + * Check if an i18n block is in a skip hydration section by looking at a parent TNode + * to determine if this TNode is in a skip hydration section or the TNode has + * the `ngSkipHydration` attribute. + */ +export function isI18nInSkipHydrationBlock(parentTNode: TNode): boolean { + return ( + hasInSkipHydrationBlockFlag(parentTNode) || + hasSkipHydrationAttrOnTNode(parentTNode) || + isInSkipHydrationBlock(parentTNode) + ); +} diff --git a/packages/core/src/linker/view_container_ref.ts b/packages/core/src/linker/view_container_ref.ts index e619e71834d1..2614cb92e518 100644 --- a/packages/core/src/linker/view_container_ref.ts +++ b/packages/core/src/linker/view_container_ref.ts @@ -10,7 +10,7 @@ import {Injector} from '../di/injector'; import {EnvironmentInjector} from '../di/r3_injector'; import {validateMatchingNode} from '../hydration/error_handling'; import {CONTAINERS} from '../hydration/interfaces'; -import {hasInSkipHydrationBlockFlag, isInSkipHydrationBlock} from '../hydration/skip_hydration'; +import {isInSkipHydrationBlock} from '../hydration/skip_hydration'; import { getSegmentHead, isDisconnectedNode, diff --git a/packages/core/src/render3/i18n/i18n_parse.ts b/packages/core/src/render3/i18n/i18n_parse.ts index c1cf4592d86f..585876d0fa5b 100644 --- a/packages/core/src/render3/i18n/i18n_parse.ts +++ b/packages/core/src/render3/i18n/i18n_parse.ts @@ -236,6 +236,7 @@ export function i18nStartFirstCreatePass( create: createOpCodes, update: updateOpCodes, ast: astStack[0], + parentTNodeIndex, }; } diff --git a/packages/core/src/render3/interfaces/i18n.ts b/packages/core/src/render3/interfaces/i18n.ts index 2802e40e87fc..a7350cfca2d6 100644 --- a/packages/core/src/render3/interfaces/i18n.ts +++ b/packages/core/src/render3/interfaces/i18n.ts @@ -345,6 +345,11 @@ export interface TI18n { * while the Update and Create OpCodes are used at runtime. */ ast: Array; + + /** + * Index of a parent TNode, which represents a host node for this i18n block. + */ + parentTNodeIndex: number; } /** diff --git a/packages/core/test/render3/i18n/i18n_spec.ts b/packages/core/test/render3/i18n/i18n_spec.ts index edf05f0efdc8..76aa51f7abe4 100644 --- a/packages/core/test/render3/i18n/i18n_spec.ts +++ b/packages/core/test/render3/i18n/i18n_spec.ts @@ -105,6 +105,7 @@ describe('Runtime i18n', () => { ]), update: [] as unknown as I18nUpdateOpCodes, ast: [{kind: 0, index: HEADER_OFFSET + 1}], + parentTNodeIndex: HEADER_OFFSET, }); }); @@ -154,6 +155,7 @@ describe('Runtime i18n', () => { }, {kind: 0, index: HEADER_OFFSET + 8}, ], + parentTNodeIndex: HEADER_OFFSET, }); }); @@ -189,6 +191,7 @@ describe('Runtime i18n', () => { }] as Text).textContent = \`Hello \${lView[i-1]}!\`; }`, ]), ast: [{kind: 0, index: HEADER_OFFSET + 2}], + parentTNodeIndex: HEADER_OFFSET, }); }); @@ -218,6 +221,7 @@ describe('Runtime i18n', () => { }] as Text).textContent = \`Hello \${lView[i-1]} and \${lView[i-2]}, again \${lView[i-1]}!\`; }`, ]), ast: [{kind: 0, index: HEADER_OFFSET + 2}], + parentTNodeIndex: HEADER_OFFSET, }); }); @@ -264,6 +268,7 @@ describe('Runtime i18n', () => { {kind: 2, index: HEADER_OFFSET + 2, children: [], type: 1}, {kind: 0, index: HEADER_OFFSET + 4}, ], + parentTNodeIndex: HEADER_OFFSET, }); /**** First sub-template ****/ @@ -298,6 +303,7 @@ describe('Runtime i18n', () => { type: 0, }, ], + parentTNodeIndex: HEADER_OFFSET, }); /**** Second sub-template ****/ @@ -325,6 +331,7 @@ describe('Runtime i18n', () => { type: 0, }, ], + parentTNodeIndex: HEADER_OFFSET, }); }); @@ -390,6 +397,7 @@ describe('Runtime i18n', () => { currentCaseLViewIndex: HEADER_OFFSET + 3, }, ], + parentTNodeIndex: HEADER_OFFSET, }); expect(getTIcu(tView, HEADER_OFFSET + 2)).toEqual({ type: 1, @@ -511,6 +519,7 @@ describe('Runtime i18n', () => { currentCaseLViewIndex: HEADER_OFFSET + 3, }, ], + parentTNodeIndex: HEADER_OFFSET, }); expect(getTIcu(tView, HEADER_OFFSET + 2)).toEqual({ type: 1, diff --git a/packages/core/test/render3/is_shape_of.ts b/packages/core/test/render3/is_shape_of.ts index b7ce9d5f9c15..6a2db4472525 100644 --- a/packages/core/test/render3/is_shape_of.ts +++ b/packages/core/test/render3/is_shape_of.ts @@ -77,6 +77,7 @@ const ShapeOfTI18n: ShapeOf = { create: true, update: true, ast: true, + parentTNodeIndex: true, }; /** diff --git a/packages/platform-server/test/hydration_spec.ts b/packages/platform-server/test/hydration_spec.ts index 669f61646f8e..1e16ae7f8cd5 100644 --- a/packages/platform-server/test/hydration_spec.ts +++ b/packages/platform-server/test/hydration_spec.ts @@ -117,6 +117,10 @@ function verifyNodeHasMismatchInfo(doc: Document, selector = 'app'): void { expect(readHydrationInfo(doc.querySelector(selector)!)?.status).toBe(HydrationStatus.Mismatched); } +function verifyNodeHasSkipHydrationMarker(element: HTMLElement): void { + expect(readHydrationInfo(element)?.status).toBe(HydrationStatus.Skipped); +} + /** Checks whether a given element is a Syntax'; -} diff --git a/adev/src/content/examples/property-binding/src/app/item-detail.component.ts b/adev/src/content/examples/property-binding/src/app/item-detail.component.ts deleted file mode 100644 index 3495c32896bd..000000000000 --- a/adev/src/content/examples/property-binding/src/app/item-detail.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {Component, Input} from '@angular/core'; -// import { Item } from '../item'; -// import { ITEMS } from '../mock-items'; - -@Component({ - standalone: true, - selector: 'app-item-detail', - template: `

Your item is: {{ childItem }}

`, - imports: [], -}) -export class ItemDetailComponent { - // #docregion input-type - @Input() childItem = ''; - // #enddocregion input-type - - // items = ITEMS; - - currentItem = 'bananas in boxes'; -} diff --git a/adev/src/content/examples/property-binding/src/app/item-list.component.ts b/adev/src/content/examples/property-binding/src/app/item-list.component.ts deleted file mode 100644 index 8896cb4cd0a2..000000000000 --- a/adev/src/content/examples/property-binding/src/app/item-list.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {Component, Input} from '@angular/core'; -import {NgFor} from '@angular/common'; -import {ITEMS} from './mock-items'; -import {Item} from './item'; - -@Component({ - standalone: true, - selector: 'app-item-list', - template: ` -

Nested component's list of items:

-
    - @for (item of listItems; track item) { -
  • {{item.id}} {{item.name}}
  • - } -
- -

Pass an object from parent to nested component:

-
    - @for (item of items; track item) { -
  • {{item.id}} {{item.name}}
  • - } -
- `, - imports: [NgFor], -}) -export class ItemListComponent { - listItems = ITEMS; - // #docregion item-input - @Input() items: Item[] = []; - // #enddocregion item-input -} diff --git a/adev/src/content/examples/property-binding/src/app/item.ts b/adev/src/content/examples/property-binding/src/app/item.ts deleted file mode 100644 index 3424ea4a8e46..000000000000 --- a/adev/src/content/examples/property-binding/src/app/item.ts +++ /dev/null @@ -1,6 +0,0 @@ -// #docregion item-class -export interface Item { - id: number; - name: string; -} -// #enddocregion item-class diff --git a/adev/src/content/examples/property-binding/src/app/mock-items.ts b/adev/src/content/examples/property-binding/src/app/mock-items.ts deleted file mode 100644 index ad4d7ef3cbdc..000000000000 --- a/adev/src/content/examples/property-binding/src/app/mock-items.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {Item} from './item'; - -export const ITEMS: Item[] = [ - {id: 11, name: 'bottle'}, - {id: 12, name: 'boombox'}, - {id: 13, name: 'chair'}, - {id: 14, name: 'fishbowl'}, - {id: 15, name: 'lamp'}, - {id: 16, name: 'tv'}, - {id: 17, name: 'mug'}, - {id: 18, name: 'paintbrush'}, - {id: 19, name: 'plant'}, - {id: 20, name: 'teapot'}, -]; diff --git a/adev/src/content/examples/property-binding/src/assets/phone.svg b/adev/src/content/examples/property-binding/src/assets/phone.svg deleted file mode 100644 index 5f4fb44ad9a6..000000000000 --- a/adev/src/content/examples/property-binding/src/assets/phone.svg +++ /dev/null @@ -1,521 +0,0 @@ - - - diff --git a/adev/src/content/examples/property-binding/src/index.html b/adev/src/content/examples/property-binding/src/index.html deleted file mode 100644 index 1e94efa11b9a..000000000000 --- a/adev/src/content/examples/property-binding/src/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Codestin Search App - - - - - - - - diff --git a/adev/src/content/examples/property-binding/src/main.ts b/adev/src/content/examples/property-binding/src/main.ts deleted file mode 100644 index 52a1ee4382f8..000000000000 --- a/adev/src/content/examples/property-binding/src/main.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser'; - -import {AppComponent} from './app/app.component'; - -bootstrapApplication(AppComponent, { - providers: [ - provideProtractorTestingSupport(), // essential for e2e testing - ], -}); diff --git a/adev/src/content/examples/property-binding/stackblitz.json b/adev/src/content/examples/property-binding/stackblitz.json deleted file mode 100644 index 89869613e48f..000000000000 --- a/adev/src/content/examples/property-binding/stackblitz.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "description": "Property Binding", - "files": [ - "!**/*.d.ts", - "!**/*.js", - "!**/*.[0,1,2].*" - ], - "file": "src/app/app.component.ts", - "tags": ["property binding"] -} diff --git a/adev/src/content/examples/template-reference-variables/BUILD.bazel b/adev/src/content/examples/template-reference-variables/BUILD.bazel deleted file mode 100644 index 4c9f61ce001c..000000000000 --- a/adev/src/content/examples/template-reference-variables/BUILD.bazel +++ /dev/null @@ -1,3 +0,0 @@ -package(default_visibility = ["//visibility:public"]) - -exports_files(["src/app/app.component.html"]) diff --git a/adev/src/content/examples/template-reference-variables/e2e/src/app.e2e-spec.ts b/adev/src/content/examples/template-reference-variables/e2e/src/app.e2e-spec.ts deleted file mode 100644 index 0e2abfd432cd..000000000000 --- a/adev/src/content/examples/template-reference-variables/e2e/src/app.e2e-spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {browser, element, by, logging} from 'protractor'; - -describe('Template-reference-variables-example', () => { - beforeEach(() => browser.get('')); - - // helper function used to test what's logged to the console - async function logChecker(contents: string) { - const logs = await browser.manage().logs().get(logging.Type.BROWSER); - const messages = logs.filter(({message}) => message.indexOf(contents) !== -1); - expect(messages.length).toBeGreaterThan(0); - } - - it('should display Template reference variables', async () => { - expect(await element(by.css('h1')).getText()).toEqual('Template reference variables'); - }); - - it('should log a Calling 123 ... message', async () => { - const callButton = element.all(by.css('button')).get(0); - const phoneInput = element.all(by.css('input')).get(0); - await phoneInput.sendKeys('123'); - await callButton.click(); - const contents = 'Calling 123 ...'; - await logChecker(contents); - }); - - it('should submit form', async () => { - const submitButton = element.all(by.css('button')).get(2); - const nameInput = element.all(by.css('input')).get(1); - await nameInput.sendKeys('123'); - await submitButton.click(); - expect(await element.all(by.css('div > p')).get(2).getText()).toEqual( - 'Submitted. Form value is {"name":"123"}', - ); - }); -}); diff --git a/adev/src/content/examples/template-reference-variables/example-config.json b/adev/src/content/examples/template-reference-variables/example-config.json deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/adev/src/content/examples/template-reference-variables/src/app/app.component.css b/adev/src/content/examples/template-reference-variables/src/app/app.component.css deleted file mode 100644 index 9a9e6dc50f01..000000000000 --- a/adev/src/content/examples/template-reference-variables/src/app/app.component.css +++ /dev/null @@ -1,14 +0,0 @@ -h3 { - font-weight: 700; -} - -pre, .wrapper { - background-color: rgb(240, 250, 250); - padding: 1rem; - border: 1px solid #444; -} - -input { - margin: .5rem; - padding: .5rem; -} diff --git a/adev/src/content/examples/template-reference-variables/src/app/app.component.html b/adev/src/content/examples/template-reference-variables/src/app/app.component.html deleted file mode 100644 index c89e4e3dbacb..000000000000 --- a/adev/src/content/examples/template-reference-variables/src/app/app.component.html +++ /dev/null @@ -1,105 +0,0 @@ -

Template reference variables

- -
-

Pass value to an event handler

-

See console for output.

- - - - - - - - - - -
- -
- -
-

Template reference variable with disabled button

-

btn refers to the button element.

- -
- -
- -

Reference variables, forms, and NgForm

- -
- - - -
- -
-

{{ submitMessage }}

-
- - - - -

JSON: {{ itemForm.form.value | json }}

- -
- -

Template Reference Variable Scope

- -

This section demonstrates in which situations you can access local template reference variables (#ref).

- -

Accessing in a child template

- - -
- - - @if (true) { - Value: {{ ref1.value }} - } - -
- - - -

Here's the desugared syntax:

-
- -

Accessing from outside parent template. (Doesn't work.)

- - -
- @if (true) { - - } - - -
- -

Here's the desugared syntax:

-
- -

*ngFor and template reference variable scope

- - - -
- -

Accessing a on an ng-template

- -See the console output to see that when you declare the variable on an ng-template, the variable refers to a TemplateRef instance, which represents the template. - - - diff --git a/adev/src/content/examples/template-reference-variables/src/app/app.component.ts b/adev/src/content/examples/template-reference-variables/src/app/app.component.ts deleted file mode 100644 index ab458acffe27..000000000000 --- a/adev/src/content/examples/template-reference-variables/src/app/app.component.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {Component, ViewChild} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {NgForm, FormsModule} from '@angular/forms'; - -@Component({ - standalone: true, - selector: 'app-root', - templateUrl: './app.component.html', - imports: [CommonModule, FormsModule], - styleUrls: ['./app.component.css'], -}) -export class AppComponent { - public firstExample = 'Hello, World!'; - public secondExample = 'Hello, World!'; - - public ref2 = ''; - - public desugared1 = ` - - - - Value: {{ ref1.value }} - `; - - public desugared2 = ` - - - - - - Value: {{ ref2?.value }}`; - - public ngForExample = ` - - - - - - {{ ref.value }}`; - - @ViewChild('itemForm', {static: false}) form!: NgForm; - - get submitMessage() { - return this._submitMessage; - } - private _submitMessage = ''; - - onSubmit(form: NgForm) { - this._submitMessage = 'Submitted. Form value is ' + JSON.stringify(form.value); - } - - callPhone(value: string) { - console.warn(`Calling ${value} ...`); - } - - callFax(value: string) { - console.warn(`Faxing ${value} ...`); - } - - log(ref3: any) { - console.warn(ref3.constructor); - } -} diff --git a/adev/src/content/examples/template-reference-variables/src/index.html b/adev/src/content/examples/template-reference-variables/src/index.html deleted file mode 100644 index 679d64c77fe8..000000000000 --- a/adev/src/content/examples/template-reference-variables/src/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Codestin Search App - - - - - - Loading... - - diff --git a/adev/src/content/examples/template-reference-variables/src/main.ts b/adev/src/content/examples/template-reference-variables/src/main.ts deleted file mode 100644 index bba4ddf9ad8e..000000000000 --- a/adev/src/content/examples/template-reference-variables/src/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser'; - -import {AppComponent} from './app/app.component'; - -bootstrapApplication(AppComponent, {providers: [provideProtractorTestingSupport()]}).catch((err) => - console.error(err), -); diff --git a/adev/src/content/examples/template-reference-variables/stackblitz.json b/adev/src/content/examples/template-reference-variables/stackblitz.json deleted file mode 100644 index b683ad18b920..000000000000 --- a/adev/src/content/examples/template-reference-variables/stackblitz.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "description": "Template Reference Variables", - "files": ["!**/*.d.ts", "!**/*.js", "!**/*.[1,2].*"], - "file": "src/app/app.component.ts", - "tags": ["Template Reference Variables"] -} diff --git a/adev/src/content/examples/template-syntax/BUILD.bazel b/adev/src/content/examples/template-syntax/BUILD.bazel deleted file mode 100644 index 54064a686914..000000000000 --- a/adev/src/content/examples/template-syntax/BUILD.bazel +++ /dev/null @@ -1,7 +0,0 @@ -package(default_visibility = ["//visibility:public"]) - -exports_files([ - "src/app/app.component.html", - "src/app/svg.component.svg", - "src/app/svg.component.ts", -]) diff --git a/adev/src/content/examples/template-syntax/e2e/src/app.e2e-spec.ts b/adev/src/content/examples/template-syntax/e2e/src/app.e2e-spec.ts deleted file mode 100644 index a00efc6b0539..000000000000 --- a/adev/src/content/examples/template-syntax/e2e/src/app.e2e-spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {browser, element, by} from 'protractor'; - -// TODO Not yet complete -describe('Template Syntax', () => { - beforeAll(() => browser.get('')); - - it('should be able to use interpolation with a hero', async () => { - const heroInterEle = element.all(by.css('h2+p')).get(0); - expect(await heroInterEle.getText()).toEqual('My current hero is Hercules'); - }); - - it('should be able to use interpolation with a calculation', async () => { - const theSumEles = element.all(by.cssContainingText('h3~p', 'The sum of')); - expect(await theSumEles.count()).toBe(2); - expect(await theSumEles.get(0).getText()).toEqual('The sum of 1 + 1 is 2'); - expect(await theSumEles.get(1).getText()).toEqual('The sum of 1 + 1 is not 4'); - }); - - it('should be able to use class binding syntax', async () => { - const specialEle = element(by.cssContainingText('div', 'Special')); - expect(await specialEle.getAttribute('class')).toMatch('special'); - }); - - it('should be able to use style binding syntax', async () => { - const specialButtonEle = element(by.cssContainingText('div.special~button', 'button')); - expect(await specialButtonEle.getAttribute('style')).toMatch('color: red'); - }); - - it('should two-way bind to sizer', async () => { - const div = element(by.css('div#two-way-1')); - const incButton = div.element(by.buttonText('+')); - const input = div.element(by.css('input')); - const initSize = await input.getAttribute('value'); - await incButton.click(); - expect(await input.getAttribute('value')).toEqual((+initSize + 1).toString()); - }); - - it("should change SVG rectangle's fill color on click", async () => { - const div = element(by.css('app-svg')); - const colorSquare = div.element(by.css('rect')); - const initialColor = await colorSquare.getAttribute('fill'); - await colorSquare.click(); - expect(await colorSquare.getAttribute('fill')).not.toEqual(initialColor); - }); -}); diff --git a/adev/src/content/examples/template-syntax/example-config.json b/adev/src/content/examples/template-syntax/example-config.json deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/adev/src/content/examples/template-syntax/src/app/app.component.css b/adev/src/content/examples/template-syntax/src/app/app.component.css deleted file mode 100644 index 01163bffbcab..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/app.component.css +++ /dev/null @@ -1,17 +0,0 @@ -a.to-toc { margin: 30px 0; } -button { font-size: 100%; margin: 0 2px; } -div[clickable] {cursor: pointer; max-width: 200px; margin: 16px 0} -#noTrackByCnt, #withTrackByCnt {color: darkred; max-width: 450px; margin: 4px;} -img {height: 100px;} -.box {border: 1px solid black; padding: 6px; max-width: 450px;} -.child-div {margin-left: 1em; font-weight: normal} -.context {margin-left: 1em;} -.hidden {display: none} -.parent-div {margin-top: 1em; font-weight: bold} -.special {font-weight:bold; font-size: x-large} -.bad {color: red;} -.saveable {color: limegreen;} -.curly, .modified {font-family: "Brush Script MT", cursive} -.toe {margin-left: 1em; font-style: italic;} -little-hero {color:blue; font-size: smaller; background-color: Turquoise } -.to-toc {margin-top: 10px; display: block} diff --git a/adev/src/content/examples/template-syntax/src/app/app.component.html b/adev/src/content/examples/template-syntax/src/app/app.component.html deleted file mode 100644 index 66899e233c77..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/app.component.html +++ /dev/null @@ -1,753 +0,0 @@ - -

Template Syntax

-Interpolation
-Expression context
-Statement context
-Mental Model
-Buttons
-Properties vs. Attributes
-
-Property Binding
- -
-Event Binding
-Two-way Binding
-
-
Directives
- -
-Template reference variables
-Inputs and outputs
-Pipes
-Safe navigation operator ?.
-Non-null assertion operator !.
-Enums
-SVG Templates
- - -

Interpolation

- -

My current hero is {{ currentHero.name }}

- -

- {{ title }} - {{ hero.name }} -

- - -

The sum of 1 + 1 is {{ 1 + 1 }}

- - -

The sum of 1 + 1 is not {{ 1 + 1 + getVal() }}

- -top - -

Expression context

- -

Component expression context ({{title}}, [hidden]="isUnchanged")

-
- {{ title }} - changed -
- - -

Template input variable expression context (let hero)

- - - @for (hero of heroes; track hero) { -
{{ hero.name }}
- } -
- -

Template reference variable expression context (#heroInput)

-
- Type something: - {{ heroInput.value }} -
- -top - -

Statement context

- -

Component statement context ( (click)="onSave() ) -

- - - -
- -

Template $event statement context

-
- - - -
- -

Template input variable statement context (let hero)

- -
- - @for (hero of heroes; track hero) { - - } - -
- -

Template reference variable statement context (#heroForm)

-
- -
...
- -
- -top - - -

New Mental Model

- - - -
Mental Model
- - -

- -
- -
Mental Model
- - -
-

- -
- - -
-

- -
- - - -
- -
-

- - - - -
click me
- -{{ clicked }} -

- -
- Hero Name: - - - - {{ name }} -
-

- - - - -

- - -
Special
- -

- - - - -top - - - -

Property vs. Attribute (img examples)

- - - -

- -icon -villain-image - -top - - -

Buttons

- - - - -

- - -

- - -top - - -

Property Binding

- - - -
[ngClass] binding to the classes property
- - - - - - - -

- interpolated image of {{ currentHero.name }} - is the interpolated image. -

-

- - is the property bound image. -

- -

"{{ title }}" is the interpolated title.

-

"" is the property bound title.

- - -

"{{evilTitle}}" is the interpolated evil title.

-

"" is the property bound evil title.

- -top - - -

Attribute Binding

- - - - - - - - - -
One-Two
FiveSix
- -
- - -

- - -
- - - - - - - -
- -top - - -

Class Binding

- - -
Bad curly special
- - -
Bad curly
- - -
The class binding is special
- - -
This one is not so special
- -top - - -

Style Binding

- - - - - - - -top - - -

Event Binding

- - - -
- -
click with myClick
-{{clickMessage}} -
- - - - -
- - - - - - -
Click me -
Click me too!
-
- - -
- -
- - -
- -
- - - -top - -

Two-way Binding

-
- -
Resizable Text
- -
-
-
-

De-sugared two-way binding

- -
- -top - - -

NgModel (two-way) Binding

- -

Result: {{ currentHero.name }}

- - -without NgModel -
- -[(ngModel)] -
- -(ngModelChange)="...name=$event" -
- -(ngModelChange)="setUppercaseName($event)" - -top - - -

NgClass Binding

- -

currentClasses is {{ currentClasses | json }}

-
This div is initially saveable, unchanged, and special
- - -
- | - | - - -

-
- This div should be {{ canSave ? "": "not"}} saveable, - {{ isUnchanged ? "unchanged" : "modified" }} and, - {{ isSpecial ? "": "not"}} special after clicking "Refresh".
-

- -
This div is special
- -
Bad curly special
-
Curly special
- -top - - -

NgStyle Binding

- -
- This div is x-large or smaller. -
- -

[ngStyle] binding to currentStyles - CSS property names

-

currentStyles is {{currentStyles | json}}

-
- This div is initially italic, normal weight, and extra large (24px). -
- - -
- | - | - - -

-
- This div should be {{ canSave ? "italic": "plain"}}, - {{ isUnchanged ? "normal weight" : "bold" }} and, - {{ isSpecial ? "extra large": "normal size"}} after clicking "Refresh".
- -top - - -

NgIf Binding

- -@if (isActive) { - -} - -@if (currentHero) { -
Hello, {{ currentHero.name }}
-} -@if (nullHero) { -
Hello, {{ nullHero.name }}
-} - - - -Add {{currentHero.name}} with template - - -
Hero Detail removed from DOM (via template) because isActive is false
- - - - - -
Show with class
-
Hide with class
- - - - -
Show with style
-
Hide with style
- -top - - -

NgFor Binding

- -
- @for (hero of heroes; track hero) { -
{{ hero.name }}
- } -
-
- -
- - @for (hero of heroes; track hero) { - - } -
- -top - -

*ngFor with index

-

with semi-colon separator

-
- @for (hero of heroes; track hero; let i = $index) { -
{{ i + 1 }} - {{ hero.name }}
- } -
- -

with comma separator

-
- - @for (hero of heroes; track hero; let i = $index) { -
{{ i + 1 }} - {{ hero.name }}
- } -
- -top - -

*ngFor trackBy

- - - - -

without trackBy

-
- @for (hero of heroes; track hero) { -
({{ hero.id }}) {{ hero.name }}
- } - - @if (heroesNoTrackByCount) { -
- Hero DOM elements change #{{ heroesNoTrackByCount }} without trackBy -
- } -
- -

with trackBy

-
- @for (hero of heroes; track trackByHeroes($index, hero)) { -
({{ hero.id }}) {{ hero.name }}
- } - - @if (heroesWithTrackByCount) { -
- Hero DOM elements change #{{heroesWithTrackByCount}} with trackBy -
- } -
- -


- -

with trackBy and semi-colon separator

-
- @for (hero of heroes; track trackByHeroes($index, hero)) { -
({{ hero.id }}) {{ hero.name }}
- } -
- -

with trackBy and comma separator

-
- @for (hero of heroes; track hero) { -
({{ hero.id }}) {{ hero.name }}
- } -
- -

with trackBy and space separator

-
- @for (hero of heroes; track hero) { -
({{ hero.id }}) {{ hero.name }}
- } -
- -

with generic trackById function

-
- @for (hero of heroes; track hero) { -
({{ hero.id }}) {{ hero.name }}
- } -
- -top - - -

NgSwitch Binding

- -

Pick your favorite hero

-
- @for (h of heroes; track h) { - - } -
- -
- @switch (currentHero.emotion) { - @case ('happy') { - - } @case ('sad') { - - } @case ('confused') { - - } @case ('confused') { -
Are you as confused as {{ currentHero.name }}?
- } @default { - - } - } -
- -top - - -

Template reference variables

- - - - - - - - - - - -

Example Form

- - -top - - -

Inputs and Outputs

- -icon - - - - - -
myClick2
-{{ clickMessage2 }} - -top - - -

Pipes

- -
Title through uppercase pipe: {{ title | uppercase }}
- - -
- Title through a pipe chain: - {{ title | uppercase | lowercase }} -
- - -
Birthdate: {{ currentHero.birthdate | date:'longDate' }}
- -
{{ currentHero | json }}
- -
Birthdate: {{ (currentHero.birthdate | date:'longDate') | uppercase }}
- -
- - Price: {{ product.price | currency:'USD':'symbol' }} -
- -top - - -

Safe navigation operator ?.

- -
- The title is {{ title }} -
- -
- The current hero's name is {{ currentHero.name }} -
- -
- The current hero's name is {{ currentHero.name }} -
- - - - - -@if (nullHero) { -
The null hero's name is {{ nullHero.name }}
-} - -
-The null hero's name is {{ nullHero && nullHero.name }} -
- -
- - The null hero's name is {{ nullHero?.name }} -
- - -top - - -

Non-null assertion operator !.

- -
- - @if (hero) { -
The hero's name is {{ hero!.name }}
- } -
- -top - - -

$any type cast function $any( ).

- -
- -
- The hero's marker is {{ $any(hero).marker }} -
-
- -
- -
- Undeclared members is {{ $any(this).member }} -
-
- -top - - - -

Enums in binding

- -

- The name of the Color.Red enum is {{ Color[Color.Red] }}.
- The current color is {{ Color[color] }} and its number is {{ color }}.
- -

- -top - -

SVG Templates

- -top diff --git a/adev/src/content/examples/template-syntax/src/app/app.component.ts b/adev/src/content/examples/template-syntax/src/app/app.component.ts deleted file mode 100644 index 0bb26ce7010c..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/app.component.ts +++ /dev/null @@ -1,224 +0,0 @@ -import {AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChildren} from '@angular/core'; -import {CommonModule, DatePipe} from '@angular/common'; -import {FormsModule} from '@angular/forms'; - -import {BigHeroDetailComponent, HeroDetailComponent} from './hero-detail.component'; -import {ClickDirective, ClickDirective2} from './click.directive'; -import {HeroFormComponent} from './hero-form.component'; -import {heroSwitchComponents} from './hero-switch.components'; -import {SizerComponent} from './sizer.component'; -import {SvgComponent} from './svg.component'; - -import {Hero} from './hero'; - -export enum Color { - Red, - Green, - Blue, -} - -/** - * Giant grab bag of stuff to drive the chapter - */ -@Component({ - standalone: true, - selector: 'app-root', - templateUrl: './app.component.html', - imports: [ - // Angular Sources - CommonModule, - FormsModule, - DatePipe, - - // Sample Components and Directives - BigHeroDetailComponent, - ClickDirective, - ClickDirective2, - HeroDetailComponent, - HeroFormComponent, - heroSwitchComponents, // an array of components - SizerComponent, - SvgComponent, - ], - styleUrls: ['./app.component.css'], -}) -export class AppComponent implements AfterViewInit, OnInit { - ngOnInit() { - this.resetHeroes(); - this.setCurrentClasses(); - this.setCurrentStyles(); - } - - ngAfterViewInit() { - // Detect effects of NgForTrackBy - trackChanges(this.heroesNoTrackBy, () => this.heroesNoTrackByCount++); - trackChanges(this.heroesWithTrackBy, () => this.heroesWithTrackByCount++); - } - - @ViewChildren('noTrackBy') heroesNoTrackBy!: QueryList; - @ViewChildren('withTrackBy') heroesWithTrackBy!: QueryList; - - actionName = 'Go for it'; - badCurly = 'bad curly'; - classes = 'special'; - help = ''; - - alert(msg?: string) { - window.alert(msg); - } - callFax(value: string) { - this.alert(`Faxing ${value} ...`); - } - callPhone(value: string) { - this.alert(`Calling ${value} ...`); - } - canSave = true; - - changeIds() { - this.resetHeroes(); - this.heroes.forEach((h) => (h.id += 10 * this.heroIdIncrement++)); - this.heroesWithTrackByCountReset = -1; - } - - clearTrackByCounts() { - const trackByCountReset = this.heroesWithTrackByCountReset; - this.resetHeroes(); - this.heroesNoTrackByCount = -1; - this.heroesWithTrackByCount = trackByCountReset; - this.heroIdIncrement = 1; - } - - clicked = ''; - clickMessage = ''; - clickMessage2 = ''; - - Color = Color; - color = Color.Red; - colorToggle() { - this.color = this.color === Color.Red ? Color.Blue : Color.Red; - } - - currentHero!: Hero; - - updateCurrentHeroName(event: Event) { - this.currentHero.name = (event.target as any).value; - } - - deleteHero(hero?: Hero) { - this.alert(`Delete ${hero ? hero.name : 'the hero'}.`); - } - - evilTitle = 'Template Syntax'; - - fontSizePx = 16; - - title = 'Template Syntax'; - - getVal(): number { - return 2; - } - - name: string = Hero.heroes[0].name || ''; - hero!: Hero; // defined to demonstrate template context precedence - heroes: Hero[] = []; - - // trackBy change counting - heroesNoTrackByCount = 0; - heroesWithTrackByCount = 0; - heroesWithTrackByCountReset = 0; - - heroIdIncrement = 1; - - // heroImageUrl = 'https://wpclipart.com/dl.php?img=/cartoon/people/hero/hero_silhoutte.svg'; - // Public Domain terms of use: https://wpclipart.com/terms.html - heroImageUrl = 'assets/images/hero.svg'; - // villainImageUrl = 'http://www.clker.com/cliparts/u/s/y/L/x/9/villain-man.svg' - // Public Domain terms of use https://www.clker.com/disclaimer.html - villainImageUrl = 'assets/images/villain.svg'; - - iconUrl = 'assets/images/ng-logo.svg'; - isActive = false; - isSpecial = true; - isUnchanged = true; - - get nullHero(): Hero | null { - return null; - } - - onClickMe(event?: MouseEvent) { - const evtMsg = event ? ' Event target class is ' + (event.target as HTMLElement).className : ''; - this.alert('Click me.' + evtMsg); - } - - onSave(event?: MouseEvent) { - const evtMsg = event ? ' Event target is ' + (event.target as HTMLElement).textContent : ''; - this.alert('Saved.' + evtMsg); - if (event) { - event.stopPropagation(); - } - } - - onSubmit(data: any) { - /* referenced but not used */ - } - - product = { - name: 'frimfram', - price: 42, - }; - - // updates with fresh set of cloned heroes - resetHeroes() { - this.heroes = Hero.heroes.map((hero) => hero.clone()); - this.currentHero = this.heroes[0]; - this.hero = this.currentHero; - this.heroesWithTrackByCountReset = 0; - } - - setUppercaseName(name: string) { - this.currentHero.name = name.toUpperCase(); - } - - currentClasses: Record = {}; - setCurrentClasses() { - // CSS classes: added/removed per current state of component properties - this.currentClasses = { - saveable: this.canSave, - modified: !this.isUnchanged, - special: this.isSpecial, - }; - } - - currentStyles: Record = {}; - setCurrentStyles() { - // CSS styles: set per current state of component properties - this.currentStyles = { - 'font-style': this.canSave ? 'italic' : 'normal', - 'font-weight': !this.isUnchanged ? 'bold' : 'normal', - 'font-size': this.isSpecial ? '24px' : '12px', - }; - } - - trackByHeroes(index: number, hero: Hero): number { - return hero.id; - } - - trackById(index: number, item: any): number { - return item.id; - } -} - -// helper to track changes to viewChildren -function trackChanges(views: QueryList, changed: () => void) { - let oldRefs = views.toArray(); - views.changes.subscribe((changes: QueryList) => { - const changedRefs = changes.toArray(); - // Check if every changed Element is the same as old and in the same position - const isSame = oldRefs.every((v, i) => v.nativeElement === changedRefs[i].nativeElement); - if (!isSame) { - oldRefs = changedRefs; - // wait a tick because called after views are constructed - setTimeout(changed, 0); - } - }); -} diff --git a/adev/src/content/examples/template-syntax/src/app/click.directive.ts b/adev/src/content/examples/template-syntax/src/app/click.directive.ts deleted file mode 100644 index ba20fbf2658f..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/click.directive.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable @angular-eslint/directive-class-suffix, - @angular-eslint/directive-selector, - @angular-eslint/no-output-rename, - @angular-eslint/no-outputs-metadata-property */ -import {Directive, ElementRef, EventEmitter, Output} from '@angular/core'; - -@Directive({ - standalone: true, - selector: '[myClick]', -}) -export class ClickDirective { - @Output('myClick') clicks = new EventEmitter(); // @Output(alias) propertyName = ... - - toggle = false; - - constructor(el: ElementRef) { - el.nativeElement.addEventListener('click', (event: Event) => { - this.toggle = !this.toggle; - this.clicks.emit(this.toggle ? 'Click!' : ''); - }); - } -} - -@Directive({ - standalone: true, - selector: '[myClick2]', - outputs: ['clicks:myClick'], // propertyName:alias -}) -export class ClickDirective2 { - clicks = new EventEmitter(); - toggle = false; - - constructor(el: ElementRef) { - el.nativeElement.addEventListener('click', (event: Event) => { - this.toggle = !this.toggle; - this.clicks.emit(this.toggle ? 'Click2!' : ''); - }); - } -} diff --git a/adev/src/content/examples/template-syntax/src/app/hero-detail.component.ts b/adev/src/content/examples/template-syntax/src/app/hero-detail.component.ts deleted file mode 100644 index 55b1d39e7de6..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/hero-detail.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-disable @angular-eslint/no-inputs-metadata-property, @angular-eslint/no-outputs-metadata-property */ -import {Component, EventEmitter, Input, Output} from '@angular/core'; -import {CurrencyPipe, DatePipe} from '@angular/common'; - -import {Hero} from './hero'; - -@Component({ - standalone: true, - selector: 'app-hero-detail', - inputs: ['hero'], - outputs: ['deleteRequest'], - styles: ['button {margin-left: 8px} div {margin: 8px 0} img {height:24px}'], - template: ` -
- {{hero.name}} - - {{prefix}} {{hero.name}} - - -
`, -}) -export class HeroDetailComponent { - hero = new Hero(-1, '', 'Zzzzzzzz'); // default sleeping hero - // heroImageUrl = 'https://wpclipart.com/cartoon/people/hero/hero_silhoutte.png.html'; - // Public Domain terms of use: https://wpclipart.com/terms.html - heroImageUrl = 'assets/images/hero.svg'; - lineThrough = ''; - @Input() prefix = ''; - - // This component makes a request but it can't actually delete a hero. - deleteRequest = new EventEmitter(); - - delete() { - this.deleteRequest.emit(this.hero); - this.lineThrough = this.lineThrough ? '' : 'line-through'; - } -} - -@Component({ - standalone: true, - selector: 'app-big-hero-detail', - template: ` -
- {{hero.name}} -
{{hero.name}}
-
Name: {{hero.name}}
-
Emotion: {{hero.emotion}}
-
Birthdate: {{hero.birthdate | date:'longDate'}}
- -
Rate/hr: {{hero.rate | currency:'EUR'}}
-
- -
- `, - imports: [CurrencyPipe, DatePipe], - styles: [ - ` - .detail { border: 1px solid black; padding: 4px; max-width: 450px; } - img { float: left; margin-right: 8px; height: 100px; } - `, - ], -}) -export class BigHeroDetailComponent extends HeroDetailComponent { - @Input() override hero!: Hero; - @Output() override deleteRequest = new EventEmitter(); - - override delete() { - this.deleteRequest.emit(this.hero); - } -} diff --git a/adev/src/content/examples/template-syntax/src/app/hero-form.component.html b/adev/src/content/examples/template-syntax/src/app/hero-form.component.html deleted file mode 100644 index 66d2657fd439..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/hero-form.component.html +++ /dev/null @@ -1,21 +0,0 @@ -
- -
-
- - -
- -
-
- {{submitMessage}} -
- -
diff --git a/adev/src/content/examples/template-syntax/src/app/hero-form.component.ts b/adev/src/content/examples/template-syntax/src/app/hero-form.component.ts deleted file mode 100644 index e6b4146552d3..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/hero-form.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {Component, Input, ViewChild} from '@angular/core'; -import {FormsModule, NgForm} from '@angular/forms'; - -import {Hero} from './hero'; - -@Component({ - standalone: true, - selector: 'app-hero-form', - templateUrl: './hero-form.component.html', - imports: [FormsModule], - styles: [ - ` - button { margin: 6px 0; } - #heroForm { border: 1px solid black; margin: 20px 0; padding: 8px; max-width: 350px; } - `, - ], -}) -export class HeroFormComponent { - @Input() hero!: Hero; - @ViewChild('heroForm') form!: NgForm; - - private _submitMessage = ''; - - get submitMessage() { - if (this.form && !this.form.valid) { - this._submitMessage = ''; - } - return this._submitMessage; - } - - onSubmit(form: NgForm) { - this._submitMessage = 'Submitted. form value is ' + JSON.stringify(form.value); - } -} diff --git a/adev/src/content/examples/template-syntax/src/app/hero-switch.components.ts b/adev/src/content/examples/template-syntax/src/app/hero-switch.components.ts deleted file mode 100644 index a92c5909f72f..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/hero-switch.components.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {Component, Input} from '@angular/core'; -import {Hero} from './hero'; - -@Component({ - standalone: true, - selector: 'app-happy-hero', - template: 'Wow. You like {{hero.name}}. What a happy hero ... just like you.', -}) -export class HappyHeroComponent { - @Input() hero!: Hero; -} - -@Component({ - standalone: true, - selector: 'app-sad-hero', - template: 'You like {{hero.name}}? Such a sad hero. Are you sad too?', -}) -export class SadHeroComponent { - @Input() hero!: Hero; -} - -@Component({ - standalone: true, - selector: 'app-confused-hero', - template: 'Are you as confused as {{hero.name}}?', -}) -export class ConfusedHeroComponent { - @Input() hero!: Hero; -} - -@Component({ - standalone: true, - selector: 'app-unknown-hero', - template: '{{message}}', -}) -export class UnknownHeroComponent { - @Input() hero!: Hero; - get message() { - return this.hero && this.hero.name - ? `${this.hero.name} is strange and mysterious.` - : 'Are you feeling indecisive?'; - } -} - -export const heroSwitchComponents = [ - HappyHeroComponent, - SadHeroComponent, - ConfusedHeroComponent, - UnknownHeroComponent, -]; diff --git a/adev/src/content/examples/template-syntax/src/app/hero.ts b/adev/src/content/examples/template-syntax/src/app/hero.ts deleted file mode 100644 index 9a3cffcb1a55..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/hero.ts +++ /dev/null @@ -1,31 +0,0 @@ -export class Hero { - static nextId = 0; - - static heroes: Hero[] = [ - new Hero( - 0, - 'Hercules', - 'happy', - new Date(1970, 1, 25), - 'https://www.imdb.com/title/tt0065832/', - 325, - ), - new Hero(1, 'Dr Nice', 'happy'), - new Hero(2, 'RubberMan', 'sad'), - new Hero(3, 'Windstorm', 'confused'), - new Hero(4, 'Magneta'), - ]; - - constructor( - public id = Hero.nextId++, - public name?: string, - public emotion?: string, - public birthdate?: Date, - public url?: string, - public rate = 100, - ) {} - - clone(): Hero { - return Object.assign(new Hero(), this); - } -} diff --git a/adev/src/content/examples/template-syntax/src/app/sizer.component.ts b/adev/src/content/examples/template-syntax/src/app/sizer.component.ts deleted file mode 100644 index a397f38ba2b1..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/sizer.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -// #docregion -import {Component, EventEmitter, Input, Output} from '@angular/core'; - -@Component({ - standalone: true, - selector: 'app-sizer', - template: ` -
- - - FontSize: {{size}}px -
`, -}) -export class SizerComponent { - @Input() size!: number | string; - @Output() sizeChange = new EventEmitter(); - - dec() { - this.resize(-1); - } - inc() { - this.resize(+1); - } - - resize(delta: number) { - this.size = Math.min(40, Math.max(8, +this.size + delta)); - this.sizeChange.emit(this.size); - } -} diff --git a/adev/src/content/examples/template-syntax/src/app/svg.component.css b/adev/src/content/examples/template-syntax/src/app/svg.component.css deleted file mode 100644 index c849558dfec1..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/svg.component.css +++ /dev/null @@ -1,4 +0,0 @@ -svg { - display: block; - width: 100%; -} diff --git a/adev/src/content/examples/template-syntax/src/app/svg.component.svg b/adev/src/content/examples/template-syntax/src/app/svg.component.svg deleted file mode 100644 index 634b66df8caa..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/svg.component.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - click the rectangle to change the fill color - - diff --git a/adev/src/content/examples/template-syntax/src/app/svg.component.ts b/adev/src/content/examples/template-syntax/src/app/svg.component.ts deleted file mode 100644 index f2165dee478d..000000000000 --- a/adev/src/content/examples/template-syntax/src/app/svg.component.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {Component} from '@angular/core'; - -@Component({ - standalone: true, - selector: 'app-svg', - templateUrl: './svg.component.svg', - styleUrls: ['./svg.component.css'], -}) -export class SvgComponent { - fillColor = 'rgb(255, 0, 0)'; - - changeColor() { - const r = Math.floor(Math.random() * 256); - const g = Math.floor(Math.random() * 256); - const b = Math.floor(Math.random() * 256); - this.fillColor = `rgb(${r}, ${g}, ${b})`; - } -} diff --git a/adev/src/content/examples/template-syntax/src/assets/images/hero.svg b/adev/src/content/examples/template-syntax/src/assets/images/hero.svg deleted file mode 100644 index f0a002fc7249..000000000000 --- a/adev/src/content/examples/template-syntax/src/assets/images/hero.svg +++ /dev/null @@ -1,202 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - image/svg+xml - - - - - - Codestin Search App - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/adev/src/content/examples/template-syntax/src/assets/images/ng-logo.svg b/adev/src/content/examples/template-syntax/src/assets/images/ng-logo.svg deleted file mode 100644 index ac78f203d21f..000000000000 --- a/adev/src/content/examples/template-syntax/src/assets/images/ng-logo.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - diff --git a/adev/src/content/examples/template-syntax/src/assets/images/villain.svg b/adev/src/content/examples/template-syntax/src/assets/images/villain.svg deleted file mode 100644 index 75851ef7d960..000000000000 --- a/adev/src/content/examples/template-syntax/src/assets/images/villain.svg +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - diff --git a/adev/src/content/examples/template-syntax/src/index.html b/adev/src/content/examples/template-syntax/src/index.html deleted file mode 100644 index 866fc067cb48..000000000000 --- a/adev/src/content/examples/template-syntax/src/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Codestin Search App - - - - - - - - - - diff --git a/adev/src/content/examples/template-syntax/src/main.ts b/adev/src/content/examples/template-syntax/src/main.ts deleted file mode 100644 index bba4ddf9ad8e..000000000000 --- a/adev/src/content/examples/template-syntax/src/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser'; - -import {AppComponent} from './app/app.component'; - -bootstrapApplication(AppComponent, {providers: [provideProtractorTestingSupport()]}).catch((err) => - console.error(err), -); diff --git a/adev/src/content/examples/template-syntax/stackblitz.json b/adev/src/content/examples/template-syntax/stackblitz.json deleted file mode 100644 index 5ba51f973b33..000000000000 --- a/adev/src/content/examples/template-syntax/stackblitz.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "description": "Template Syntax Collection", - "files":["!**/*.d.ts", "!**/*.js"], - "tags": ["template"] -} diff --git a/adev/src/content/examples/two-way-binding/BUILD.bazel b/adev/src/content/examples/two-way-binding/BUILD.bazel deleted file mode 100644 index 9d9843d33b88..000000000000 --- a/adev/src/content/examples/two-way-binding/BUILD.bazel +++ /dev/null @@ -1,8 +0,0 @@ -package(default_visibility = ["//visibility:public"]) - -exports_files([ - "src/app/app.component.html", - "src/app/app.component.ts", - "src/app/sizer/sizer.component.html", - "src/app/sizer/sizer.component.ts", -]) diff --git a/adev/src/content/examples/two-way-binding/e2e/app.po.ts b/adev/src/content/examples/two-way-binding/e2e/app.po.ts deleted file mode 100644 index 27fb1afae071..000000000000 --- a/adev/src/content/examples/two-way-binding/e2e/app.po.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {browser, by, element} from 'protractor'; - -export class AppPage { - navigateTo() { - return browser.get('/'); - } - - getParagraphText() { - return element(by.css('app-root h1')).getText(); - } -} diff --git a/adev/src/content/examples/two-way-binding/e2e/src/app.e2e-spec.ts b/adev/src/content/examples/two-way-binding/e2e/src/app.e2e-spec.ts deleted file mode 100644 index f339f4af25a3..000000000000 --- a/adev/src/content/examples/two-way-binding/e2e/src/app.e2e-spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {browser, element, by} from 'protractor'; - -describe('Two-way binding e2e tests', () => { - beforeEach(() => browser.get('')); - - const minusButton = element.all(by.css('button')).get(0); - const plusButton = element.all(by.css('button')).get(1); - const minus2Button = element.all(by.css('button')).get(2); - const plus2Button = element.all(by.css('button')).get(3); - - it('should display Two-way Binding', async () => { - expect(await element(by.css('h1')).getText()).toEqual('Two-way Binding'); - }); - - it('should display four buttons', async () => { - expect(await minusButton.getText()).toBe('-'); - expect(await plusButton.getText()).toBe('+'); - expect(await minus2Button.getText()).toBe('-'); - expect(await plus2Button.getText()).toBe('+'); - }); - - it('should change font size texts', async () => { - await minusButton.click(); - expect(await element.all(by.css('span')).get(0).getText()).toEqual('FontSize: 15px'); - expect(await element.all(by.css('input')).get(0).getAttribute('value')).toEqual('15'); - - await plusButton.click(); - expect(await element.all(by.css('span')).get(0).getText()).toEqual('FontSize: 16px'); - expect(await element.all(by.css('input')).get(0).getAttribute('value')).toEqual('16'); - - await minus2Button.click(); - expect(await element.all(by.css('span')).get(1).getText()).toEqual('FontSize: 15px'); - }); - - it('should display De-sugared two-way binding', async () => { - expect(await element(by.css('h2')).getText()).toEqual('De-sugared two-way binding'); - }); -}); diff --git a/adev/src/content/examples/two-way-binding/example-config.json b/adev/src/content/examples/two-way-binding/example-config.json deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/adev/src/content/examples/two-way-binding/src/app/app.component.css b/adev/src/content/examples/two-way-binding/src/app/app.component.css deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/adev/src/content/examples/two-way-binding/src/app/app.component.html b/adev/src/content/examples/two-way-binding/src/app/app.component.html deleted file mode 100644 index 53e41b602fd0..000000000000 --- a/adev/src/content/examples/two-way-binding/src/app/app.component.html +++ /dev/null @@ -1,19 +0,0 @@ -

Two-way Binding

-
- - - - -
Resizable Text
- - -
-
-
-

De-sugared two-way binding

- - - -
- - diff --git a/adev/src/content/examples/two-way-binding/src/app/app.component.ts b/adev/src/content/examples/two-way-binding/src/app/app.component.ts deleted file mode 100644 index 87a8e9310f23..000000000000 --- a/adev/src/content/examples/two-way-binding/src/app/app.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {Component} from '@angular/core'; -import {SizerComponent} from './sizer/sizer.component'; -import {FormsModule} from '@angular/forms'; - -@Component({ - standalone: true, - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'], - imports: [FormsModule, SizerComponent], -}) -export class AppComponent { - // #docregion font-size - fontSizePx = 16; - // #enddocregion font-size -} diff --git a/adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.css b/adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.css deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.html b/adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.html deleted file mode 100644 index 533ab9f46dae..000000000000 --- a/adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.html +++ /dev/null @@ -1,5 +0,0 @@ -
- - - FontSize: {{size}}px -
diff --git a/adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.ts b/adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.ts deleted file mode 100644 index 305d45599d53..000000000000 --- a/adev/src/content/examples/two-way-binding/src/app/sizer/sizer.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {Component, Input, Output, EventEmitter} from '@angular/core'; - -@Component({ - standalone: true, - selector: 'app-sizer', - templateUrl: './sizer.component.html', - styleUrls: ['./sizer.component.css'], -}) -// #docregion sizer-component -export class SizerComponent { - @Input() size!: number | string; - @Output() sizeChange = new EventEmitter(); - - dec() { - this.resize(-1); - } - inc() { - this.resize(+1); - } - - resize(delta: number) { - this.size = Math.min(40, Math.max(8, +this.size + delta)); - this.sizeChange.emit(this.size); - } -} -// #enddocregion sizer-component diff --git a/adev/src/content/examples/two-way-binding/src/index.html b/adev/src/content/examples/two-way-binding/src/index.html deleted file mode 100644 index 04db6d5de59b..000000000000 --- a/adev/src/content/examples/two-way-binding/src/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Codestin Search App - - - - - - - - diff --git a/adev/src/content/examples/two-way-binding/src/main.ts b/adev/src/content/examples/two-way-binding/src/main.ts deleted file mode 100644 index 46d4cf887072..000000000000 --- a/adev/src/content/examples/two-way-binding/src/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser'; - -import {AppComponent} from './app/app.component'; - -bootstrapApplication(AppComponent, { - providers: [provideProtractorTestingSupport()], -}); diff --git a/adev/src/content/examples/two-way-binding/stackblitz.json b/adev/src/content/examples/two-way-binding/stackblitz.json deleted file mode 100644 index 912837223904..000000000000 --- a/adev/src/content/examples/two-way-binding/stackblitz.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "description": "Two-way binding", - "files": [ - "!**/*.d.ts", - "!**/*.js", - "!**/*.[1,2].*" - ], - "file": "src/app/app.component.ts", - "tags": ["Two-way binding"] -} diff --git a/adev/src/content/guide/components/queries.md b/adev/src/content/guide/components/queries.md index bbb27f62311c..b7b7b64600ed 100644 --- a/adev/src/content/guide/components/queries.md +++ b/adev/src/content/guide/components/queries.md @@ -29,7 +29,7 @@ export class CustomCard { @ViewChild(CustomCardHeader) header: CustomCardHeader; ngAfterViewInit() { - console.log(this.header.text); + console.log(this.header.text); } } @@ -165,7 +165,7 @@ This first parameter for each query decorator is its **locator**. Most of the time, you want to use a component or directive as your locator. You can alternatively specify a string locator corresponding to -a [template reference variable](guide/templates/reference-variables). +a [template reference variable](guide/templates/variables#template-reference-variables). ```angular-ts @Component({ diff --git a/adev/src/content/guide/defer.md b/adev/src/content/guide/defer.md index 9a2c3bc422b6..21a5537af259 100644 --- a/adev/src/content/guide/defer.md +++ b/adev/src/content/guide/defer.md @@ -52,13 +52,13 @@ The `@placeholder` block accepts an optional parameter to specify the `minimum` } ``` -Note: Certain triggers may require the presence of either a `@placeholder` or a [template reference variable](guide/templates/reference-variables) to function. See the [Triggers](guide/defer#triggers) section for more details. +Note: Certain triggers may require the presence of either a `@placeholder` or a [template reference variable](guide/templates/variables#template-reference-variables) to function. See the [Triggers](guide/defer#triggers) section for more details. ### `@loading` The `@loading` block is an optional block that allows you to declare content that will be shown during the loading of any deferred dependencies. Its dependences are eagerly loaded (similar to `@placeholder`). -For example, you could show a loading spinner. Once loading has been triggered, the `@loading` block replaces the `@placeholder` block. +For example, you could show a loading spinner. Once loading has been triggered, the `@loading` block replaces the `@placeholder` block. The `@loading` block accepts two optional parameters to specify the `minimum` amount of time that this placeholder should be shown and amount of time to wait `after` loading begins before showing the loading template. `minimum` and `after` parameters are specified in time increments of milliseconds (ms) or seconds (s). Just like `@placeholder`, these parameters exist to prevent fast flickering of content in the case that the deferred dependencies are fetched quickly. Both the `minimum` and `after` timers for the `@loading` block begins immediately after the loading has been triggered. @@ -140,7 +140,7 @@ By default, the placeholder will act as the element watched for entering viewpor } ``` -Alternatively, you can specify a [template reference variable](guide/templates/reference-variables) in the same template as the `@defer` block as the element that is watched to enter the viewport. This variable is passed in as a parameter on the viewport trigger. +Alternatively, you can specify a [template reference variable](guide/templates/variables#template-reference-variables) in the same template as the `@defer` block as the element that is watched to enter the viewport. This variable is passed in as a parameter on the viewport trigger. ```angular-html
Hello!
@@ -164,7 +164,7 @@ By default, the placeholder will act as the interaction element as long as it is } ``` -Alternatively, you can specify a [template reference variable](guide/templates/reference-variables) as the element that triggers interaction. This variable is passed in as a parameter on the interaction trigger. +Alternatively, you can specify a [template reference variable](guide/templates/variables#template-reference-variables) as the element that triggers interaction. This variable is passed in as a parameter on the interaction trigger. ```angular-html @@ -190,7 +190,7 @@ By default, the placeholder will act as the hover element as long as it is a sin } ``` -Alternatively, you can specify a [template reference variable](guide/templates/reference-variables) as the hover element. This variable is passed in as a parameter on the hover trigger. +Alternatively, you can specify a [template reference variable](guide/templates/variables#template-reference-variables) as the hover element. This variable is passed in as a parameter on the hover trigger. ```angular-html
Hello!
diff --git a/adev/src/content/guide/directives/overview.md b/adev/src/content/guide/directives/overview.md index 43f52ee63a55..9c223f4858a1 100644 --- a/adev/src/content/guide/directives/overview.md +++ b/adev/src/content/guide/directives/overview.md @@ -51,17 +51,17 @@ Because `isSpecial` is true, `ngClass` applies the class of `special` to the ` + 1. In the template, add the `ngClass` property binding to `currentClasses` to set the element's classes: - + For this use case, Angular applies the classes on initialization and in case of changes caused by reassigning the `currentClasses` object. The full example calls `setCurrentClasses()` initially with `ngOnInit()` when the user clicks on the `Refresh currentClasses` button. @@ -79,13 +79,13 @@ Use `NgStyle` to set multiple inline styles simultaneously, based on the state o 1. To use `NgStyle`, add a method to the component class. - In the following example, `setCurrentStyles()` sets the property `currentStyles` with an object that defines three styles, based on the state of three other component properties. + In the following example, `setCurrentStyles()` sets the property `currentStyles` with an object that defines three styles, based on the state of three other component properties. - + 1. To set the element's styles, add an `ngStyle` property binding to `currentStyles`. - + For this use case, Angular applies the styles upon initialization and in case of changes. To do this, the full example calls `setCurrentStyles()` initially with `ngOnInit()` and when the dependent properties change through a button click. @@ -97,16 +97,16 @@ Use the `NgModel` directive to display a data property and update that property 1. Import `FormsModule` and add it to the AppComponent's `imports` list. - + 1. Add an `[(ngModel)]` binding on an HTML `
` element and set it equal to the property, here `name`. - + - This `[(ngModel)]` syntax can only set a data-bound property. + This `[(ngModel)]` syntax can only set a data-bound property. To customize your configuration, write the expanded form, which separates the property and event binding. -Use [property binding](guide/templates/property-binding) to set the property and [event binding](guide/templates/event-binding) to respond to changes. +Use [property binding](guide/templates/property-binding) to set the property and [event binding](guide/templates/event-listeners) to respond to changes. The following example changes the `` value to uppercase: @@ -253,14 +253,14 @@ Reduce the number of calls your application makes to the server by tracking chan With the `*ngFor` `trackBy` property, Angular can change and re-render only those items that have changed, rather than reloading the entire list of items. 1. Add a method to the component that returns the value `NgFor` should track. - In this example, the value to track is the item's `id`. - If the browser has already rendered `id`, Angular keeps track of it and doesn't re-query the server for the same `id`. +In this example, the value to track is the item's `id`. +If the browser has already rendered `id`, Angular keeps track of it and doesn't re-query the server for the same `id`. - + 1. In the shorthand expression, set `trackBy` to the `trackByItems()` method. - + **Change ids** creates new items with new `item.id`s. In the following illustration of the `trackBy` effect, **Reset items** creates new items with the same `item.id`s. @@ -288,9 +288,9 @@ Here's a conditional paragraph using ``. 1. To conditionally exclude an `