Skip to content

Commit c77976d

Browse files
pkozlowski-opensourcedevversion
authored andcommitted
feat(core): reactive queries
Initial runtime implementation of the reactive view queries (in preparation for the compiler work).
1 parent 2c75408 commit c77976d

File tree

5 files changed

+266
-6
lines changed

5 files changed

+266
-6
lines changed

packages/core/src/core_reactivity_export_internal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ export {
3939
export {input} from './render3/reactivity/input';
4040
export {InputSignal, ɵɵGetInputSignalWriteType} from './render3/reactivity/input_signal';
4141
export {ModelSignal} from './render3/reactivity/model_signal';
42+
export {viewChild, viewChildren, ɵɵqueryCreate} from './render3/reactivity/queries';
4243
// clang-format on

packages/core/src/linker/query_list.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ function symbolIterator<T>(this: QueryList<T>): Iterator<T> {
4444
* @publicApi
4545
*/
4646
export class QueryList<T> implements Iterable<T> {
47-
public readonly dirty = true;
47+
public readonly dirty: boolean = true;
48+
private _onDirty?: () => void;
4849
private _results: Array<T> = [];
4950
private _changesDetected: boolean = false;
5051
private _changes: EventEmitter<QueryList<T>>|null = null;
@@ -173,9 +174,19 @@ export class QueryList<T> implements Iterable<T> {
173174
this._changes.emit(this);
174175
}
175176

177+
/** internal */
178+
onDirty(cb: () => void) {
179+
this._onDirty = cb;
180+
}
181+
176182
/** internal */
177183
setDirty() {
178-
(this as {dirty: boolean}).dirty = true;
184+
if (this.dirty === false) {
185+
(this as {dirty: boolean}).dirty = true;
186+
if (this._onDirty) {
187+
this._onDirty();
188+
}
189+
}
179190
}
180191

181192
/** internal */

packages/core/src/render3/query.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -418,12 +418,18 @@ function collectQueryResults<T>(tView: TView, lView: LView, queryIndex: number,
418418
*/
419419
export function ɵɵqueryRefresh(queryList: QueryList<any>): boolean {
420420
const lView = getLView();
421-
const tView = getTView();
422421
const queryIndex = getCurrentQueryIndex();
423422

424423
setCurrentQueryIndex(queryIndex + 1);
424+
return queryRefreshInternal(lView, queryIndex);
425+
}
425426

427+
export function queryRefreshInternal(lView: LView, queryIndex: number): boolean {
428+
const tView = lView[TVIEW];
426429
const tQuery = getTQuery(tView, queryIndex);
430+
const lQuery = lView[QUERIES]!.queries![queryIndex];
431+
const queryList = lQuery.queryList;
432+
427433
if (queryList.dirty &&
428434
(isCreationMode(lView) ===
429435
((tQuery.metadata.flags & QueryFlags.isStatic) === QueryFlags.isStatic))) {
@@ -453,6 +459,12 @@ export function ɵɵqueryRefresh(queryList: QueryList<any>): boolean {
453459
*/
454460
export function ɵɵviewQuery<T>(
455461
predicate: ProviderToken<unknown>|string[], flags: QueryFlags, read?: any): void {
462+
createViewQueryInternal(getLView(), predicate, flags, read);
463+
}
464+
465+
export function createViewQueryInternal<T>(
466+
lView: LView, predicate: ProviderToken<unknown>|string[], flags: QueryFlags,
467+
read?: any): number {
456468
ngDevMode && assertNumber(flags, 'Expecting flags');
457469
const tView = getTView();
458470
if (tView.firstCreatePass) {
@@ -461,7 +473,7 @@ export function ɵɵviewQuery<T>(
461473
tView.staticViewQueries = true;
462474
}
463475
}
464-
createLQuery<T>(tView, getLView(), flags);
476+
return createLQuery<T>(tView, lView, flags);
465477
}
466478

467479
/**
@@ -502,20 +514,22 @@ export function ɵɵloadQuery<T>(): QueryList<T> {
502514
return loadQueryInternal<T>(getLView(), getCurrentQueryIndex());
503515
}
504516

505-
function loadQueryInternal<T>(lView: LView, queryIndex: number): QueryList<T> {
517+
export function loadQueryInternal<T>(lView: LView, queryIndex: number): QueryList<T> {
506518
ngDevMode &&
507519
assertDefined(lView[QUERIES], 'LQueries should be defined when trying to load a query');
508520
ngDevMode && assertIndexInRange(lView[QUERIES]!.queries, queryIndex);
509521
return lView[QUERIES]!.queries[queryIndex].queryList;
510522
}
511523

512-
function createLQuery<T>(tView: TView, lView: LView, flags: QueryFlags) {
524+
function createLQuery<T>(tView: TView, lView: LView, flags: QueryFlags): number {
513525
const queryList = new QueryList<T>(
514526
(flags & QueryFlags.emitDistinctChangesOnly) === QueryFlags.emitDistinctChangesOnly);
515527
storeCleanupWithContext(tView, lView, queryList, queryList.destroy);
516528

517529
if (lView[QUERIES] === null) lView[QUERIES] = new LQueries_();
518530
lView[QUERIES]!.queries.push(new LQuery_(queryList));
531+
532+
return lView[QUERIES]!.queries.length - 1;
519533
}
520534

521535
function createTQuery(tView: TView, metadata: TQueryMetadata, nodeIndex: number): void {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ProviderToken} from '../../di/provider_token';
10+
import {QueryList} from '../../linker';
11+
import {createSignalFromFunction, ReactiveNode, SIGNAL, Signal} from '../../signals';
12+
import {QueryFlags} from '../interfaces/query';
13+
import {LView} from '../interfaces/view';
14+
import {createViewQueryInternal, loadQueryInternal, queryRefreshInternal} from '../query';
15+
import {getLView} from '../state';
16+
17+
export interface InternalQuerySignal {
18+
bindToQuery(queryIndex: number): void;
19+
}
20+
21+
abstract class QuerySignal<T> extends ReactiveNode implements InternalQuerySignal {
22+
private _lView?: LView;
23+
private _queryIndex?: number;
24+
protected queryList?: QueryList<T>;
25+
26+
protected override consumerAllowSignalWrites = false;
27+
28+
protected override onConsumerDependencyMayHaveChanged(): void {
29+
// This never happens for query signals as they're not consumers.
30+
}
31+
protected override onProducerUpdateValueVersion(): void {
32+
if (this.queryList === undefined || !this.queryList?.dirty) {
33+
// The current value and its version are already up to date.
34+
return;
35+
}
36+
37+
// The current value is stale. Check whether we need to produce a new one.
38+
// TODO: assert: I've got both the lView and queryIndex stored
39+
if (queryRefreshInternal(this._lView!, this._queryIndex!)) {
40+
this.valueVersion++;
41+
}
42+
}
43+
44+
bindToQuery(queryIndex: number) {
45+
// TODO: assert: should bind only once, make sure it is not re-assigned again
46+
this._lView = getLView();
47+
this._queryIndex = queryIndex;
48+
this.queryList = loadQueryInternal(this._lView, queryIndex);
49+
50+
this.queryList.onDirty(() => {
51+
// Notify any consumers about the potential change. Note that the onDirty callback will fire
52+
// only on the initial dirty marking (that is, subsequent dirty notifications are not fired -
53+
// until the QueryList becomes clean again).
54+
this.producerMayHaveChanged();
55+
});
56+
}
57+
58+
protected signalInternal(): void {
59+
// Check if the value needs updating before returning it.
60+
this.onProducerUpdateValueVersion();
61+
62+
// Record that someone looked at this signal.
63+
this.producerAccessed();
64+
}
65+
}
66+
67+
export class ChildQuerySignalImpl<T> extends QuerySignal<T> {
68+
signal(): T|undefined {
69+
this.signalInternal();
70+
return this.queryList?.first;
71+
}
72+
}
73+
74+
export class ChildrenQuerySignalImpl<T> extends QuerySignal<T> {
75+
signal(): T[] {
76+
this.signalInternal();
77+
// TODO: perf - I should not be obliged to create a new array every time we call signal()
78+
return this.queryList?.toArray() ?? [];
79+
}
80+
}
81+
82+
// THINK: code duplication for predicate, flags etc.? Or would it be extracted by the compiler?
83+
export function ɵɵqueryCreate<T>(
84+
target: Signal<T|undefined>, predicate: ProviderToken<unknown>|string[], flags: QueryFlags,
85+
read?: any) {
86+
const lView = getLView();
87+
const reactiveQueryNode = target[SIGNAL] as InternalQuerySignal;
88+
reactiveQueryNode.bindToQuery(createViewQueryInternal<T>(lView, predicate, flags, read));
89+
}
90+
91+
// Q: assuming that the return type must be similar to InputSignal, with the write ability? (this is
92+
// needed only from the generated code so maybe not?)
93+
export function viewChild<T>(
94+
selector: ProviderToken<unknown>|Function|string,
95+
opts?: {read?: any, static?: boolean}): Signal<T|undefined> {
96+
const node = new ChildQuerySignalImpl();
97+
return createSignalFromFunction<T|undefined>(node, node.signal.bind(node) as Signal<T|undefined>);
98+
}
99+
100+
export function viewChildren<T>(
101+
selector: ProviderToken<unknown>|Function|string,
102+
opts?: {read?: any, emitDistinctChangesOnly?: boolean}): Signal<T[]> {
103+
// Q: by returning a signal we are effectively "dropping" QueryList from the public API. Is there
104+
// anything valuable there that we would be loosing?
105+
const node = new ChildrenQuerySignalImpl();
106+
return createSignalFromFunction<T[]>(node, node.signal.bind(node) as Signal<T[]>);
107+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Component, computed, ElementRef, ViewChild, viewChild, viewChildren, ɵɵdefineComponent, ɵɵelement, ɵɵqueryCreate, ɵɵStandaloneFeature} from '@angular/core';
10+
import {TestBed} from '@angular/core/testing';
11+
12+
13+
describe('queries', () => {
14+
describe('view queries', () => {
15+
xit('should support child query in a single view', () => {
16+
@Component({
17+
signals: true,
18+
standalone: true,
19+
template: `<div #el></div>`,
20+
})
21+
class App {
22+
// Q1: similar to input, do we allow people to "observe" moment before assigning by throwing
23+
// if it is not set? Or return null / undefined? Do static queries make sense in this
24+
// context?
25+
// Q2: similar to input, do we need both the viewChild function _and_ @ViewChild annotation?
26+
@ViewChild('el') divEl = viewChild<ElementRef>('el');
27+
foundEl = computed(() => this.divEl() != null);
28+
}
29+
30+
const fixture = TestBed.createComponent(App);
31+
fixture.detectChanges();
32+
expect(fixture.componentInstance.foundEl()).toBeTrue();
33+
});
34+
35+
xit('should support children query in a single view', () => {
36+
@Component({
37+
signals: true,
38+
standalone: true,
39+
template: `<div #el1><div><div #el2><div>`,
40+
})
41+
class App {
42+
@ViewChild('el1,el2') divEls = viewChildren<ElementRef>('el');
43+
foundEl = computed(() => this.divEls().length === 2);
44+
}
45+
46+
const fixture = TestBed.createComponent(App);
47+
fixture.detectChanges();
48+
expect(fixture.componentInstance.foundEl()).toBeTrue();
49+
});
50+
51+
it('view child - HAND GENERATED CODE - delete after compiler is done', () => {
52+
const _c0 = ['el'];
53+
class AppComponent {
54+
divEl = viewChild<ElementRef>('el');
55+
foundEl = computed(() => this.divEl() != null);
56+
57+
static ɵfac = () => new AppComponent();
58+
static ɵcmp = ɵɵdefineComponent({
59+
type: AppComponent,
60+
selectors: [['test-cmp']],
61+
viewQuery:
62+
function App_Query(rf, ctx) {
63+
// TODO: there should be no update mode for queries any more
64+
if (rf & 1) {
65+
ɵɵqueryCreate(ctx.divEl, _c0, 1);
66+
}
67+
},
68+
standalone: true,
69+
signals: true,
70+
features: [ɵɵStandaloneFeature],
71+
decls: 3,
72+
vars: 0,
73+
consts: [['el', '']],
74+
template:
75+
function App_Template(rf) {
76+
if ((rf & 1)) {
77+
ɵɵelement(0, 'div', null, 0);
78+
}
79+
},
80+
encapsulation: 2
81+
});
82+
}
83+
84+
const fixture = TestBed.createComponent(AppComponent);
85+
fixture.detectChanges();
86+
expect(fixture.componentInstance.foundEl()).toBeTrue();
87+
});
88+
89+
it('view children - HAND GENERATED CODE - delete after compiler is done', () => {
90+
const _c0 = ['el'];
91+
class AppComponent {
92+
divEls = viewChildren<ElementRef>('el');
93+
foundElsCount = computed(() => this.divEls().length);
94+
95+
static ɵfac = () => new AppComponent();
96+
static ɵcmp = ɵɵdefineComponent({
97+
type: AppComponent,
98+
selectors: [['test-cmp']],
99+
viewQuery:
100+
function App_Query(rf, ctx) {
101+
// TODO: there should be no update mode for queries any more
102+
if (rf & 1) {
103+
ɵɵqueryCreate(ctx.divEls, _c0, 1);
104+
}
105+
},
106+
standalone: true,
107+
signals: true,
108+
features: [ɵɵStandaloneFeature],
109+
decls: 3,
110+
vars: 0,
111+
consts: [['el', '']],
112+
template:
113+
function App_Template(rf) {
114+
if ((rf & 1)) {
115+
ɵɵelement(0, 'div', null, 0);
116+
}
117+
},
118+
encapsulation: 2
119+
});
120+
}
121+
122+
const fixture = TestBed.createComponent(AppComponent);
123+
fixture.detectChanges();
124+
expect(fixture.componentInstance.foundElsCount()).toBe(1);
125+
});
126+
});
127+
});

0 commit comments

Comments
 (0)