diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts index 7fe9c8d574ad..fe8a092bcc20 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts @@ -11,7 +11,7 @@ import { ɵDeferBlockData as DeferBlockData, ɵHydratedNode as HydrationNode, } from '@angular/core'; -import {CurrentDeferBlock, HydrationStatus} from '../../../../protocol'; +import {RenderedDeferBlock, HydrationStatus} from '../../../../protocol'; import {ComponentTreeNode} from '../interfaces'; import {ngDebugClient} from '../ng-debug-api/ng-debug-api'; @@ -126,14 +126,19 @@ function groupDeferChildrenIfNeeded( getDirectiveMetadata?: FrameworkAgnosticGlobalUtils['getDirectiveMetadata'], ) { const currentDeferBlock = deferBlocks.currentBlock; - const isFirstDefferedChild = node === currentDeferBlock?.rootNodes[0]; - if (isFirstDefferedChild) { + const isFirstDeferredChild = node === currentDeferBlock?.rootNodes[0]; + // Handles the case where the @defer is still unresolved but doesn't + // have a placeholder, for instance, by which children we mark + // the position of the block normally. In this case, we use the host. + const isHostNode = node === currentDeferBlock?.hostNode; + + if (isFirstDeferredChild || isHostNode) { deferBlocks.advance(); - // When encountering the first child of a defer block - // We create a synthetic TreeNode reprensenting the defer block + // When encountering the first child of a defer block (or the host node), + // we create a synthetic TreeNode representing the defer block. const childrenTree: ComponentTreeNode[] = []; - currentDeferBlock.rootNodes.forEach((child) => { + for (const child of currentDeferBlock.rootNodes) { extractViewTree( child, childrenTree, @@ -143,7 +148,7 @@ function groupDeferChildrenIfNeeded( getDirectives, getDirectiveMetadata, ); - }); + } const deferBlockTreeNode = { children: childrenTree, @@ -155,7 +160,7 @@ function groupDeferChildrenIfNeeded( defer: { id: `deferId-${rootId}-${deferBlocks.currentIndex}`, state: currentDeferBlock.state, - currentBlock: currentBlock(currentDeferBlock), + renderedBlock: getRenderedBlock(currentDeferBlock), triggers: groupTriggers(currentDeferBlock.triggers), blocks: { hasErrorBlock: currentDeferBlock.hasErrorBlock, @@ -213,12 +218,16 @@ function groupTriggers(triggers: string[]) { return {defer, hydrate, prefetch}; } -function currentBlock(deferBlock: DeferBlockData): CurrentDeferBlock | null { +function getRenderedBlock(deferBlock: DeferBlockData): RenderedDeferBlock | null { if (['placeholder', 'loading', 'error'].includes(deferBlock.state)) { return deferBlock.state as 'placeholder' | 'loading' | 'error'; } + if (deferBlock.state === 'complete') { + return 'defer'; + } return null; } + export class RTreeStrategy { supports(): boolean { return (['getDirectiveMetadata', 'getComponent'] as const).every( @@ -253,7 +262,7 @@ class DeferBlocksIterator { this.currentIndex++; } - get currentBlock() { + get currentBlock(): DeferBlockData | undefined { return this.blocks[this.currentIndex]; } } diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.html index ff1a7157b2fc..ea358b723923 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.html @@ -29,18 +29,22 @@ @if (node().onPush) { - OnPush + OnPush } @let defer = node().defer; @if (!defer && (!hydration || hydration.status !== 'dehydrated')) { - == $ng0 + == $ng0 } - @if (defer && defer.currentBlock) { - (@{{ defer.currentBlock }}) + @if (defer) { + @if (defer.renderedBlock && defer.renderedBlock !== 'defer') { + (@{{ defer.renderedBlock }}) + } @else if (!defer.renderedBlock) { + (non-rendered) + } } @switch (hydration?.status) { diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.scss b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.scss index 4a6ae0191588..ae6ff1db2f56 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.scss +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.scss @@ -99,8 +99,7 @@ display: none; } - .console-reference, - .on-push { + .trait { color: var(--color-tree-node-console-ref); padding-left: 8px; font-style: italic; diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.spec.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.spec.ts index 544911630bdb..e251f766a201 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.spec.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/tree-node/tree-node.component.spec.ts @@ -97,9 +97,9 @@ describe('TreeNodeComponent', () => { }); await fixture.whenStable(); - onPush = fixture.debugElement.query(By.css('.on-push')); + onPush = fixture.debugElement.query(By.css('.trait')); - expect(onPush).toBeTruthy(); + expect(onPush.nativeElement.textContent).toEqual('OnPush'); }); it('should handle selection', async () => { diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/defer-view/defer-view.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/defer-view/defer-view.component.html index 28d83da4696d..747d05bfccfa 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/defer-view/defer-view.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/defer-view/defer-view.component.html @@ -1,30 +1,36 @@ -Current block +Rendered block
- @{{ defer().currentBlock ?? 'defer' }} -
- -@let triggers = defer().triggers; -@let blocks = defer().blocks; -Declared blocks -
- @if (blocks.placeholderBlock) { - - @placeholder{{ - blocks.placeholderBlock.minimumTime - ? `(minimum ${blocks.placeholderBlock.minimumTime} ms)` - : '' - }} - } - @if (blocks.loadingBlock) { - @loading{{ loadingBlockInfo() }} - } - @if (blocks.hasErrorBlock) { - @error + @if (defer().renderedBlock) { + @{{ defer().renderedBlock }} + } @else { + Nothing rendered yet }
+@if (hasDeclaredBlocks()) { + @let blocks = defer().blocks; + Declared blocks +
+ @if (blocks.placeholderBlock.exists) { + + @placeholder{{ + blocks.placeholderBlock.minimumTime + ? `(minimum ${blocks.placeholderBlock.minimumTime} ms)` + : '' + }} + } + @if (blocks.loadingBlock.exists) { + @loading{{ loadingBlockInfo() }} + } + @if (blocks.hasErrorBlock) { + @error + } +
+} + +@let triggers = defer().triggers; Triggers

Defer triggers

diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/defer-view/defer-view.component.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/defer-view/defer-view.component.ts index 15854389c9ff..7cf6408f83e7 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/defer-view/defer-view.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/defer-view/defer-view.component.ts @@ -22,7 +22,7 @@ export class DeferViewComponent { readonly loadingBlockInfo = computed(() => { const loadingBlock = this.defer().blocks.loadingBlock; - if (!loadingBlock) { + if (!loadingBlock.exists) { return null; } @@ -35,4 +35,9 @@ export class DeferViewComponent { } return info.length ? `(${info.join(', ')})` : null; }); + + readonly hasDeclaredBlocks = computed(() => { + const blocks = this.defer().blocks; + return blocks.hasErrorBlock || blocks.placeholderBlock.exists || blocks.loadingBlock.exists; + }); } diff --git a/devtools/projects/protocol/src/lib/messages.ts b/devtools/projects/protocol/src/lib/messages.ts index 9a5baa86b96a..4925c317116e 100644 --- a/devtools/projects/protocol/src/lib/messages.ts +++ b/devtools/projects/protocol/src/lib/messages.ts @@ -78,12 +78,12 @@ export type HydrationStatus = actualNodeDetails: string | null; }; -export type CurrentDeferBlock = 'placeholder' | 'loading' | 'error'; +export type RenderedDeferBlock = 'defer' | 'placeholder' | 'loading' | 'error'; export interface DeferInfo { id: string; state: 'placeholder' | 'loading' | 'complete' | 'error' | 'initial'; - currentBlock: CurrentDeferBlock | null; + renderedBlock: RenderedDeferBlock | null; triggers: { defer: string[]; hydrate: string[]; @@ -94,8 +94,8 @@ export interface DeferInfo { export interface BlockDetails { hasErrorBlock: boolean; - placeholderBlock: null | {minimumTime: number | null}; - loadingBlock: null | {minimumTime: number | null; afterTime: number | null}; + placeholderBlock: {exists: boolean; minimumTime: number | null}; + loadingBlock: {exists: boolean; minimumTime: number | null; afterTime: number | null}; } // TODO: refactor to remove nativeElement as it is not serializable diff --git a/packages/core/src/render3/util/defer.ts b/packages/core/src/render3/util/defer.ts index 30badf0fbe23..29c7cc77cad9 100644 --- a/packages/core/src/render3/util/defer.ts +++ b/packages/core/src/render3/util/defer.ts @@ -27,7 +27,7 @@ import {assertLView} from '../assert'; import {collectNativeNodes} from '../collect_native_nodes'; import {getLContext} from '../context_discovery'; import {CONTAINER_HEADER_OFFSET, NATIVE} from '../interfaces/container'; -import {INJECTOR, LView, TVIEW} from '../interfaces/view'; +import {HOST, INJECTOR, LView, TVIEW} from '../interfaces/view'; import {getNativeByTNode} from './view_utils'; /** Retrieved information about a `@defer` block. */ @@ -65,6 +65,9 @@ export interface DeferBlockData { /** Stringified version of the block's triggers. */ triggers: string[]; + /** The comment host/container node next to which all of the root nodes are rendered. */ + hostNode: Node; + /** Element root nodes that are currently being shown in the block. */ rootNodes: Node[]; } @@ -156,6 +159,7 @@ function findDeferBlocks(node: Node, lView: LView, results: DeferBlockData[]) { minimumTime: tDetails.placeholderBlockConfig?.[MINIMUM_SLOT] ?? null, }, triggers: tDetails.debug?.triggers ? Array.from(tDetails.debug.triggers).sort() : [], + hostNode: details.lContainer[HOST] as Node, rootNodes, }; diff --git a/packages/core/test/acceptance/defer_utils_spec.ts b/packages/core/test/acceptance/defer_utils_spec.ts index ed655ff1620c..0efd6b7891a8 100644 --- a/packages/core/test/acceptance/defer_utils_spec.ts +++ b/packages/core/test/acceptance/defer_utils_spec.ts @@ -301,6 +301,26 @@ describe('@defer debugging utilities', () => { await block.render(DeferBlockState.Complete); }); + it('should return the host comment node of the currently-rendered block', () => { + @Component({ + template: ` + @defer (when false) { + Loaded + } + `, + }) + class App {} + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const results = getDeferBlocks(fixture.nativeElement); + + expect(results.length).toBe(1); + expect(results[0].hostNode).toBeTruthy(); + expect(stringifyNodes([results[0].hostNode])).toEqual(['Comment(container)']); + }); + function stringifyNodes(nodes: Node[]): string[] { return nodes.map((node) => { switch (node.nodeType) {