diff --git a/.github/actions/yarn-install/action.yml b/.github/actions/yarn-install/action.yml index afe572f29af758..7455b8479a9cb7 100644 --- a/.github/actions/yarn-install/action.yml +++ b/.github/actions/yarn-install/action.yml @@ -4,7 +4,7 @@ description: 'Installs the dependencies using Yarn' runs: using: 'composite' steps: - - uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4 + - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4 with: path: | ./node_modules/ diff --git a/adev/shared-docs/styles/global-styles.scss b/adev/shared-docs/styles/global-styles.scss index 8093ddb1450b56..5c073d2f32ece6 100644 --- a/adev/shared-docs/styles/global-styles.scss +++ b/adev/shared-docs/styles/global-styles.scss @@ -64,6 +64,7 @@ $theme: mat.m2-define-light-theme( @include mat.core(); @include mat.tabs-theme($theme); @include mat.button-toggle-theme($theme); +@include mat.tooltip-theme($theme); // Include custom docs styles @include alert.docs-alert(); @@ -116,7 +117,7 @@ $theme: mat.m2-define-light-theme( &.cli { padding-inline-start: 1rem; } - + a { color: inherit; &:hover { diff --git a/adev/src/app/editor/code-editor/code-editor.component.html b/adev/src/app/editor/code-editor/code-editor.component.html index 06b87384d0b058..93d50cde385f7f 100644 --- a/adev/src/app/editor/code-editor/code-editor.component.html +++ b/adev/src/app/editor/code-editor/code-editor.component.html @@ -89,7 +89,9 @@ class="adev-editor-download-button" type="button" (click)="downloadCurrentCodeEditorState()" - aria-label="Download current code in editor" + aria-label="Download current source code" + matTooltip="Download current source code" + matTooltipPosition="above" > download diff --git a/adev/src/app/editor/code-editor/code-editor.component.spec.ts b/adev/src/app/editor/code-editor/code-editor.component.spec.ts index 0e9099cdef422f..65c0d9ba7e6c4d 100644 --- a/adev/src/app/editor/code-editor/code-editor.component.spec.ts +++ b/adev/src/app/editor/code-editor/code-editor.component.spec.ts @@ -21,6 +21,8 @@ import {CodeEditor, REQUIRED_FILES} from './code-editor.component'; import {CodeMirrorEditor} from './code-mirror-editor.service'; import {FakeChangeDetectorRef} from '@angular/docs'; import {TutorialType} from '@angular/docs'; +import {MatTooltip} from '@angular/material/tooltip'; +import {MatTooltipHarness} from '@angular/material/tooltip/testing'; const files = [ {filename: 'a', content: '', language: {} as any}, @@ -51,7 +53,7 @@ describe('CodeEditor', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CodeEditor, NoopAnimationsModule], + imports: [CodeEditor, NoopAnimationsModule, MatTooltip], providers: [ { provide: CodeMirrorEditor, @@ -200,4 +202,28 @@ describe('CodeEditor', () => { expect(fixture.debugElement.query(By.css('[aria-label="Delete file"]'))).toBeNull(); } }); + + it('should be able to display the tooltip on the download button', async () => { + const tooltip = await loader.getHarness( + MatTooltipHarness.with({selector: '.adev-editor-download-button'}), + ); + expect(await tooltip.isOpen()).toBeFalse(); + await tooltip.show(); + expect(await tooltip.isOpen()).toBeTrue(); + }); + + it('should be able to get the tooltip message on the download button', async () => { + const tooltip = await loader.getHarness( + MatTooltipHarness.with({selector: '.adev-editor-download-button'}), + ); + await tooltip.show(); + expect(await tooltip.getTooltipText()).toBe('Download current source code'); + }); + + it('should not be able to get the tooltip message on the download button when the tooltip is not shown', async () => { + const tooltip = await loader.getHarness( + MatTooltipHarness.with({selector: '.adev-editor-download-button'}), + ); + expect(await tooltip.getTooltipText()).toBe(''); + }); }); diff --git a/adev/src/app/editor/code-editor/code-editor.component.ts b/adev/src/app/editor/code-editor/code-editor.component.ts index 19a6f0510c38a4..99fa1ddcc79e67 100644 --- a/adev/src/app/editor/code-editor/code-editor.component.ts +++ b/adev/src/app/editor/code-editor/code-editor.component.ts @@ -33,6 +33,7 @@ import {StackBlitzOpener} from '../stackblitz-opener.service'; import {ClickOutside, IconComponent} from '@angular/docs'; import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu'; import {IDXLauncher} from '../idx-launcher.service'; +import {MatTooltip} from '@angular/material/tooltip'; export const REQUIRED_FILES = new Set([ 'src/main.ts', @@ -48,7 +49,15 @@ const ANGULAR_DEV = 'https://angular.dev'; templateUrl: './code-editor.component.html', styleUrls: ['./code-editor.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [MatTabsModule, IconComponent, ClickOutside, CdkMenu, CdkMenuItem, CdkMenuTrigger], + imports: [ + MatTabsModule, + MatTooltip, + IconComponent, + ClickOutside, + CdkMenu, + CdkMenuItem, + CdkMenuTrigger, + ], }) export class CodeEditor implements AfterViewInit, OnDestroy { @ViewChild('codeEditorWrapper') private codeEditorWrapperRef!: ElementRef; diff --git a/adev/src/content/guide/di/dependency-injection-providers.md b/adev/src/content/guide/di/dependency-injection-providers.md index e8065ff70b40f9..afd9575b67498b 100644 --- a/adev/src/content/guide/di/dependency-injection-providers.md +++ b/adev/src/content/guide/di/dependency-injection-providers.md @@ -161,6 +161,10 @@ The following example defines a token, `APP_CONFIG`. of the type `InjectionToken import { InjectionToken } from '@angular/core'; +export interface AppConfig { + title: string; +} + export const APP_CONFIG = new InjectionToken('app.config description'); @@ -169,6 +173,10 @@ The optional type parameter, ``, and the token description, `app.conf Next, register the dependency provider in the component using the `InjectionToken` object of `APP_CONFIG`: +const MY_APP_CONFIG_VARIABLE: AppConfig = { + title: 'Hello', +}; + providers: [{ provide: APP_CONFIG, useValue: MY_APP_CONFIG_VARIABLE }] diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts b/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts index 5d1b7c76e2b705..2f73e7e5fc7cd4 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts @@ -264,6 +264,7 @@ const getDependenciesForDirective = ( let dependencies = ngDebugClient().ɵgetDependenciesFromInjectable(injector, directive)?.dependencies ?? []; + const uniqueServices = new Set(); const serializedInjectedServices: SerializedInjectedService[] = []; let position = 0; @@ -298,24 +299,43 @@ const getDependenciesForDirective = ( }), ]; - if (dependency.token && isInjectionToken(dependency.token)) { - serializedInjectedServices.push({ - token: dependency.token!.toString(), + let flags = dependency.flags as InjectOptions; + let flagToken = ''; + if (flags !== undefined) { + // TODO: We need to remove this once the InjectFlags enum is removed from core + if (typeof flags === 'number') { + flags = { + optional: !!(flags & 8), + skipSelf: !!(flags & 4), + self: !!(flags & 2), + host: !!(flags & 1), + }; + } + flagToken = (['optional', 'skipSelf', 'self', 'host'] as (keyof InjectOptions)[]) + .filter((key) => flags[key]) + .join('-'); + } + + const serviceKey = `${dependency.token}-${flagToken}`; + if (!uniqueServices.has(serviceKey)) { + uniqueServices.add(serviceKey); + + const service = { + token: valueToLabel(dependency.token), value: valueToLabel(dependency.value), - flags: dependency.flags as InjectOptions, - position: [position++], + flags, + position: [position], resolutionPath: dependencyResolutionPath, - }); - continue; + }; + + if (dependency.token && isInjectionToken(dependency.token)) { + service.token = dependency.token!.toString(); + } + + serializedInjectedServices.push(service); } - serializedInjectedServices.push({ - token: valueToLabel(dependency.token), - value: valueToLabel(dependency.value), - flags: dependency.flags as InjectOptions, - position: [position++], - resolutionPath: dependencyResolutionPath, - }); + position++; } return serializedInjectedServices; 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 a9ff113286afe3..156a4a36d19e2a 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 @@ -2709,21 +2709,11 @@ class TcbExpressionTranslator { * context). This method assists in resolving those. */ protected resolve(ast: AST): ts.Expression | null { - // TODO: this is actually a bug, because `ImplicitReceiver` extends `ThisReceiver`. Consider a - // case when the explicit `this` read is inside a template with a context that also provides the - // variable name being read: - // ``` - // {{this.a}} - // ``` - // Clearly, `this.a` should refer to the class property `a`. However, because of this code, - // `this.a` will refer to `let-a` on the template context. - // - // Note that the generated code is actually consistent with this bug. To fix it, we have to: - // - Check `!(ast.receiver instanceof ThisReceiver)` in this condition - // - Update `ingest.ts` in the Template Pipeline (see the corresponding comment) - // - Turn off legacy TemplateDefinitionBuilder - // - Fix g3, and release in a major version - if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) { + if ( + ast instanceof PropertyRead && + ast.receiver instanceof ImplicitReceiver && + !(ast.receiver instanceof ThisReceiver) + ) { // Try to resolve a bound target for this expression. If no such target is available, then // the expression is referencing the top-level component context. In that case, `null` is // returned here to let it fall through resolution so it will be caught when the diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/GOLDEN_PARTIAL.js index c65ce590881c01..330673b8425cdf 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/GOLDEN_PARTIAL.js @@ -1249,13 +1249,13 @@ import * as i0 from "@angular/core"; export class MyComponent { } MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); -MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: '{{this.a}}', isInline: true }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: '{{a}}', isInline: true }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ type: Component, args: [{ selector: 'my-component', standalone: true, - template: '{{this.a}}', + template: '{{a}}', }] }] }); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/ng_template_implicit.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/ng_template_implicit.js index bd438ff81bac8c..9591f67d00b03d 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/ng_template_implicit.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/ng_template_implicit.js @@ -2,7 +2,6 @@ MyComponent_ng_template_0_Template(rf, ctx) { if (rf & 1) { i0.ɵɵtext(0); } if (rf & 2) { - // NOTE: The fact that `this.` still refers to template context is a TDB and TCB bug; we should fix this eventually. const $a_r1$ = ctx.$implicit; i0.ɵɵtextInterpolate($a_r1$); } @@ -16,4 +15,4 @@ function MyComponent_Template(rf, ctx) { } if (rf & 2) { i0.ɵɵproperty("ngIf", true); } -} \ No newline at end of file +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/ng_template_implicit.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/ng_template_implicit.ts index d4033861a55c18..62a9a9a1563d6a 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/ng_template_implicit.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/ng_template_implicit.ts @@ -3,7 +3,7 @@ import {Component} from '@angular/core'; @Component({ selector: 'my-component', standalone: true, - template: '{{this.a}}', + template: '{{a}}', }) export class MyComponent { p1!: any; diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 23f170fa2bcbba..e2236966699c86 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -290,7 +290,6 @@ export function compileComponentFromMetadata( let hasStyles = !!meta.externalStyles?.length; // e.g. `styles: [str1, str2]` if (meta.styles && meta.styles.length) { - hasStyles = true; const styleValues = meta.encapsulation == core.ViewEncapsulation.Emulated ? compileStyles(meta.styles, CONTENT_ATTR, HOST_ATTR) @@ -303,6 +302,7 @@ export function compileComponentFromMetadata( }, [] as o.Expression[]); if (styleNodes.length > 0) { + hasStyles = true; definitionMap.set('styles', o.literalArr(styleNodes)); } } diff --git a/packages/compiler/src/render3/view/t2_binder.ts b/packages/compiler/src/render3/view/t2_binder.ts index be87bac5a782cf..8d19507022f867 100644 --- a/packages/compiler/src/render3/view/t2_binder.ts +++ b/packages/compiler/src/render3/view/t2_binder.ts @@ -922,21 +922,13 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor { private maybeMap(ast: PropertyRead | SafePropertyRead | PropertyWrite, name: string): void { // If the receiver of the expression isn't the `ImplicitReceiver`, this isn't the root of an // `AST` expression that maps to a `Variable` or `Reference`. - if (!(ast.receiver instanceof ImplicitReceiver)) { + if (!(ast.receiver instanceof ImplicitReceiver) || ast.receiver instanceof ThisReceiver) { return; } // Check whether the name exists in the current scope. If so, map it. Otherwise, the name is // probably a property on the top-level component context. const target = this.scope.lookup(name); - - // It's not allowed to read template entities via `this`, however it previously worked by - // accident (see #55115). Since `@let` declarations are new, we can fix it from the beginning, - // whereas pre-existing template entities will be fixed in #55115. - if (target instanceof LetDeclaration && ast.receiver instanceof ThisReceiver) { - return; - } - if (target !== null) { this.bindings.set(ast, target); } diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index c891c9066b77d5..5c51a7623dcaa7 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -1003,30 +1003,10 @@ function convertAst( if (ast instanceof e.ASTWithSource) { return convertAst(ast.ast, job, baseSourceSpan); } else if (ast instanceof e.PropertyRead) { - const isThisReceiver = ast.receiver instanceof e.ThisReceiver; // Whether this is an implicit receiver, *excluding* explicit reads of `this`. const isImplicitReceiver = ast.receiver instanceof e.ImplicitReceiver && !(ast.receiver instanceof e.ThisReceiver); - // Whether the name of the read is a node that should be never retain its explicit this - // receiver. - const isSpecialNode = ast.name === '$any' || ast.name === '$event'; - // TODO: The most sensible condition here would be simply `isImplicitReceiver`, to convert only - // actual implicit `this` reads, and not explicit ones. However, TemplateDefinitionBuilder (and - // the Typecheck block!) both have the same bug, in which they also consider explicit `this` - // reads to be implicit. This causes problems when the explicit `this` read is inside a - // template with a context that also provides the variable name being read: - // ``` - // {{this.a}} - // ``` - // The whole point of the explicit `this` was to access the class property, but TDB and the - // current TCB treat the read as implicit, and give you the context property instead! - // - // For now, we emulate this old behavior by aggressively converting explicit reads to to - // implicit reads, except for the special cases that TDB and the current TCB protect. However, - // it would be an improvement to fix this. - // - // See also the corresponding comment for the TCB, in `type_check_block.ts`. - if (isImplicitReceiver || (isThisReceiver && !isSpecialNode)) { + if (isImplicitReceiver) { return new ir.LexicalReadExpr(ast.name); } else { return new o.ReadPropExpr( diff --git a/packages/compiler/test/render3/view/binding_spec.ts b/packages/compiler/test/render3/view/binding_spec.ts index 9d97a459e99b6b..2d054b9dd0a7fa 100644 --- a/packages/compiler/test/render3/view/binding_spec.ts +++ b/packages/compiler/test/render3/view/binding_spec.ts @@ -373,6 +373,35 @@ describe('t2 binding', () => { expect((target as a.LetDeclaration)?.name).toBe('value'); }); + it('should not resolve a `this` access to a template reference', () => { + const template = parseTemplate( + ` + + {{this.value}} + `, + '', + ); + const binder = new R3TargetBinder(new SelectorMatcher()); + const res = binder.bind({template: template.nodes}); + const interpolationWrapper = (template.nodes[1] as a.BoundText).value as e.ASTWithSource; + const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0]; + const target = res.getExpressionTarget(propertyRead); + + expect(target).toBe(null); + }); + + it('should not resolve a `this` access to a template variable', () => { + const template = parseTemplate(`{{this.value}}`, ''); + const binder = new R3TargetBinder(new SelectorMatcher()); + const res = binder.bind({template: template.nodes}); + const templateNode = template.nodes[0] as a.Template; + const interpolationWrapper = (templateNode.children[0] as a.BoundText).value as e.ASTWithSource; + const propertyRead = (interpolationWrapper.ast as e.Interpolation).expressions[0]; + const target = res.getExpressionTarget(propertyRead); + + expect(target).toBe(null); + }); + it('should not resolve a `this` access to a `@let` declaration', () => { const template = parseTemplate( ` diff --git a/packages/core/schematics/migrations/signal-migration/src/utils/is_identifier_free_in_scope.ts b/packages/core/schematics/migrations/signal-migration/src/utils/is_identifier_free_in_scope.ts index 334142153d63e0..ea5ed640c5ae89 100644 --- a/packages/core/schematics/migrations/signal-migration/src/utils/is_identifier_free_in_scope.ts +++ b/packages/core/schematics/migrations/signal-migration/src/utils/is_identifier_free_in_scope.ts @@ -10,9 +10,12 @@ import assert from 'assert'; import ts from 'typescript'; import {isNodeDescendantOf} from './is_descendant_of'; +/** Symbol that can be used to mark a variable as reserved, synthetically. */ +export const ReservedMarker = Symbol(); + // typescript/stable/src/compiler/types.ts;l=967;rcl=651008033 export interface LocalsContainer extends ts.Node { - locals?: Map; + locals?: Map; nextContainer?: LocalsContainer; } @@ -71,9 +74,9 @@ function isIdentifierFreeInContainer(name: string, container: LocalsContainer): // Note: This check is similar to the check by the TypeScript emitter. // typescript/stable/src/compiler/emitter.ts;l=5436;rcl=651008033 const local = container.locals.get(name)!; - return !( - local.flags & - (ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue | ts.SymbolFlags.Alias) + return ( + local !== ReservedMarker && + !(local.flags & (ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue | ts.SymbolFlags.Alias)) ); } diff --git a/packages/core/schematics/migrations/signal-migration/src/utils/unique_names.ts b/packages/core/schematics/migrations/signal-migration/src/utils/unique_names.ts index 7b0bb988c0f031..2277083931d47e 100644 --- a/packages/core/schematics/migrations/signal-migration/src/utils/unique_names.ts +++ b/packages/core/schematics/migrations/signal-migration/src/utils/unique_names.ts @@ -7,7 +7,7 @@ */ import ts from 'typescript'; -import {isIdentifierFreeInScope} from './is_identifier_free_in_scope'; +import {isIdentifierFreeInScope, ReservedMarker} from './is_identifier_free_in_scope'; /** * Helper that can generate unique identifier names at a @@ -27,7 +27,8 @@ export class UniqueNamesGenerator { } // Claim the locals to avoid conflicts with future generations. - freeInfo.container.locals?.set(name, null! as ts.Symbol); + freeInfo.container.locals ??= new Map(); + freeInfo.container.locals.set(name, ReservedMarker); return true; }; diff --git a/packages/core/schematics/migrations/signal-migration/test/golden-test/temporary_variables.ts b/packages/core/schematics/migrations/signal-migration/test/golden-test/temporary_variables.ts new file mode 100644 index 00000000000000..ddf0a63707f090 --- /dev/null +++ b/packages/core/schematics/migrations/signal-migration/test/golden-test/temporary_variables.ts @@ -0,0 +1,23 @@ +// tslint:disable + +import {Directive, Input} from '@angular/core'; + +export class OtherCmp { + @Input() name = false; +} + +@Directive() +export class MyComp { + @Input() name = ''; + other: OtherCmp = null!; + + click() { + if (this.name) { + console.error(this.name); + } + + if (this.other.name) { + console.error(this.other.name); + } + } +} diff --git a/packages/core/schematics/migrations/signal-migration/test/golden.txt b/packages/core/schematics/migrations/signal-migration/test/golden.txt index f2fd5803be747b..0afbbfb0a9266a 100644 --- a/packages/core/schematics/migrations/signal-migration/test/golden.txt +++ b/packages/core/schematics/migrations/signal-migration/test/golden.txt @@ -602,12 +602,12 @@ export class AppComponent { if (isAudi(car)) { console.log(car.__audi); } - const narrowableMultipleTimes = ctx.narrowableMultipleTimes(); - if (!isCar(narrowableMultipleTimes!) || !isAudi(narrowableMultipleTimes)) { + const narrowableMultipleTimesValue = ctx.narrowableMultipleTimes(); + if (!isCar(narrowableMultipleTimesValue!) || !isAudi(narrowableMultipleTimesValue)) { return; } - narrowableMultipleTimes.__audi; + narrowableMultipleTimesValue.__audi; } // iife @@ -1249,6 +1249,33 @@ class TwoWayBinding { @Input() inputC = false; readonly inputD = input(false); } +@@@@@@ temporary_variables.ts @@@@@@ + +// tslint:disable + +import {Directive, input} from '@angular/core'; + +export class OtherCmp { + readonly name = input(false); +} + +@Directive() +export class MyComp { + readonly name = input(''); + other: OtherCmp = null!; + + click() { + const name = this.name(); + if (name) { + console.error(name); + } + + const nameValue = this.other.name(); + if (nameValue) { + console.error(nameValue); + } + } +} @@@@@@ transform_functions.ts @@@@@@ // tslint:disable diff --git a/packages/core/schematics/migrations/signal-migration/test/golden_best_effort.txt b/packages/core/schematics/migrations/signal-migration/test/golden_best_effort.txt index 29f26627cea3de..61d8945a34dae6 100644 --- a/packages/core/schematics/migrations/signal-migration/test/golden_best_effort.txt +++ b/packages/core/schematics/migrations/signal-migration/test/golden_best_effort.txt @@ -576,12 +576,12 @@ export class AppComponent { if (isAudi(car)) { console.log(car.__audi); } - const narrowableMultipleTimes = ctx.narrowableMultipleTimes(); - if (!isCar(narrowableMultipleTimes!) || !isAudi(narrowableMultipleTimes)) { + const narrowableMultipleTimesValue = ctx.narrowableMultipleTimes(); + if (!isCar(narrowableMultipleTimesValue!) || !isAudi(narrowableMultipleTimesValue)) { return; } - narrowableMultipleTimes.__audi; + narrowableMultipleTimesValue.__audi; } // iife @@ -1201,6 +1201,33 @@ class TwoWayBinding { readonly inputC = input(false); readonly inputD = input(false); } +@@@@@@ temporary_variables.ts @@@@@@ + +// tslint:disable + +import {Directive, input} from '@angular/core'; + +export class OtherCmp { + readonly name = input(false); +} + +@Directive() +export class MyComp { + readonly name = input(''); + other: OtherCmp = null!; + + click() { + const name = this.name(); + if (name) { + console.error(name); + } + + const nameValue = this.other.name(); + if (nameValue) { + console.error(nameValue); + } + } +} @@@@@@ transform_functions.ts @@@@@@ // tslint:disable diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 04fd6dc3dde1f4..bb3dc4bb4e86eb 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -99,6 +99,7 @@ export { export {createComponent, reflectComponentType, ComponentMirror} from './render3/component'; export {isStandalone} from './render3/definition'; export {AfterRenderPhase, AfterRenderRef} from './render3/after_render/api'; +export {publishExternalGlobalUtil as ɵpublishExternalGlobalUtil} from './render3/util/global_utils'; export { AfterRenderOptions, afterRender, diff --git a/packages/core/src/linker/view_ref.ts b/packages/core/src/linker/view_ref.ts index 55e4b7cdbb76f1..ef684ba61cdc18 100644 --- a/packages/core/src/linker/view_ref.ts +++ b/packages/core/src/linker/view_ref.ts @@ -100,13 +100,3 @@ export abstract class EmbeddedViewRef extends ViewRef { */ abstract get rootNodes(): any[]; } - -/** - * Interface for tracking root `ViewRef`s in `ApplicationRef`. - * - * NOTE: Importing `ApplicationRef` here directly creates circular dependency, which is why we have - * a subset of the `ApplicationRef` interface `ViewRefTracker` here. - */ -export interface ViewRefTracker { - detachView(viewRef: ViewRef): void; -} diff --git a/packages/core/src/render3/after_render/manager.ts b/packages/core/src/render3/after_render/manager.ts index 6c876bbcdd7095..3077ea920055b5 100644 --- a/packages/core/src/render3/after_render/manager.ts +++ b/packages/core/src/render3/after_render/manager.ts @@ -82,6 +82,9 @@ export class AfterRenderImpl { sequence.afterRun(); if (sequence.once) { this.sequences.delete(sequence); + // Destroy the sequence so its on destroy callbacks can be cleaned up + // immediately, instead of waiting until the injector is destroyed. + sequence.destroy(); } } diff --git a/packages/core/src/render3/util/global_utils.ts b/packages/core/src/render3/util/global_utils.ts index c51a8792e200c4..9a4501812c8f37 100644 --- a/packages/core/src/render3/util/global_utils.ts +++ b/packages/core/src/render3/util/global_utils.ts @@ -47,6 +47,13 @@ import { * */ export const GLOBAL_PUBLISH_EXPANDO_KEY = 'ng'; +// Typing for externally published global util functions +// Ideally we should be able to use `NgGlobalPublishUtils` using declaration merging but that doesn't work with API extractor yet. +// Have included the typings to have type safety when working with editors that support it (VSCode). +interface NgGlobalPublishUtils { + ɵgetLoadedRoutes(route: any): any; +} + const globalUtilsFunctions = { /** * Warning: functions that start with `ɵ` are considered *INTERNAL* and should not be relied upon @@ -71,7 +78,8 @@ const globalUtilsFunctions = { 'applyChanges': applyChanges, 'isSignal': isSignal, }; -type GlobalUtilsFunctions = keyof typeof globalUtilsFunctions; +type CoreGlobalUtilsFunctions = keyof typeof globalUtilsFunctions; +type ExternalGlobalUtilsFunctions = keyof NgGlobalPublishUtils; let _published = false; /** @@ -90,7 +98,7 @@ export function publishDefaultGlobalUtils() { } for (const [methodName, method] of Object.entries(globalUtilsFunctions)) { - publishGlobalUtil(methodName as GlobalUtilsFunctions, method); + publishGlobalUtil(methodName as CoreGlobalUtilsFunctions, method); } } } @@ -106,16 +114,31 @@ export type GlobalDevModeUtils = { * Publishes the given function to `window.ng` so that it can be * used from the browser console when an application is not in production. */ -export function publishGlobalUtil( +export function publishGlobalUtil( name: K, fn: (typeof globalUtilsFunctions)[K], ): void { + publishUtil(name, fn); +} + +/** + * Publishes the given function to `window.ng` from package other than @angular/core + * So that it can be used from the browser console when an application is not in production. + */ +export function publishExternalGlobalUtil( + name: K, + fn: NgGlobalPublishUtils[K], +): void { + publishUtil(name, fn); +} + +function publishUtil(name: string, fn: Function) { if (typeof COMPILED === 'undefined' || !COMPILED) { // Note: we can't export `ng` when using closure enhanced optimization as: // - closure declares globals itself for minified names, which sometimes clobber our `ng` global // - we can't declare a closure extern as the namespace `ng` is already used within Google // for typings for AngularJS (via `goog.provide('ng....')`). - const w = global as GlobalDevModeUtils; + const w = global; ngDevMode && assertDefined(fn, 'function not defined'); w[GLOBAL_PUBLISH_EXPANDO_KEY] ??= {} as any; diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index 1b1b3f0cad0e4a..11f42cb7d7b6fe 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -8,8 +8,9 @@ import {ChangeDetectorRef} from '../change_detection/change_detector_ref'; import {NotificationSource} from '../change_detection/scheduling/zoneless_scheduling'; +import type {ApplicationRef} from '../core'; import {RuntimeError, RuntimeErrorCode} from '../errors'; -import {EmbeddedViewRef, ViewRefTracker} from '../linker/view_ref'; +import {EmbeddedViewRef} from '../linker/view_ref'; import {removeFromArray} from '../util/array_utils'; import {assertEqual} from '../util/assert'; @@ -48,7 +49,7 @@ import { interface ChangeDetectorRefInterface extends ChangeDetectorRef {} export class ViewRef implements EmbeddedViewRef, ChangeDetectorRefInterface { - private _appRef: ViewRefTracker | null = null; + private _appRef: ApplicationRef | null = null; private _attachedToViewContainer = false; get rootNodes(): any[] { @@ -370,7 +371,7 @@ export class ViewRef implements EmbeddedViewRef, ChangeDetectorRefInterfac detachViewFromDOM(this._lView[TVIEW], this._lView); } - attachToAppRef(appRef: ViewRefTracker) { + attachToAppRef(appRef: ApplicationRef) { if (this._attachedToViewContainer) { throw new RuntimeError( RuntimeErrorCode.VIEW_ALREADY_ATTACHED, diff --git a/packages/core/test/acceptance/after_render_hook_spec.ts b/packages/core/test/acceptance/after_render_hook_spec.ts index e8a2ce8e337ce5..378cd061ed041e 100644 --- a/packages/core/test/acceptance/after_render_hook_spec.ts +++ b/packages/core/test/acceptance/after_render_hook_spec.ts @@ -1401,6 +1401,40 @@ describe('after render hooks', () => { appRef.tick(); }).toThrowError(/NG0103.*(Infinite change detection while refreshing application views)/); }); + + it('should destroy after the hook has run', () => { + let hookRef: AfterRenderRef | null = null; + let afterRenderCount = 0; + + @Component({selector: 'comp'}) + class Comp { + constructor() { + hookRef = afterNextRender(() => { + afterRenderCount++; + }); + } + } + + TestBed.configureTestingModule({ + declarations: [Comp], + ...COMMON_CONFIGURATION, + }); + createAndAttachComponent(Comp); + const appRef = TestBed.inject(ApplicationRef); + const destroySpy = spyOn(hookRef!, 'destroy').and.callThrough(); + expect(afterRenderCount).toBe(0); + expect(destroySpy).not.toHaveBeenCalled(); + + // Run once and ensure that it was called and then cleaned up. + appRef.tick(); + expect(afterRenderCount).toBe(1); + expect(destroySpy).toHaveBeenCalledTimes(1); + + // Make sure we're not retaining it. + appRef.tick(); + expect(afterRenderCount).toBe(1); + expect(destroySpy).toHaveBeenCalledTimes(1); + }); }); describe('server', () => { diff --git a/packages/core/test/acceptance/bootstrap_spec.ts b/packages/core/test/acceptance/bootstrap_spec.ts index e1f19e5fa231aa..a9e631680a1e70 100644 --- a/packages/core/test/acceptance/bootstrap_spec.ts +++ b/packages/core/test/acceptance/bootstrap_spec.ts @@ -92,7 +92,8 @@ describe('bootstrap', () => { ) { @Component({ selector: options.selector || 'my-app', - styles: [''], + // styles must be non-empty to trigger `ViewEncapsulation.Emulated` + styles: 'span {color:red}', template: 'a b', encapsulation: options.encapsulation, preserveWhitespaces: options.preserveWhitespaces, diff --git a/packages/core/test/acceptance/component_spec.ts b/packages/core/test/acceptance/component_spec.ts index 60d463e6e57433..a641369a2c190c 100644 --- a/packages/core/test/acceptance/component_spec.ts +++ b/packages/core/test/acceptance/component_spec.ts @@ -145,8 +145,8 @@ describe('component', () => { @Component({ selector: 'encapsulated', encapsulation: ViewEncapsulation.Emulated, - // styles array must contain a value (even empty) to trigger `ViewEncapsulation.Emulated` - styles: [``], + // styles must be non-empty to trigger `ViewEncapsulation.Emulated` + styles: `:host {display: block}`, template: `foo`, }) class EncapsulatedComponent {} @@ -182,9 +182,9 @@ describe('component', () => { }); it('should encapsulate host and children with different attributes', () => { - // styles array must contain a value (even empty) to trigger `ViewEncapsulation.Emulated` + // styles must be non-empty to trigger `ViewEncapsulation.Emulated` TestBed.overrideComponent(LeafComponent, { - set: {encapsulation: ViewEncapsulation.Emulated, styles: [``]}, + set: {encapsulation: ViewEncapsulation.Emulated, styles: [`span {color:red}`]}, }); const fixture = TestBed.createComponent(EncapsulatedComponent); fixture.detectChanges(); @@ -198,6 +198,28 @@ describe('component', () => { }="">bar`, ); }); + + it('should be off for a component with no styles', () => { + TestBed.overrideComponent(EncapsulatedComponent, { + set: {styles: undefined}, + }); + const fixture = TestBed.createComponent(EncapsulatedComponent); + fixture.detectChanges(); + const html = fixture.nativeElement.outerHTML; + expect(html).not.toContain(' { + TestBed.overrideComponent(EncapsulatedComponent, { + set: {styles: [` `, '', '/*comment*/']}, + }); + const fixture = TestBed.createComponent(EncapsulatedComponent); + fixture.detectChanges(); + const html = fixture.nativeElement.outerHTML; + expect(html).not.toContain(' { diff --git a/packages/core/test/acceptance/embedded_views_spec.ts b/packages/core/test/acceptance/embedded_views_spec.ts index a7ce9bd7731aa0..839ec7811a0b22 100644 --- a/packages/core/test/acceptance/embedded_views_spec.ts +++ b/packages/core/test/acceptance/embedded_views_spec.ts @@ -44,7 +44,7 @@ describe('embedded views', () => { }); it('should resolve template input variables through the implicit receiver', () => { - @Component({template: `{{this.a}}`}) + @Component({template: `{{a}}`}) class TestCmp {} TestBed.configureTestingModule({declarations: [TestCmp]});