From 1cb113cdec088068da4e3633911df531def895fc Mon Sep 17 00:00:00 2001 From: Avcharov Hryhorii Date: Mon, 25 Aug 2025 17:45:58 +0200 Subject: [PATCH 1/6] fix(devtools): prevent profiler bars flickering after change detection (#63350) Without trackBy cdkVirtualFor rerenders full list after in each update PR Close #63350 --- .../frame-selector/frame-selector.component.html | 2 +- .../frame-selector/frame-selector.component.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording-timeline/frame-selector/frame-selector.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording-timeline/frame-selector/frame-selector.component.html index e4db7b65bbb..af4dc255b81 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording-timeline/frame-selector/frame-selector.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording-timeline/frame-selector/frame-selector.component.html @@ -23,7 +23,7 @@ [class.drag-scrolling]="dragScrolling()" >
Date: Mon, 25 Aug 2025 18:30:52 +0200 Subject: [PATCH 2/6] refactor(devtools): provide signalGraphEnabled via settings service (#63374) signalGraphEnabled was previously passed down the component tree. This change refactors the logic to use the settings service instead, which already holds the value and allows sharing it across components. PR Close #63374 --- .../src/lib/application-providers/settings_provider.ts | 2 +- .../src/lib/devtools-tabs/devtools-tabs.component.html | 1 - .../directive-explorer/directive-explorer.component.html | 1 - .../directive-explorer/directive-explorer.component.ts | 2 -- .../property-tab/property-tab-header.component.ts | 8 ++++++-- .../property-tab/property-tab.component.html | 2 -- .../property-tab/property-tab.component.ts | 1 - .../property-tab/property-view/BUILD.bazel | 1 + .../property-view/property-tab-body.component.html | 1 - .../property-view/property-tab-body.component.ts | 1 - .../property-view/property-view-body.component.html | 1 - .../property-view/property-view-body.component.ts | 1 - .../property-view/property-view-tree.component.ts | 5 ++++- .../property-view/property-view.component.html | 1 - .../property-tab/property-view/property-view.component.ts | 1 - 15 files changed, 12 insertions(+), 17 deletions(-) diff --git a/devtools/projects/ng-devtools/src/lib/application-providers/settings_provider.ts b/devtools/projects/ng-devtools/src/lib/application-providers/settings_provider.ts index f54dc694f4a..1445a886e46 100644 --- a/devtools/projects/ng-devtools/src/lib/application-providers/settings_provider.ts +++ b/devtools/projects/ng-devtools/src/lib/application-providers/settings_provider.ts @@ -36,7 +36,7 @@ export function provideSettings(): (Provider | EnvironmentProviders)[] { } /** - * Migrrates the provided data to the latest data format, if needed. + * Migrates the provided data to the latest data format, if needed. * Returns a new object with the migrated data. * * @param data Non-migrated data diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html index 9e7e54132b6..bd62ef75487 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html @@ -67,7 +67,6 @@ [showCommentNodes]="showCommentNodes()" [isHydrationEnabled]="isHydrationEnabled()" (toggleInspector)="toggleInspector()" - [signalGraphEnabled]="signalGraphEnabled()" /> @let profilerVisible = activeTab() === 'Profiler'; diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.html index a9e2bdb3dbb..1a0d2a46b0a 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.html @@ -43,7 +43,6 @@ @if (currentSelectedElement(); as currentSelectedElement) { (); - readonly currentSelectedElement = signal(null); readonly forest = signal([]); readonly splitDirection = signal<'horizontal' | 'vertical'>('horizontal'); diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab-header.component.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab-header.component.ts index 78b1082fed8..e62575561f8 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab-header.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab-header.component.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.dev/license */ -import {ChangeDetectionStrategy, Component, input, output, signal} from '@angular/core'; +import {ChangeDetectionStrategy, Component, input, output, signal, inject} from '@angular/core'; import {MatExpansionModule} from '@angular/material/expansion'; import {MatIcon} from '@angular/material/icon'; import {IndexedNode} from '../directive-forest/index-forest'; import {ComponentMetadataComponent} from './component-metadata.component'; import {ButtonComponent} from '../../../shared/button/button.component'; +import {Settings} from '../../../application-services/settings'; @Component({ templateUrl: './property-tab-header.component.html', @@ -22,9 +23,12 @@ import {ButtonComponent} from '../../../shared/button/button.component'; imports: [MatExpansionModule, MatIcon, ComponentMetadataComponent, ButtonComponent], }) export class PropertyTabHeaderComponent { + private readonly settings = inject(Settings); + protected readonly currentSelectedElement = input.required(); - protected readonly signalGraphEnabled = input.required(); protected readonly showSignalGraph = output(); protected readonly expanded = signal(false); + + protected readonly signalGraphEnabled = this.settings.signalGraphEnabled; } diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab.component.html index be039b86e65..0f0a25ee626 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab.component.html @@ -3,7 +3,6 @@ @if (currentSelectedElement) { @let hydration = currentSelectedElement.hydration; diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab.component.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab.component.ts index fa4f7c7ffc7..911f08e5c2a 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-tab.component.ts @@ -24,7 +24,6 @@ import {DeferViewComponent} from './defer-view/defer-view.component'; }) export class PropertyTabComponent { readonly currentSelectedElement = input.required(); - readonly signalGraphEnabled = input.required(); readonly viewSource = output(); readonly inspect = output<{node: FlatNode; directivePosition: DirectivePosition}>(); diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/BUILD.bazel b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/BUILD.bazel index 00deb7ffa32..25184bc3a1a 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/BUILD.bazel +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/BUILD.bazel @@ -61,6 +61,7 @@ ng_project( "//devtools/projects/ng-devtools/src/lib/application-environment", "//devtools/projects/ng-devtools/src/lib/application-providers:supported_apis", "//devtools/projects/ng-devtools/src/lib/application-services:frame_manager", + "//devtools/projects/ng-devtools/src/lib/application-services:settings", "//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest", "//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver", "//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/resolution-path", diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-tab-body.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-tab-body.component.html index 58a6ee86ef5..3ab3efd95b0 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-tab-body.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-tab-body.component.html @@ -5,7 +5,6 @@ (viewSource)="viewSource.emit(directive.name)" (showSignalGraph)="showSignalGraph.emit($event)" [directive]="directive" - [signalGraphEnabled]="signalGraphEnabled()" />
} diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-tab-body.component.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-tab-body.component.ts index 445dc89f957..277622179fe 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-tab-body.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-tab-body.component.ts @@ -22,7 +22,6 @@ import {PropertyViewComponent} from './property-view.component'; }) export class PropertyTabBodyComponent { readonly currentSelectedElement = input.required(); - readonly signalGraphEnabled = input.required(); readonly inspect = output<{node: FlatNode; directivePosition: DirectivePosition}>(); readonly viewSource = output(); diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html index 16e9487e8a6..769921c1bd6 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html @@ -24,7 +24,6 @@ (); readonly directiveOutputControls = input.required(); readonly directiveStateControls = input.required(); - readonly signalGraphEnabled = input.required(); readonly inspect = output<{node: FlatNode; directivePosition: DirectivePosition}>(); readonly showSignalGraph = output(); diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-tree.component.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-tree.component.ts index 0b685781531..18166d703c3 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-tree.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-tree.component.ts @@ -20,6 +20,7 @@ import {MatTree, MatTreeNode, MatTreeNodeDef, MatTreeNodePadding} from '@angular import {SUPPORTED_APIS} from '../../../../application-providers/supported_apis'; import {SignalGraphManager} from '../../signal-graph/signal-graph-manager'; import {DebugSignalGraphNode} from '../../../../../../../protocol'; +import {Settings} from '../../../../application-services/settings'; @Component({ selector: 'ng-property-view-tree', @@ -41,14 +42,16 @@ import {DebugSignalGraphNode} from '../../../../../../../protocol'; export class PropertyViewTreeComponent { protected readonly supportedApis = inject(SUPPORTED_APIS); private readonly signalGraph = inject(SignalGraphManager); + private readonly settings = inject(Settings); readonly dataSource = input.required(); readonly treeControl = input.required>(); - readonly signalGraphEnabled = input.required(); readonly updateValue = output(); readonly inspect = output(); readonly showSignalGraph = output(); + protected readonly signalGraphEnabled = this.settings.signalGraphEnabled; + hasChild = (_: number, node: FlatNode): boolean => node.expandable; toggle(node: FlatNode): void { diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view.component.html index 3a6ea1a407f..0dada92df13 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view.component.html @@ -8,7 +8,6 @@ [directivePropControls]="directivePropControls()!" [directiveOutputControls]="directiveOutputControls()!" [directiveStateControls]="directiveStateControls()!" - [signalGraphEnabled]="signalGraphEnabled()" (inspect)="inspect.emit($event)" (showSignalGraph)="showSignalGraph.emit($event)" > diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view.component.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view.component.ts index 745fbfaba01..9d228d5ee3c 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view.component.ts @@ -22,7 +22,6 @@ import {PropertyViewHeaderComponent} from './property-view-header.component'; }) export class PropertyViewComponent { readonly directive = input.required<{name: string}>(); - readonly signalGraphEnabled = input.required(); readonly inspect = output<{node: FlatNode; directivePosition: DirectivePosition}>(); readonly viewSource = output(); From c15b8ed631d989e0d316d7b3c708ded662ab3fb6 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Mon, 25 Aug 2025 16:46:42 +0000 Subject: [PATCH 3/6] build: enable strict deps enforcement for ts_project (#63375) Enable strict_deps testings for all ts_project and ng_project targets in the repo PR Close #63375 --- MODULE.bazel | 2 +- .../pipeline/api-gen/rendering/BUILD.bazel | 3 +++ .../pipeline/api-gen/rendering/test/BUILD.bazel | 1 + adev/shared-docs/pipeline/guides/BUILD.bazel | 2 ++ .../pipeline/shared/marked/BUILD.bazel | 1 + .../pipeline/shared/marked/test/BUILD.bazel | 5 ++++- tools/bazel/ts_project_interop.bzl | 16 ++++++++-------- 7 files changed, 20 insertions(+), 10 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 0ab12b84fba..5360488d2e8 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -25,7 +25,7 @@ git_override( bazel_dep(name = "devinfra") git_override( module_name = "devinfra", - commit = "80db036355181684ed07021a175e9f039190d3d6", + commit = "00a4cf14cdc374867673a52ecb556f356da8bec0", remote = "https://github.com/angular/dev-infra.git", ) diff --git a/adev/shared-docs/pipeline/api-gen/rendering/BUILD.bazel b/adev/shared-docs/pipeline/api-gen/rendering/BUILD.bazel index 27e4986405c..36457fa2e53 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/BUILD.bazel +++ b/adev/shared-docs/pipeline/api-gen/rendering/BUILD.bazel @@ -27,9 +27,12 @@ ts_project( ), deps = [ ":entities", + "//adev:node_modules/marked", "//adev:node_modules/preact", "//adev:node_modules/preact-render-to-string", "//adev:node_modules/prettier", + "//adev:node_modules/shiki", + "//adev/shared-docs/pipeline/shared:linking", "//adev/shared-docs/pipeline/shared:shiki", "//adev/shared-docs/pipeline/shared/marked", "//adev/shared-docs/pipeline/shared/regions", diff --git a/adev/shared-docs/pipeline/api-gen/rendering/test/BUILD.bazel b/adev/shared-docs/pipeline/api-gen/rendering/test/BUILD.bazel index f592a260099..921012a3c59 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/test/BUILD.bazel +++ b/adev/shared-docs/pipeline/api-gen/rendering/test/BUILD.bazel @@ -21,6 +21,7 @@ ts_project( "//adev:node_modules/@types/jsdom", "//adev:node_modules/jsdom", "//adev/shared-docs/pipeline/api-gen/rendering:render_api_to_html_lib", + "//adev/shared-docs/pipeline/shared:shiki", ], ) diff --git a/adev/shared-docs/pipeline/guides/BUILD.bazel b/adev/shared-docs/pipeline/guides/BUILD.bazel index 00b6a32161c..70b405ded21 100644 --- a/adev/shared-docs/pipeline/guides/BUILD.bazel +++ b/adev/shared-docs/pipeline/guides/BUILD.bazel @@ -76,5 +76,7 @@ ts_project( deps = [ ":guides_lib", "//adev:node_modules/@types/node", + "//adev/shared-docs/pipeline/shared:shiki", + "//adev/shared-docs/pipeline/shared/marked", ], ) diff --git a/adev/shared-docs/pipeline/shared/marked/BUILD.bazel b/adev/shared-docs/pipeline/shared/marked/BUILD.bazel index 37ba04c7b52..796e9d11571 100644 --- a/adev/shared-docs/pipeline/shared/marked/BUILD.bazel +++ b/adev/shared-docs/pipeline/shared/marked/BUILD.bazel @@ -18,6 +18,7 @@ ts_project( "//adev:node_modules/marked", "//adev:node_modules/mermaid", "//adev:node_modules/playwright-core", + "//adev:node_modules/shiki", "//adev/shared-docs/pipeline/shared:linking", "//adev/shared-docs/pipeline/shared:shiki", "//adev/shared-docs/pipeline/shared/regions", diff --git a/adev/shared-docs/pipeline/shared/marked/test/BUILD.bazel b/adev/shared-docs/pipeline/shared/marked/test/BUILD.bazel index c117f1e707f..31e1091b227 100644 --- a/adev/shared-docs/pipeline/shared/marked/test/BUILD.bazel +++ b/adev/shared-docs/pipeline/shared/marked/test/BUILD.bazel @@ -4,5 +4,8 @@ ts_project( name = "renderer_context", srcs = ["renderer-context.mts"], visibility = ["//adev/shared-docs/pipeline/shared/marked/test:__subpackages__"], - deps = ["//adev/shared-docs/pipeline/shared/marked"], + deps = [ + "//adev/shared-docs/pipeline/shared:shiki", + "//adev/shared-docs/pipeline/shared/marked", + ], ) diff --git a/tools/bazel/ts_project_interop.bzl b/tools/bazel/ts_project_interop.bzl index 729e838fef7..f78e2490505 100644 --- a/tools/bazel/ts_project_interop.bzl +++ b/tools/bazel/ts_project_interop.bzl @@ -4,11 +4,10 @@ load("@rules_angular//src/ts_project:index.bzl", _ts_project = "ts_project") def ts_project( name, deps = [], + srcs = [], tsconfig = None, testonly = False, visibility = None, - # TODO: Enable this for all `ts_project` targets at end of migration. - ignore_strict_deps = True, rule_impl = _ts_project, **kwargs): rule_impl( @@ -17,13 +16,14 @@ def ts_project( declaration = True, tsconfig = tsconfig, visibility = visibility, + srcs = srcs, deps = deps, **kwargs ) - if not ignore_strict_deps: - strict_deps_test( - name = "%s_strict_deps_test" % name, - srcs = kwargs.get("srcs", []), - deps = deps, - ) + strict_deps_test( + name = "%s_strict_deps_test" % name, + srcs = srcs, + tsconfig = tsconfig, + deps = deps, + ) From a0388409e3d241193ab78920704dbef03b107c03 Mon Sep 17 00:00:00 2001 From: Jessica Janiuk Date: Tue, 26 Aug 2025 09:36:59 +0200 Subject: [PATCH 4/6] fix(compiler): fixes animations on elements with structural directives (#63390) The animate instructions were getting applied to the container comment nodes as well as the element nodes. This prevents that on the compiler level. fixes: #63371 PR Close #63390 --- .../animations/GOLDEN_PARTIAL.js | 51 +++++++++++++++++++ .../animations/TEST_CASES.json | 17 +++++++ ...animate_enter_with_structural_directive.ts | 21 ++++++++ ...nter_with_structural_directive_template.js | 13 +++++ .../src/render3/r3_template_transform.ts | 12 ++++- .../src/render3/instructions/animation.ts | 20 ++++++-- 6 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_structural_directive.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_structural_directive_template.js diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/GOLDEN_PARTIAL.js index 4d482625d8f..424982f3c14 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/GOLDEN_PARTIAL.js @@ -32,6 +32,57 @@ export declare class MyComponent { static ɵcmp: i0.ɵɵComponentDeclaration; } +/**************************************************************************************************** + * PARTIAL FILE: animate_enter_with_structural_directive.js + ****************************************************************************************************/ +import { Component, Directive } from '@angular/core'; +import * as i0 from "@angular/core"; +export class AnyStructuralDirective { +} +AnyStructuralDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: AnyStructuralDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); +AnyStructuralDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: AnyStructuralDirective, isStandalone: true, selector: "[any-structural-directive]", ngImport: i0 }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: AnyStructuralDirective, decorators: [{ + type: Directive, + args: [{ + selector: '[any-structural-directive]', + standalone: true, + }] + }] }); +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: ` +
+

Sliding Content

+
+ `, isInline: true, dependencies: [{ kind: "directive", type: AnyStructuralDirective, selector: "[any-structural-directive]" }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ + type: Component, + args: [{ + selector: 'my-component', + imports: [AnyStructuralDirective], + standalone: true, + template: ` +
+

Sliding Content

+
+ `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: animate_enter_with_structural_directive.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class AnyStructuralDirective { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; +} +export declare class MyComponent { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + /**************************************************************************************************** * PARTIAL FILE: animate_enter_with_string_host_bindings.js ****************************************************************************************************/ diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/TEST_CASES.json index 271db5ba2a4..0e0a16cddf1 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/TEST_CASES.json @@ -18,6 +18,23 @@ } ] }, + { + "description": "should generate animate enter instructions on element with a structural directive", + "inputFiles": [ + "animate_enter_with_structural_directive.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "animate_enter_with_structural_directive_template.js", + "generated": "animate_enter_with_structural_directive.js" + } + ], + "failureMessage": "Incorrect ɵɵanimateEnter() call" + } + ] + }, { "description": "should generate animate enter instructions with host binding and simple string", "inputFiles": [ diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_structural_directive.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_structural_directive.ts new file mode 100644 index 00000000000..0ea5d5293da --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_structural_directive.ts @@ -0,0 +1,21 @@ +import {Component, Directive} from '@angular/core'; + +@Directive({ + selector: '[any-structural-directive]', + standalone: true, +}) +export class AnyStructuralDirective {} + + +@Component({ + selector: 'my-component', + imports: [AnyStructuralDirective], + standalone: true, + template: ` +
+

Sliding Content

+
+ `, +}) +export class MyComponent { +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_structural_directive_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_structural_directive_template.js new file mode 100644 index 00000000000..eeb1bb65f7d --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_structural_directive_template.js @@ -0,0 +1,13 @@ +import * as i0 from "@angular/core"; +function MyComponent_p_1_Template(rf, ctx) { if (rf & 1) { + i0.ɵɵelementStart(0, "p"); + i0.ɵɵanimateEnter("slide"); + i0.ɵɵtext(1, "Sliding Content"); + i0.ɵɵelementEnd(); +} } +… +MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 2, vars: 0, consts: [[4, "any-structural-directive"]], template: function MyComponent_Template(rf, ctx) { if (rf & 1) { + i0.ɵɵelementStart(0, "div"); + i0.ɵɵtemplate(1, MyComponent_p_1_Template, 2, 0, "p", 0); + i0.ɵɵelementEnd(); +} }, dependencies: [AnyStructuralDirective], encapsulation: 2 }); diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index b311e15a22f..d740a75dca9 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -961,6 +961,14 @@ class HtmlAstToIvyAst implements html.Visitor { return directives; } + private filterAnimationAttributes(attributes: t.TextAttribute[]): t.TextAttribute[] { + return attributes.filter((a) => !a.name.startsWith('animate.')); + } + + private filterAnimationInputs(attributes: t.BoundAttribute[]): t.BoundAttribute[] { + return attributes.filter((a) => a.type !== BindingType.Animation); + } + private wrapInTemplate( node: t.Element | t.Component | t.Content | t.Template, templateProperties: ParsedProperty[], @@ -986,8 +994,8 @@ class HtmlAstToIvyAst implements html.Visitor { }; if (node instanceof t.Element || node instanceof t.Component) { - hoistedAttrs.attributes.push(...node.attributes); - hoistedAttrs.inputs.push(...node.inputs); + hoistedAttrs.attributes.push(...this.filterAnimationAttributes(node.attributes)); + hoistedAttrs.inputs.push(...this.filterAnimationInputs(node.inputs)); hoistedAttrs.outputs.push(...node.outputs); } diff --git a/packages/core/src/render3/instructions/animation.ts b/packages/core/src/render3/instructions/animation.ts index 79048883338..d530e74d873 100644 --- a/packages/core/src/render3/instructions/animation.ts +++ b/packages/core/src/render3/instructions/animation.ts @@ -166,6 +166,9 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn const tNode = getCurrentTNode()!; const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement; + + ngDevMode && assertElementNodes(nativeElement, 'animate.enter'); + const renderer = lView[RENDERER]; const ngZone = lView[INJECTOR]!.get(NgZone); @@ -270,6 +273,8 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa const tNode = getCurrentTNode()!; const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement; + ngDevMode && assertElementNodes(nativeElement, 'animate.enter'); + cancelLeavingNodes(tNode, lView); value.call(lView[CONTEXT], {target: nativeElement, animationComplete: noOpAnimationComplete}); @@ -306,6 +311,8 @@ export function ɵɵanimateLeave(value: string | Function): typeof ɵɵanimateLe const tNode = getCurrentTNode()!; const nativeElement = getNativeByTNode(tNode, lView) as Element; + ngDevMode && assertElementNodes(nativeElement, 'animate.leave'); + // This instruction is called in the update pass. const renderer = lView[RENDERER]; const elementRegistry = getAnimationElementRemovalRegistry(); @@ -374,9 +381,7 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa const tView = getTView(); const nativeElement = getNativeByTNode(tNode, lView) as Element; - if ((nativeElement as Node).nodeType !== Node.ELEMENT_NODE) { - return ɵɵanimateLeaveListener; - } + ngDevMode && assertElementNodes(nativeElement, 'animate.leave'); const elementRegistry = getAnimationElementRemovalRegistry(); ngDevMode && @@ -524,6 +529,15 @@ function assertAnimationTypes(value: string | Function, instruction: string) { } } +function assertElementNodes(nativeElement: Element, instruction: string) { + if ((nativeElement as Node).nodeType !== Node.ELEMENT_NODE) { + throw new RuntimeError( + RuntimeErrorCode.ANIMATE_INVALID_VALUE, + `'${instruction}' can only be used on an element node, got ${stringify((nativeElement as Node).nodeType)}`, + ); + } +} + /** * This function actually adds the classes that animate element that's leaving the DOM. * Once it finishes, it calls the remove function that was provided by the DOM renderer. From dfa2044af9cd3352776421d98d2eab65c39c8d88 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Tue, 26 Aug 2025 18:30:02 +0000 Subject: [PATCH 5/6] build: fix strict deps failure (#63403) Fix the remaining strict deps failure PR Close #63403 --- .../devtools-tabs/directive-explorer/property-tab/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/BUILD.bazel b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/BUILD.bazel index b321fa20958..2c307cfff8f 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/BUILD.bazel +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/BUILD.bazel @@ -41,6 +41,7 @@ ng_project( "//:node_modules/@angular/core", "//:node_modules/@angular/material", "//:node_modules/rxjs", + "//devtools/projects/ng-devtools/src/lib/application-services:settings", "//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest", "//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver", "//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/defer-view", From a43057c059f3f53fffb1a2247fb8af2272f56d32 Mon Sep 17 00:00:00 2001 From: Taygan Caldwell Date: Thu, 31 Jul 2025 14:03:56 -0700 Subject: [PATCH 6/6] refactor(core): Create a base effect interface and prototype to be used by both angular and wiz. (#62931) Add a common effect interface and prototype to be used to create the wiz and angular effects. PR Close #62931 --- .../core/primitives/signals/index.api.md | 20 ++++++ packages/core/primitives/signals/index.ts | 1 + .../core/primitives/signals/src/effect.ts | 60 ++++++++++++++++++ .../render3/reactivity/after_render_effect.ts | 4 +- .../core/src/render3/reactivity/effect.ts | 61 ++++++------------- 5 files changed, 102 insertions(+), 44 deletions(-) create mode 100644 packages/core/primitives/signals/src/effect.ts diff --git a/goldens/public-api/core/primitives/signals/index.api.md b/goldens/public-api/core/primitives/signals/index.api.md index 3797e3156d2..c5443f89748 100644 --- a/goldens/public-api/core/primitives/signals/index.api.md +++ b/goldens/public-api/core/primitives/signals/index.api.md @@ -4,6 +4,23 @@ ```ts +// @public (undocumented) +export const BASE_EFFECT_NODE: Omit; + +// @public (undocumented) +export interface BaseEffectNode extends ReactiveNode { + // (undocumented) + cleanup(): void; + // (undocumented) + destroy(): void; + // (undocumented) + fn: () => void; + // (undocumented) + hasRun: boolean; + // (undocumented) + run(): void; +} + // @public (undocumented) export type ComputationFn = (source: S, previous?: { source: S; @@ -134,6 +151,9 @@ export interface ReactiveNode { version: Version; } +// @public (undocumented) +export function runEffect(node: BaseEffectNode): void; + // @public (undocumented) export function runPostProducerCreatedFn(node: ReactiveNode): void; diff --git a/packages/core/primitives/signals/index.ts b/packages/core/primitives/signals/index.ts index bbf486238e2..4f9544b2c75 100644 --- a/packages/core/primitives/signals/index.ts +++ b/packages/core/primitives/signals/index.ts @@ -55,3 +55,4 @@ export { export {Watch, WatchCleanupFn, WatchCleanupRegisterFn, createWatch} from './src/watch'; export {setAlternateWeakRefImpl} from './src/weak_ref'; export {untracked} from './src/untracked'; +export {runEffect, BASE_EFFECT_NODE, BaseEffectNode} from './src/effect'; diff --git a/packages/core/primitives/signals/src/effect.ts b/packages/core/primitives/signals/src/effect.ts new file mode 100644 index 00000000000..acd9f484428 --- /dev/null +++ b/packages/core/primitives/signals/src/effect.ts @@ -0,0 +1,60 @@ +/** + * @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.dev/license + */ + +import { + consumerAfterComputation, + consumerBeforeComputation, + consumerPollProducersForChange, + REACTIVE_NODE, + ReactiveNode, +} from './graph'; + +/** + * An effect can, optionally, register a cleanup function. If registered, the cleanup is executed + * before the next effect run. The cleanup function makes it possible to "cancel" any work that the + * previous effect run might have started. + */ +export type EffectCleanupFn = () => void; + +/** + * A callback passed to the effect function that makes it possible to register cleanup logic. + */ +export type EffectCleanupRegisterFn = (cleanupFn: EffectCleanupFn) => void; + +export interface BaseEffectNode extends ReactiveNode { + hasRun: boolean; + fn: () => void; + destroy(): void; + cleanup(): void; + run(): void; +} + +export const BASE_EFFECT_NODE: Omit = + /* @__PURE__ */ (() => ({ + ...REACTIVE_NODE, + consumerIsAlwaysLive: true, + consumerAllowSignalWrites: true, + dirty: true, + hasRun: false, + kind: 'effect', + }))(); + +export function runEffect(node: BaseEffectNode) { + node.dirty = false; + if (node.hasRun && !consumerPollProducersForChange(node)) { + return; + } + node.hasRun = true; + const prevNode = consumerBeforeComputation(node); + try { + node.cleanup(); + node.fn(); + } finally { + consumerAfterComputation(node, prevNode); + } +} diff --git a/packages/core/src/render3/reactivity/after_render_effect.ts b/packages/core/src/render3/reactivity/after_render_effect.ts index 5c41d678eeb..11954d3423c 100644 --- a/packages/core/src/render3/reactivity/after_render_effect.ts +++ b/packages/core/src/render3/reactivity/after_render_effect.ts @@ -16,10 +16,8 @@ import { SIGNAL_NODE, type SignalNode, } from '../../../primitives/signals'; - -import {type Signal} from '../reactivity/api'; import {type EffectCleanupFn, type EffectCleanupRegisterFn} from './effect'; - +import {type Signal} from '../reactivity/api'; import {TracingService, TracingSnapshot} from '../../application/tracing'; import { ChangeDetectionScheduler, diff --git a/packages/core/src/render3/reactivity/effect.ts b/packages/core/src/render3/reactivity/effect.ts index 8d869be118d..8aa7eefb02e 100644 --- a/packages/core/src/render3/reactivity/effect.ts +++ b/packages/core/src/render3/reactivity/effect.ts @@ -7,15 +7,13 @@ */ import { - REACTIVE_NODE, - ReactiveNode, SIGNAL, - consumerAfterComputation, - consumerBeforeComputation, consumerDestroy, - consumerPollProducersForChange, isInNotificationPhase, setActiveConsumer, + BaseEffectNode, + BASE_EFFECT_NODE, + runEffect, } from '../../../primitives/signals'; import {FLAGS, LViewFlags, LView, EFFECTS} from '../interfaces/view'; import {markAncestorsForTraversal} from '../util/view_utils'; @@ -191,17 +189,12 @@ export function effect( return effectRef; } -export interface EffectNode extends ReactiveNode, SchedulableEffect { - hasRun: boolean; +export interface EffectNode extends BaseEffectNode, SchedulableEffect { cleanupFns: EffectCleanupFn[] | undefined; injector: Injector; notifier: ChangeDetectionScheduler; onDestroyFn: () => void; - fn: (cleanupFn: EffectCleanupRegisterFn) => void; - run(): void; - destroy(): void; - maybeCleanup(): void; } export interface ViewEffectNode extends EffectNode { @@ -212,47 +205,27 @@ export interface RootEffectNode extends EffectNode { scheduler: EffectScheduler; } -export const BASE_EFFECT_NODE: Omit = +export const EFFECT_NODE: Omit = /* @__PURE__ */ (() => ({ - ...REACTIVE_NODE, - consumerIsAlwaysLive: true, - consumerAllowSignalWrites: true, - dirty: true, - hasRun: false, + ...BASE_EFFECT_NODE, cleanupFns: undefined, zone: null, - kind: 'effect', onDestroyFn: noop, run(this: EffectNode): void { - this.dirty = false; - if (ngDevMode && isInNotificationPhase()) { throw new Error(`Schedulers cannot synchronously execute watches while scheduling.`); } - - if (this.hasRun && !consumerPollProducersForChange(this)) { - return; - } - this.hasRun = true; - - const registerCleanupFn: EffectCleanupRegisterFn = (cleanupFn) => - (this.cleanupFns ??= []).push(cleanupFn); - - const prevNode = consumerBeforeComputation(this); - // We clear `setIsRefreshingViews` so that `markForCheck()` within the body of an effect will // cause CD to reach the component in question. const prevRefreshingViews = setIsRefreshingViews(false); try { - this.maybeCleanup(); - this.fn(registerCleanupFn); + runEffect(this); } finally { setIsRefreshingViews(prevRefreshingViews); - consumerAfterComputation(this, prevNode); } }, - maybeCleanup(this: EffectNode): void { + cleanup(this: EffectNode): void { if (!this.cleanupFns?.length) { return; } @@ -273,7 +246,7 @@ export const BASE_EFFECT_NODE: Omit = /* @__PURE__ */ (() => ({ - ...BASE_EFFECT_NODE, + ...EFFECT_NODE, consumerMarkedDirty(this: RootEffectNode) { this.scheduler.schedule(this); this.notifier.notify(NotificationSource.RootEffect); @@ -281,14 +254,14 @@ export const ROOT_EFFECT_NODE: Omit = /* @__PURE__ */ (() => ({ - ...BASE_EFFECT_NODE, + ...EFFECT_NODE, consumerMarkedDirty(this: ViewEffectNode): void { this.view[FLAGS] |= LViewFlags.HasChildViewsToRefresh; markAncestorsForTraversal(this.view); @@ -297,7 +270,7 @@ export const VIEW_EFFECT_NODE: Omit void) { + return () => { + fn((cleanupFn) => (node.cleanupFns ??= []).push(cleanupFn)); + }; +}