diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8c74a72a9d242..0c5a10a41d538 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -95,7 +95,7 @@ jobs: - name: Run CI tests for framework run: yarn tsx ./scripts/build/build-packages-dist.mts - name: Archive build artifacts - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: pr-artifacts-${{ github.event.number }} path: dist/packages-dist/ diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index fdedc6c44a1d7..59309bf2e9d25 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -39,7 +39,7 @@ jobs: # Upload the results as artifacts. - name: 'Upload artifact' - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: SARIF file path: results.sarif @@ -47,6 +47,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 + uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 with: sarif_file: results.sarif diff --git a/CHANGELOG.md b/CHANGELOG.md index ae61edc6458d4..a5725b75d1f55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ + +# 19.0.0-next.3 (2024-09-04) +## Breaking Changes +### core +- * TypeScript versions less than 5.5 are no longer supported. +### compiler +| Commit | Type | Description | +| -- | -- | -- | +| [a2e4ee0cb3](https://github.com/angular/angular/commit/a2e4ee0cb3d40cadc05e28d58b06853973944456) | feat | add diagnostic for unused standalone imports ([#57605](https://github.com/angular/angular/pull/57605)) | +### core +| Commit | Type | Description | +| -- | -- | -- | +| [8bcc663a53](https://github.com/angular/angular/commit/8bcc663a53888717cdf4ce0c23404caa00abb1b2) | feat | drop support for TypeScript 5.4 ([#57577](https://github.com/angular/angular/pull/57577)) | +| [e6e5d29e83](https://github.com/angular/angular/commit/e6e5d29e830a0a74d7677d5f2345f29391064853) | feat | initial version of the output migration ([#57604](https://github.com/angular/angular/pull/57604)) | +| [be2e49639b](https://github.com/angular/angular/commit/be2e49639bda831831ad62d49253db942a83fd46) | feat | introduce `afterRenderEffect` ([#57549](https://github.com/angular/angular/pull/57549)) | +### elements +| Commit | Type | Description | +| -- | -- | -- | +| [fe5c4e086a](https://github.com/angular/angular/commit/fe5c4e086add655bf53315d71b0736ff758c7199) | fix | support `output()`-shaped outputs ([#57535](https://github.com/angular/angular/pull/57535)) | +### http +| Commit | Type | Description | +| -- | -- | -- | +| [c2892fee58](https://github.com/angular/angular/commit/c2892fee58d28ffec0dfeaad6a5d6822c040cf03) | fix | Dynamicaly call the global fetch implementation ([#57531](https://github.com/angular/angular/pull/57531)) | +### language-service +| Commit | Type | Description | +| -- | -- | -- | +| [8da9fb49b5](https://github.com/angular/angular/commit/8da9fb49b54e50de2d028691f73fb773def62ecd) | feat | add code fix for unused standalone imports ([#57605](https://github.com/angular/angular/pull/57605)) | +| [1f067f4507](https://github.com/angular/angular/commit/1f067f4507b6e908fe991d5de0dc4d3a627ab2f9) | feat | add code reactoring action to migrate `@Input` to signal-input ([#57214](https://github.com/angular/angular/pull/57214)) | +| [56ee47f2ec](https://github.com/angular/angular/commit/56ee47f2ec6e983e2ffdf59476ab29a92590811e) | feat | allow code refactorings to compute edits asynchronously ([#57214](https://github.com/angular/angular/pull/57214)) | + + + + +# 18.2.3 (2024-09-04) +### http +| Commit | Type | Description | +| -- | -- | -- | +| [de68e049e4](https://github.com/angular/angular/commit/de68e049e40ab702d9e2b7dd02070de9856377df) | fix | Dynamicaly call the global fetch implementation ([#57531](https://github.com/angular/angular/pull/57531)) | + + + # 19.0.0-next.2 (2024-08-28) ## Breaking Changes diff --git a/WORKSPACE b/WORKSPACE index ee4d0d50b6f04..2cd61da95dce2 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -143,10 +143,10 @@ cldr_xml_data_repository( # sass rules http_archive( name = "io_bazel_rules_sass", - sha256 = "cd83736ea65d0df064283aea5922dbaf132dd2b3aa54e7151aae7edaa9572c3e", - strip_prefix = "rules_sass-83022b98114c07e9588089c7fe8f76bc0262c7e7", + sha256 = "99c26eb38ef3e4e63253796dec37030834b1db25d12f1aeed1481f7020eb95b3", + strip_prefix = "rules_sass-5a7e3f4e1ed8def01b2a1e52625b09f11e98f1f8", urls = [ - "https://github.com/bazelbuild/rules_sass/archive/83022b98114c07e9588089c7fe8f76bc0262c7e7.zip", + "https://github.com/bazelbuild/rules_sass/archive/5a7e3f4e1ed8def01b2a1e52625b09f11e98f1f8.zip", ], ) diff --git a/adev/shared-docs/components/top-level-banner/top-level-banner.component.html b/adev/shared-docs/components/top-level-banner/top-level-banner.component.html index 68e00ae15a992..3659e9f97eb35 100644 --- a/adev/shared-docs/components/top-level-banner/top-level-banner.component.html +++ b/adev/shared-docs/components/top-level-banner/top-level-banner.component.html @@ -1,11 +1,13 @@ @if (!hasClosed()) { @if (link()) { -

{{ text() }}

+

{{ text() }}

+

{{ text() }}

} @else {
-

{{ text() }}

+

{{ text() }}

+

{{ text() }}

} diff --git a/adev/shared-docs/components/top-level-banner/top-level-banner.component.scss b/adev/shared-docs/components/top-level-banner/top-level-banner.component.scss index 922b1d8d2e53b..88657a0c82b7e 100644 --- a/adev/shared-docs/components/top-level-banner/top-level-banner.component.scss +++ b/adev/shared-docs/components/top-level-banner/top-level-banner.component.scss @@ -18,40 +18,42 @@ h1.docs-top-level-banner-cta { display: inline; - position: relative; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); font-size: 0.875rem; margin: 0; - background-image: var(--red-to-pink-to-purple-horizontal-gradient); - background-clip: text; - -webkit-background-clip: text; - color: transparent; width: fit-content; font-weight: 500; - &::after { - content: ''; - position: absolute; - width: 100%; - transform: scaleX(0); - height: 1px; - bottom: -2px; - left: 0; - background: var(--tertiary-contrast); - animation-name: shimmer; - -webkit-animation-duration: 5s; - -moz-animation-duration: 5s; - animation-duration: 5s; - -webkit-animation-iteration-count: infinite; - -moz-animation-iteration-count: infinite; - animation-iteration-count: infinite; + &.background { + color: var(--tertiary-contrast); } - } - &:hover { - h1.docs-top-level-banner-cta { + &:not(.background) { + color: transparent; + &::after { - transform: scaleX(1); - transform-origin: bottom left; + content: ''; + position: absolute; + width: 100%; + height: 1px; + bottom: -2px; + left: 0; + background: var(--tertiary-contrast); + transform: scaleX(0); + transform-origin: bottom right; + @media (prefers-reduced-motion: no-preference) { + transition: transform 0.3s ease; + } + } + + &:hover { + &::after { + transform: scaleX(1); + transform-origin: bottom left; + } } } } @@ -60,17 +62,73 @@ position: absolute; top: 0.25rem; right: 0.5rem; - color: var(--primary-contrast); + color: var(--tertiary-contrast); + } +} + +.shimmer { + background: var(--red-to-pink-to-purple-horizontal-gradient); + + @media (prefers-reduced-motion: no-preference) { + background-repeat: no-repeat; + -webkit-background-size: 125px 100%; + -moz-background-size: 125px 100%; + background-size: 125px 100%; + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + -webkit-animation-name: shimmer; + -moz-animation-name: shimmer; + animation-name: shimmer; + -webkit-animation-duration: 10s; + -moz-animation-duration: 10s; + animation-duration: 10s; + -webkit-animation-iteration-count: infinite; + -moz-animation-iteration-count: infinite; + animation-iteration-count: infinite; + } +} + +@-moz-keyframes shimmer { + 0% { + background-position: top left; + background-position-x: -150px; + } + 100% { + background-position: top right; + background-position-x: 500px; + } +} + +@-webkit-keyframes shimmer { + 0% { + background-position: top left; + background-position-x: -150px; + } + 100% { + background-position: top right; + background-position-x: 500px; + } +} + +@-o-keyframes shimmer { + 0% { + background-position: top left; + background-position-x: -150px; + } + 100% { + background-position: top right; + background-position-x: 500px; } } @keyframes shimmer { 0% { - transform: scaleX(0); - transform-origin: bottom right; + background-position: top left; + background-position-x: -150px; } 100% { - transform: scaleX(1); - transform-origin: bottom left; + background-position: top right; + background-position-x: 500px; } } diff --git a/adev/shared-docs/package.json b/adev/shared-docs/package.json index a8686dc6ac445..04c71fe3ca018 100644 --- a/adev/shared-docs/package.json +++ b/adev/shared-docs/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@webcontainer/api": "^1.1.8", - "diff": "~5.2.0", + "diff": "~6.0.0", "emoji-regex": "~10.4.0", "fast-glob": "~3.3.2", "fflate": "^0.8.2", diff --git a/adev/shared-docs/pipeline/api-gen/rendering/entities.ts b/adev/shared-docs/pipeline/api-gen/rendering/entities.ts index 7b7de01a5bb64..3f616713ebfd5 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/entities.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/entities.ts @@ -101,6 +101,8 @@ export interface ClassEntry extends DocEntry { isAbstract: boolean; members: MemberEntry[]; generics: GenericEntry[]; + extends?: string; + implements: string[]; } // From an API doc perspective, class and interfaces are identical. diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts index b77227963ffb7..e8dde55a86357 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts @@ -426,9 +426,28 @@ function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContents data.contents = `${firstLine}\n${data.contents}${lastLine}`; }; - if (isClassEntry(entry)) { - const abstractPrefix = entry.isAbstract ? 'abstract ' : ''; - appendFirstAndLastLines(codeTocData, `${abstractPrefix}class ${entry.name} {`, `}`); + if (isClassEntry(entry) || isInterfaceEntry(entry)) { + const generics = + entry.generics?.length > 0 + ? `<${entry.generics + .map((g) => (g.constraint ? `${g.name} extends ${g.constraint}` : g.name)) + .join(', ')}>` + : ''; + + const extendsStr = entry.extends ? ` extends ${entry.extends}` : ''; + // TODO: remove the ? when we distinguish Class & Decorator entries + const implementsStr = + entry.implements?.length > 0 ? ` implements ${entry.implements.join(' ,')}` : ''; + + const signature = `${entry.name}${generics}${extendsStr}${implementsStr}`; + if (isClassEntry(entry)) { + const abstractPrefix = entry.isAbstract ? 'abstract ' : ''; + appendFirstAndLastLines(codeTocData, `${abstractPrefix}class ${signature} {`, `}`); + } + + if (isInterfaceEntry(entry)) { + appendFirstAndLastLines(codeTocData, `interface ${signature} {`, `}`); + } } if (isEnumEntry(entry)) { diff --git a/adev/src/app/app.component.html b/adev/src/app/app.component.html index 119152d5901e9..5fb1d688a3f1e 100644 --- a/adev/src/app/app.component.html +++ b/adev/src/app/app.component.html @@ -1,6 +1,6 @@ @defer (when isBrowser) { - + } diff --git a/adev/src/app/sub-navigation-data.ts b/adev/src/app/sub-navigation-data.ts index c46b6e5da8b78..2f71db14394e6 100644 --- a/adev/src/app/sub-navigation-data.ts +++ b/adev/src/app/sub-navigation-data.ts @@ -1368,6 +1368,11 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'extended-diagnostics/NG8111', contentPath: 'reference/extended-diagnostics/NG8111', }, + { + label: 'NG8113: Unused Standalone Imports', + path: 'extended-diagnostics/NG8113', + contentPath: 'reference/extended-diagnostics/NG8113', + }, ], }, { diff --git a/adev/src/content/guide/components/content-projection.md b/adev/src/content/guide/components/content-projection.md index a91950897e12c..59895427bfb48 100644 --- a/adev/src/content/guide/components/content-projection.md +++ b/adev/src/content/guide/components/content-projection.md @@ -148,6 +148,38 @@ did not match a `select` attribute: If a component does not include an `` placeholder without a `select` attribute, any elements that don't match one of the component's placeholders do not render into the DOM. +## Fallback content + +Angular can show *fallback content* for a component's `` placeholder if that component doesn't have any matching child content. You can specify fallback content by adding child content to the `` element itself. + +```angular-html + +
+ Default Title +
+ Default Body +
+``` + +```angular-html + + + Hello + + +``` + +```angular-html + + +
+ Hello +
+ Default Body +
+
+``` + ## Aliasing content for projection Angular supports a special attribute, `ngProjectAs`, that allows you to specify a CSS selector on diff --git a/adev/src/content/guide/di/hierarchical-dependency-injection.md b/adev/src/content/guide/di/hierarchical-dependency-injection.md index 75465edfb4187..ac464ebe1f539 100644 --- a/adev/src/content/guide/di/hierarchical-dependency-injection.md +++ b/adev/src/content/guide/di/hierarchical-dependency-injection.md @@ -16,7 +16,7 @@ Angular has two injector hierarchies: | Injector hierarchies | Details | |:--- |:--- | -| `EnvironmentInjector` hierarchy | Configure an `ElementInjector` in this hierarchy using `@Injectable()` or `providers` array in `ApplicationConfig`. | +| `EnvironmentInjector` hierarchy | Configure an `EnvironmentInjector` in this hierarchy using `@Injectable()` or `providers` array in `ApplicationConfig`. | | `ElementInjector` hierarchy | Created implicitly at each DOM element. An `ElementInjector` is empty by default unless you configure it in the `providers` property on `@Directive()` or `@Component()`. | diff --git a/adev/src/content/reference/extended-diagnostics/NG8113.md b/adev/src/content/reference/extended-diagnostics/NG8113.md new file mode 100644 index 0000000000000..92a50a251cd8d --- /dev/null +++ b/adev/src/content/reference/extended-diagnostics/NG8113.md @@ -0,0 +1,48 @@ +# Unused Standalone Imports + +This diagnostic detects cases where the `imports` array of a `@Component` contains symbols that +aren't used within the template. + + + +@Component({ + imports: [UsedDirective, UnusedPipe] +}) +class AwesomeCheckbox {} + + + +## What's wrong with that? + +The unused imports add unnecessary noise to your code and can increase your compilation time. + +## What should I do instead? + +Delete the unused import. + + + +@Component({ + imports: [UsedDirective] +}) +class AwesomeCheckbox {} + + + +## What if I can't avoid this? + +This diagnostic can be disabled by editing the project's `tsconfig.json` file: + + +{ + "angularCompilerOptions": { + "extendedDiagnostics": { + "checks": { + "unusedStandaloneImports": "suppress" + } + } + } +} + + +See [extended diagnostic configuration](extended-diagnostics#configuration) for more info. diff --git a/adev/src/content/reference/extended-diagnostics/overview.md b/adev/src/content/reference/extended-diagnostics/overview.md index d64df7a3614b9..94714804e9d96 100644 --- a/adev/src/content/reference/extended-diagnostics/overview.md +++ b/adev/src/content/reference/extended-diagnostics/overview.md @@ -20,6 +20,7 @@ Currently, Angular supports the following extended diagnostics: | `NG8108` | [`skipHydrationNotStatic`](extended-diagnostics/NG8108) | | `NG8109` | [`interpolatedSignalNotInvoked`](extended-diagnostics/NG8109) | | `NG8111` | [`uninvokedFunctionInEventBinding`](extended-diagnostics/NG8111) | +| `NG8113` | [`unusedStandaloneImports`](extended-diagnostics/NG8113) | ## Configuration diff --git a/adev/src/content/tutorials/learn-angular/steps/8-input/README.md b/adev/src/content/tutorials/learn-angular/steps/8-input/README.md index 2b11ce81980b8..8ff5431a9aab9 100644 --- a/adev/src/content/tutorials/learn-angular/steps/8-input/README.md +++ b/adev/src/content/tutorials/learn-angular/steps/8-input/README.md @@ -31,7 +31,7 @@ Make sure you bind the property `occupation` in your `UserComponent`. @Component({ ... - template: `

The user's name is {{occupation}}

` + template: `

The user's occupation is {{occupation}}

` })
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 da5a29ac089ec..071adb53754fb 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 @@ -1,4 +1,4 @@ - + @@ -9,18 +9,18 @@ (highlightComponent)="highlightComponent($event)" (removeComponentHighlight)="removeComponentHighlight()" (toggleInspector)="toggleInspector.emit()" - [forest]="forest" - [currentSelectedElement]="currentSelectedElement" - [showCommentNodes]="showCommentNodes" + [forest]="forest()" + [currentSelectedElement]="currentSelectedElement()" + [showCommentNodes]="showCommentNodes()" /> - @if (parents) { + @if (parents()) { } @@ -29,7 +29,7 @@
@@ -38,7 +38,7 @@
Show hydration overlays
diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.ts index 6031f8f23be0f..03fe8fe485ec5 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.ts @@ -7,17 +7,17 @@ */ import { - ChangeDetectorRef, + afterNextRender, Component, ElementRef, - EventEmitter, inject, Input, - NgZone, + input, OnDestroy, - OnInit, - Output, + output, + signal, ViewChild, + viewChild, } from '@angular/core'; import { ComponentExplorerView, @@ -86,57 +86,50 @@ const sameDirectives = (a: IndexedNode, b: IndexedNode) => { FormsModule, ], }) -export class DirectiveExplorerComponent implements OnInit, OnDestroy { - @Input() showCommentNodes = false; +export class DirectiveExplorerComponent implements OnDestroy { + readonly showCommentNodes = input(false); @Input() isHydrationEnabled = false; - @Output() toggleInspector = new EventEmitter(); + readonly toggleInspector = output(); - @ViewChild(DirectiveForestComponent) directiveForest!: DirectiveForestComponent; - @ViewChild(BreadcrumbsComponent) breadcrumbs!: BreadcrumbsComponent; + readonly directiveForest = viewChild(DirectiveForestComponent); @ViewChild(SplitComponent, {static: true, read: ElementRef}) splitElementRef!: ElementRef; @ViewChild('directiveForestSplitArea', {static: true, read: ElementRef}) directiveForestSplitArea!: ElementRef; - currentSelectedElement: IndexedNode | null = null; - forest!: DevToolsNode[]; - splitDirection: 'horizontal' | 'vertical' = 'horizontal'; - parents: FlatNode[] | null = null; - showHydrationNodeHighlights: boolean = false; + readonly currentSelectedElement = signal(null); + readonly forest = signal([]); + readonly splitDirection = signal<'horizontal' | 'vertical'>('horizontal'); + readonly parents = signal(null); + readonly showHydrationNodeHighlights = signal(false); - private _resizeObserver = new ResizeObserver((entries) => - this._ngZone.run(() => { - this.refreshHydrationNodeHighlightsIfNeeded(); - - const resizedEntry = entries[0]; - if (resizedEntry.target === this.splitElementRef.nativeElement) { - this.splitDirection = resizedEntry.contentRect.width <= 500 ? 'vertical' : 'horizontal'; - } - - if (!this.breadcrumbs) { - return; - } - - this.breadcrumbs.updateScrollButtonVisibility(); - }), - ); + private _resizeObserver!: ResizeObserver; private _clickedElement: IndexedNode | null = null; private _refreshRetryTimeout: null | ReturnType = null; - constructor( - private readonly _appOperations: ApplicationOperations, - private readonly _messageBus: MessageBus, - private readonly _propResolver: ElementPropertyResolver, - private readonly _cdr: ChangeDetectorRef, - private readonly _ngZone: NgZone, - private readonly _frameManager: FrameManager, - ) {} - - ngOnInit(): void { - this.subscribeToBackendEvents(); - this.refresh(); - this._resizeObserver.observe(this.splitElementRef.nativeElement); - this._resizeObserver.observe(this.directiveForestSplitArea.nativeElement); + private readonly _appOperations = inject(ApplicationOperations); + private readonly _messageBus = inject>(MessageBus); + private readonly _propResolver = inject(ElementPropertyResolver); + private readonly _frameManager = inject(FrameManager); + + constructor() { + afterNextRender(() => { + this._resizeObserver = new ResizeObserver((entries) => { + this.refreshHydrationNodeHighlightsIfNeeded(); + + const resizedEntry = entries[0]; + if (resizedEntry.target === this.splitElementRef.nativeElement) { + this.splitDirection.set( + resizedEntry.contentRect.width <= 500 ? 'vertical' : 'horizontal', + ); + } + }); + + this.subscribeToBackendEvents(); + this.refresh(); + this._resizeObserver.observe(this.splitElementRef.nativeElement); + this._resizeObserver.observe(this.directiveForestSplitArea.nativeElement); + }); } ngOnDestroy(): void { @@ -157,16 +150,17 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy { this._messageBus.emit('setSelectedComponent', [node.position]); this.refresh(); } else { - this._clickedElement = this.currentSelectedElement = null; + this._clickedElement = null; + this.currentSelectedElement.set(null); } } subscribeToBackendEvents(): void { this._messageBus.on('latestComponentExplorerView', (view: ComponentExplorerView) => { - this.forest = view.forest; - this.currentSelectedElement = this._clickedElement; - if (view.properties && this.currentSelectedElement) { - this._propResolver.setProperties(this.currentSelectedElement, view.properties); + this.forest.set(view.forest); + this.currentSelectedElement.set(this._clickedElement); + if (view.properties && this._clickedElement) { + this._propResolver.setProperties(this._clickedElement, view.properties); } }); @@ -192,12 +186,10 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy { viewSource(directiveName: string): void { // find the index of the directive with directiveName in this.currentSelectedElement.directives + const selectedEl = this.currentSelectedElement(); + if (!selectedEl) return; - if (!this.currentSelectedElement) { - return; - } - - const directiveIndex = this.currentSelectedElement.directives.findIndex( + const directiveIndex = selectedEl.directives.findIndex( (directive) => directive.name === directiveName, ); @@ -213,7 +205,7 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy { } this._appOperations.viewSource( - this.currentSelectedElement.position, + selectedEl.position, directiveIndex !== -1 ? directiveIndex : undefined, new URL(selectedFrame!.url), ); @@ -262,8 +254,8 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy { // set of properties which are already expanded. if ( !this._clickedElement || - !this.currentSelectedElement || - !sameDirectives(this._clickedElement, this.currentSelectedElement) + !this.currentSelectedElement() || + !sameDirectives(this._clickedElement, this.currentSelectedElement()!) ) { return { type: PropertyQueryTypes.All, @@ -284,12 +276,11 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy { } handleSelect(node: FlatNode): void { - this.directiveForest.handleSelect(node); + this.directiveForest()?.handleSelect(node); } handleSetParents(parents: FlatNode[] | null): void { - this.parents = parents; - this._cdr.detectChanges(); + this.parents.set(parents); } inspect({ @@ -324,7 +315,7 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy { } refreshHydrationNodeHighlightsIfNeeded() { - if (this.showHydrationNodeHighlights) { + if (this.showHydrationNodeHighlights()) { this.removeHydrationNodesHightlights(); this.hightlightHydrationNodes(); } diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.spec.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.spec.ts index 9fbda282341c2..69ed06b125197 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.spec.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.spec.ts @@ -19,7 +19,7 @@ import SpyObj = jasmine.SpyObj; import {By} from '@angular/platform-browser'; import {FrameManager} from '../../frame_manager'; import {TabUpdate} from '../tab-update'; -import {Component, EventEmitter, Input, Output, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {Component, CUSTOM_ELEMENTS_SCHEMA, output, input} from '@angular/core'; import {ElementPropertyResolver, FlatNode} from './property-resolver/element-property-resolver'; import {BreadcrumbsComponent} from './directive-forest/breadcrumbs/breadcrumbs.component'; import {PropertyTabComponent} from './property-tab/property-tab.component'; @@ -30,15 +30,15 @@ import {PropertyTabComponent} from './property-tab/property-tab.component'; standalone: true, }) class MockDirectiveForestComponent { - @Input() forest: IndexedNode[] = []; - @Input() currentSelectedElement: IndexedNode | null = null; - @Input() showCommentNodes = false; - @Output() selectNode = new EventEmitter(); - @Output() selectDomElement = new EventEmitter(); - @Output() setParents = new EventEmitter(); - @Output() highlightComponent = new EventEmitter(); - @Output() removeComponentHighlight = new EventEmitter(); - @Output() toggleInspector = new EventEmitter(); + readonly forest = input([]); + readonly currentSelectedElement = input(null); + readonly showCommentNodes = input(false); + readonly selectNode = output(); + readonly selectDomElement = output(); + readonly setParents = output(); + readonly highlightComponent = output(); + readonly removeComponentHighlight = output(); + readonly toggleInspector = output(); } @Component({ @@ -47,10 +47,10 @@ class MockDirectiveForestComponent { standalone: true, }) class MockBreadcrumbsComponent { - @Input() parents: IndexedNode[] = []; - @Output() handleSelect = new EventEmitter(); - @Output() mouseLeaveNode = new EventEmitter(); - @Output() mouseOverNode = new EventEmitter(); + readonly parents = input([]); + readonly handleSelect = output(); + readonly mouseLeaveNode = output(); + readonly mouseOverNode = output(); } @Component({ @@ -59,9 +59,9 @@ class MockBreadcrumbsComponent { standalone: true, }) class MockPropertyTabComponent { - @Input() currentSelectedElement: IndexedNode | null = null; - @Output() inspect = new EventEmitter<{node: FlatNode; directivePosition: DirectivePosition}>(); - @Output() viewSource = new EventEmitter(); + readonly currentSelectedElement = input(null); + readonly inspect = output<{node: FlatNode; directivePosition: DirectivePosition}>(); + readonly viewSource = output(); } describe('DirectiveExplorerComponent', () => { @@ -157,9 +157,9 @@ describe('DirectiveExplorerComponent', () => { ]); currentSelectedElement.position = [0]; currentSelectedElement.children = []; - comp.currentSelectedElement = currentSelectedElement; + comp.currentSelectedElement.set(currentSelectedElement); comp.refresh(); - expect(comp.currentSelectedElement).toBeTruthy(); + expect(comp.currentSelectedElement()).toBeTruthy(); expect(messageBusMock.emit).toHaveBeenCalledWith('getLatestComponentExplorerView', [ undefined, ]); @@ -199,12 +199,12 @@ describe('DirectiveExplorerComponent', () => { }); it('should show hydration slide toggle', () => { - comp.isHydrationEnabled = true; + fixture.componentRef.setInput('isHydrationEnabled', true); fixture.detectChanges(); const toggle = fixture.debugElement.query(By.css('mat-slide-toggle')); expect(toggle).toBeTruthy(); - comp.isHydrationEnabled = false; + fixture.componentRef.setInput('isHydrationEnabled', false); fixture.detectChanges(); const toggle2 = fixture.debugElement.query(By.css('mat-slide-toggle')); expect(toggle2).toBeFalsy(); @@ -215,11 +215,11 @@ describe('DirectiveExplorerComponent', () => { describe('view source', () => { it('should not call application operations view source if no frames are detected', () => { const directiveName = 'test'; - comp.currentSelectedElement = { + comp.currentSelectedElement.set({ directives: [{name: directiveName}], position: [0], children: [] as IndexedNode[], - } as IndexedNode; + } as IndexedNode); comp.viewSource(directiveName); expect(applicationOperationsSpy.viewSource).toHaveBeenCalledTimes(0); }); @@ -229,11 +229,11 @@ describe('DirectiveExplorerComponent', () => { contentScriptConnected(1, 'test2', 'http://localhost:4200/url'); const directiveName = 'test'; - comp.currentSelectedElement = { + comp.currentSelectedElement.set({ directives: [{name: directiveName}], position: [0], children: [] as IndexedNode[], - } as IndexedNode; + } as IndexedNode); comp.viewSource(directiveName); @@ -252,11 +252,11 @@ describe('DirectiveExplorerComponent', () => { contentScriptConnected(1, 'test2', 'http://localhost:4200/url2'); const directiveName = 'test'; - comp.currentSelectedElement = { + comp.currentSelectedElement.set({ directives: [{name: directiveName}], position: [0], children: [] as IndexedNode[], - } as IndexedNode; + } as IndexedNode); comp.viewSource(directiveName); diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/breadcrumbs/breadcrumbs.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/breadcrumbs/breadcrumbs.component.html index e0ca0503ffb6f..9888a1425f645 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/breadcrumbs/breadcrumbs.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/breadcrumbs/breadcrumbs.component.html @@ -1,9 +1,9 @@ - - @if ( - treeControl.isExpanded(node) && - node.hydration?.status === 'mismatched' && + treeControl.isExpanded(node) && + node.hydration?.status === 'mismatched' && (node.hydration.expectedNodeDetails || node.hydration.actualNodeDetails) ) {
diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.ts index 81cd5a6c69765..fe41f660fe976 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.ts @@ -14,14 +14,16 @@ import { import {FlatTreeControl} from '@angular/cdk/tree'; import { ChangeDetectionStrategy, - ChangeDetectorRef, Component, + computed, + effect, ElementRef, - EventEmitter, HostListener, - Input, - Output, - ViewChild, + inject, + input, + output, + signal, + viewChild, } from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {DevToolsNode, ElementPosition, Events, MessageBus} from 'protocol'; @@ -51,31 +53,19 @@ import {MatTooltip} from '@angular/material/tooltip'; ], }) export class DirectiveForestComponent { - @Input() - set forest(forest: DevToolsNode[]) { - this._latestForest = forest; - const result = this._updateForest(forest); - const changed = - result.movedItems.length || result.newItems.length || result.removedItems.length; - if (this.currentSelectedElement && changed) { - this._reselectNodeOnUpdate(); - } - } - @Input({required: true}) currentSelectedElement!: IndexedNode; - @Input() - set showCommentNodes(show: boolean) { - this._showCommentNodes = show; - this.forest = this._latestForest; - } + readonly forest = input([]); + readonly showCommentNodes = input(false); + readonly currentSelectedElement = input.required(); - @Output() selectNode = new EventEmitter(); - @Output() selectDomElement = new EventEmitter(); - @Output() setParents = new EventEmitter(); - @Output() highlightComponent = new EventEmitter(); - @Output() removeComponentHighlight = new EventEmitter(); - @Output() toggleInspector = new EventEmitter(); + readonly selectNode = output(); + readonly selectDomElement = output(); + readonly setParents = output(); + readonly highlightComponent = output(); + readonly removeComponentHighlight = output(); + readonly toggleInspector = output(); - @ViewChild(CdkVirtualScrollViewport) viewport!: CdkVirtualScrollViewport; + readonly viewport = viewChild.required(CdkVirtualScrollViewport); + private readonly updateForestResult = computed(() => this._updateForest(this.forest())); filterRegex = new RegExp('.^'); currentlyMatchedIndex = -1; @@ -83,14 +73,7 @@ export class DirectiveForestComponent { selectedNode: FlatNode | null = null; parents!: FlatNode[]; - private _highlightIDinTreeFromElement: number | null = null; - private _showCommentNodes = false; - private _latestForest!: DevToolsNode[]; - - set highlightIDinTreeFromElement(id: number | null) { - this._highlightIDinTreeFromElement = id; - this._cdr.markForCheck(); - } + private readonly highlightIDinTreeFromElement = signal(null); readonly treeControl = new FlatTreeControl( (node) => node!.level, @@ -102,28 +85,37 @@ export class DirectiveForestComponent { private _initialized = false; private resizeObserver: ResizeObserver; - constructor( - private _tabUpdate: TabUpdate, - private _messageBus: MessageBus, - private _cdr: ChangeDetectorRef, - private elementRef: ElementRef, - ) { + private _tabUpdate = inject(TabUpdate); + private _messageBus = inject>(MessageBus); + private elementRef = inject(ElementRef); + + constructor() { this.subscribeToInspectorEvents(); this._tabUpdate.tabUpdate$.pipe(takeUntilDestroyed()).subscribe(() => { - if (this.viewport) { + if (this.viewport()) { setTimeout(() => { - this.viewport.scrollToIndex(0); - this.viewport.checkViewportSize(); + const viewport = this.viewport(); + viewport.scrollToIndex(0); + viewport.checkViewportSize(); }); } }); // In some cases there a height changes, we need to recalculate the viewport size. this.resizeObserver = new ResizeObserver(() => { - this.viewport.scrollToIndex(0); - this.viewport.checkViewportSize(); + this.viewport().scrollToIndex(0); + this.viewport().checkViewportSize(); }); this.resizeObserver.observe(this.elementRef.nativeElement); + + effect(() => { + const result = this.updateForestResult(); + const changed = + result.movedItems.length || result.newItems.length || result.removedItems.length; + if (this.currentSelectedElement() && changed) { + this._reselectNodeOnUpdate(); + } + }); } ngOnDestroy(): void { @@ -138,11 +130,11 @@ export class DirectiveForestComponent { }); this._messageBus.on('highlightComponent', (id: number) => { - this.highlightIDinTreeFromElement = id; + this.highlightIDinTreeFromElement.set(id); }); this._messageBus.on('removeComponentHighlight', () => { - this.highlightIDinTreeFromElement = null; + this.highlightIDinTreeFromElement.set(null); }); } @@ -167,7 +159,7 @@ export class DirectiveForestComponent { selectAndEnsureVisible(node: FlatNode): void { this.select(node); - const scrollParent = this.viewport.elementRef.nativeElement; + const scrollParent = this.viewport().elementRef.nativeElement; // The top most point we see an element const top = scrollParent.scrollTop; // That's the bottom most point we currently see an element. @@ -201,7 +193,7 @@ export class DirectiveForestComponent { private _reselectNodeOnUpdate(): void { const nodeThatStillExists = this.dataSource.getFlatNodeFromIndexedNode( - this.currentSelectedElement, + this.currentSelectedElement(), ); if (nodeThatStillExists) { this.select(nodeThatStillExists); @@ -215,7 +207,7 @@ export class DirectiveForestComponent { movedItems: FlatNode[]; removedItems: FlatNode[]; } { - const result = this.dataSource.update(forest, this._showCommentNodes); + const result = this.dataSource.update(forest, this.showCommentNodes()); if (!this._initialized && forest && forest.length) { this.treeControl.expandAll(); this._initialized = true; @@ -399,7 +391,7 @@ export class DirectiveForestComponent { } highlightNode(position: ElementPosition): void { - this._highlightIDinTreeFromElement = null; + this.highlightIDinTreeFromElement.set(null); this.highlightComponent.emit(position); } @@ -409,8 +401,8 @@ export class DirectiveForestComponent { isHighlighted(node: FlatNode): boolean { return ( - !!this._highlightIDinTreeFromElement && - this._highlightIDinTreeFromElement === node.original.component?.id + !!this.highlightIDinTreeFromElement() && + this.highlightIDinTreeFromElement() === node.original.component?.id ); } diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/filter/filter.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/filter/filter.component.html index 50585d253d2d5..974bebbd645b7 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/filter/filter.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/filter/filter.component.html @@ -12,7 +12,7 @@ placeholder="Search components" /> - @if (hasMatched) { + @if (hasMatched()) {