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]});