From a40e2cebc878965c3e21bfb61658f3f80cbd2ebf Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Mon, 27 Apr 2026 23:52:04 +0200 Subject: [PATCH] fix(core): fix ordering of view queries metadata in JIT mode AOT was generating an array that was ordered as signal queries first, then the decorator queries. Aligning JIT with AOT fixes the issue illustrated by the test. fixes #68404 (cherry picked from commit 8c11816490074f9d7dbde2fb854d8225b775a9cb) --- packages/core/src/render3/jit/directive.ts | 16 ++++++-- .../authoring/signal_queries_spec.ts | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 6793ccd3fd6..f6e0669e376 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -28,7 +28,7 @@ import {ViewEncapsulation} from '../../metadata/view'; import {flatten} from '../../util/array_utils'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../../util/empty'; import {initNgDevMode} from '../../util/ng_dev_mode'; -import {getComponentDef, getDirectiveDef, getNgModuleDef, getPipeDef} from '../def_getters'; +import {getComponentDef, getDirectiveDef, getPipeDef} from '../def_getters'; import {depsTracker} from '../deps_tracker/deps_tracker'; import {NG_COMP_DEF, NG_DIR_DEF, NG_FACTORY_DEF} from '../fields'; import {ComponentDef, ComponentType, DirectiveDefList, PipeDefList} from '../interfaces/definition'; @@ -442,7 +442,8 @@ function extractQueriesMetadata( propMetadata: {[key: string]: any[]}, isQueryAnn: (ann: any) => ann is Query, ): R3QueryMetadataFacade[] { - const queriesMeta: R3QueryMetadataFacade[] = []; + const signalQueriesMeta: R3QueryMetadataFacade[] = []; + const decoratorQueriesMeta: R3QueryMetadataFacade[] = []; for (const field in propMetadata) { if (propMetadata.hasOwnProperty(field)) { const annotations = propMetadata[field]; @@ -457,12 +458,19 @@ function extractQueriesMetadata( if (annotations.some(isInputAnnotation)) { throw new Error(`Cannot combine @Input decorators with query decorators`); } - queriesMeta.push(convertToR3QueryMetadata(field, ann)); + const queryMeta = convertToR3QueryMetadata(field, ann); + if (queryMeta.isSignal) { + signalQueriesMeta.push(queryMeta); + } else { + decoratorQueriesMeta.push(queryMeta); + } } }); } } - return queriesMeta; + + // We match the behavior of AOT here by having signal queries first, then the decorator queries. + return [...signalQueriesMeta, ...decoratorQueriesMeta]; } function extractExportAs(exportAs: string | undefined): string[] | null { diff --git a/packages/core/test/acceptance/authoring/signal_queries_spec.ts b/packages/core/test/acceptance/authoring/signal_queries_spec.ts index 57d3636fede..9b76cdd4774 100644 --- a/packages/core/test/acceptance/authoring/signal_queries_spec.ts +++ b/packages/core/test/acceptance/authoring/signal_queries_spec.ts @@ -16,6 +16,7 @@ import { ElementRef, EnvironmentInjector, QueryList, + ViewChild, viewChild, ViewChildren, viewChildren, @@ -660,4 +661,42 @@ describe('queries as signals', () => { expect(fixture.componentInstance.divElsDecorator.length).toBe(1); }); }); + + it('should resolve static decorator queries when mixed with signal queries', () => { + @Directive({ + selector: 'div', + }) + class DivDirective {} + + @Component({ + imports: [DivDirective], + template: ` +
Content A
+
Content B
+
Content C
+ `, + }) + class App { + @ViewChildren(DivDirective) divs!: QueryList>; + @ViewChild('templateA') elRefA!: ElementRef; + readonly elRefB = viewChild>('templateB'); + @ViewChild('templateC') elRefC!: ElementRef; + } + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const componentInstance = fixture.componentInstance; + + expect(fixture.componentInstance.divs).withContext('divs').toBeDefined(); + expect(componentInstance.divs.length).toBe(3); + + expect(componentInstance.elRefA).withContext('A').toBeDefined(); + expect(componentInstance.elRefA.nativeElement.textContent).toBe('Content A'); + + expect(fixture.componentInstance.elRefB()).withContext('B').toBeDefined(); + expect(componentInstance.elRefB()?.nativeElement.textContent).toBe('Content B'); + + expect(fixture.componentInstance.elRefC).withContext('C').toBeDefined(); + expect(componentInstance.elRefC.nativeElement.textContent).toBe('Content C'); + }); });