Skip to content

Commit 018f826

Browse files
crisbetoatscott
authored andcommitted
fix(core): ensure all initializer functions run in an injection context (#54761)
Ensures that all of the functions intended to be run in initializers are in an injection context. This is a stop-gap until we have a compiler diagnostic for it. PR Close #54761
1 parent 47f79e7 commit 018f826

File tree

4 files changed

+97
-34
lines changed

4 files changed

+97
-34
lines changed

packages/core/src/authoring/input/input.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {assertInInjectionContext} from '../../di';
10+
911
import {createInputSignal, InputOptions, InputOptionsWithoutTransform, InputOptionsWithTransform, InputSignal, InputSignalWithTransform} from './input_signal';
1012
import {REQUIRED_UNSET_VALUE} from './input_signal_node';
1113

1214
export function inputFunction<ReadT, WriteT>(
1315
initialValue?: ReadT,
1416
opts?: InputOptions<ReadT, WriteT>): InputSignalWithTransform<ReadT|undefined, WriteT> {
17+
ngDevMode && assertInInjectionContext(input);
1518
return createInputSignal(initialValue, opts);
1619
}
1720

1821
export function inputRequiredFunction<ReadT, WriteT>(opts?: InputOptions<ReadT, WriteT>):
1922
InputSignalWithTransform<ReadT, WriteT> {
23+
ngDevMode && assertInInjectionContext(input);
2024
return createInputSignal(REQUIRED_UNSET_VALUE as never, opts);
2125
}
2226

packages/core/src/authoring/queries.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {assertInInjectionContext} from '../di';
910
import {ProviderToken} from '../di/provider_token';
1011
import {createMultiResultQuerySignalFn, createSingleResultOptionalQuerySignalFn, createSingleResultRequiredQuerySignalFn} from '../render3/query_reactive';
1112
import {Signal} from '../render3/reactivity/api';
1213

1314
function viewChildFn<LocatorT, ReadT>(
1415
locator: ProviderToken<LocatorT>|string,
1516
opts?: {read?: ProviderToken<ReadT>}): Signal<ReadT|undefined> {
17+
ngDevMode && assertInInjectionContext(viewChild);
1618
return createSingleResultOptionalQuerySignalFn<ReadT>();
1719
}
1820

1921
function viewChildRequiredFn<LocatorT, ReadT>(
2022
locator: ProviderToken<LocatorT>|string, opts?: {read?: ProviderToken<ReadT>}): Signal<ReadT> {
23+
ngDevMode && assertInInjectionContext(viewChild);
2124
return createSingleResultRequiredQuerySignalFn<ReadT>();
2225
}
2326

@@ -108,18 +111,21 @@ export function viewChildren<LocatorT, ReadT>(
108111
export function viewChildren<LocatorT, ReadT>(
109112
locator: ProviderToken<LocatorT>|string,
110113
opts?: {read?: ProviderToken<ReadT>}): Signal<ReadonlyArray<ReadT>> {
114+
ngDevMode && assertInInjectionContext(viewChildren);
111115
return createMultiResultQuerySignalFn<ReadT>();
112116
}
113117

114118
export function contentChildFn<LocatorT, ReadT>(
115119
locator: ProviderToken<LocatorT>|string,
116120
opts?: {descendants?: boolean, read?: ProviderToken<ReadT>}): Signal<ReadT|undefined> {
121+
ngDevMode && assertInInjectionContext(contentChild);
117122
return createSingleResultOptionalQuerySignalFn<ReadT>();
118123
}
119124

120125
function contentChildRequiredFn<LocatorT, ReadT>(
121126
locator: ProviderToken<LocatorT>|string,
122127
opts?: {descendants?: boolean, read?: ProviderToken<ReadT>}): Signal<ReadT> {
128+
ngDevMode && assertInInjectionContext(contentChildren);
123129
return createSingleResultRequiredQuerySignalFn<ReadT>();
124130
}
125131

packages/core/test/acceptance/authoring/signal_queries_spec.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,25 @@ describe('queries as signals', () => {
3131

3232
fixture.detectChanges();
3333
expect(fixture.componentInstance.foundEl()).toBeTrue();
34+
});
3435

35-
// non-required query results are undefined before we run creation mode on the view queries
36-
const appCmpt = new AppComponent();
37-
expect(appCmpt.divEl()).toBeUndefined();
36+
it('should return undefined if optional query is read in the constructor', () => {
37+
let result: {}|undefined = {};
38+
39+
@Component({
40+
standalone: true,
41+
template: `<div #el></div>`,
42+
})
43+
class AppComponent {
44+
divEl = viewChild<ElementRef<HTMLDivElement>>('el');
45+
46+
constructor() {
47+
result = this.divEl();
48+
}
49+
}
50+
51+
TestBed.createComponent(AppComponent);
52+
expect(result).toBeUndefined();
3853
});
3954

4055
it('should query for a required element in a template', () => {
@@ -55,11 +70,24 @@ describe('queries as signals', () => {
5570

5671
fixture.detectChanges();
5772
expect(fixture.componentInstance.foundEl()).toBeTrue();
73+
});
74+
75+
it('should throw if required query is read in the constructor', () => {
76+
@Component({
77+
standalone: true,
78+
template: `<div #el></div>`,
79+
})
80+
class AppComponent {
81+
divEl = viewChild.required<ElementRef<HTMLDivElement>>('el');
82+
83+
constructor() {
84+
this.divEl();
85+
}
86+
}
5887

5988
// non-required query results are undefined before we run creation mode on the view queries
60-
const appCmpt = new AppComponent();
6189
expect(() => {
62-
appCmpt.divEl();
90+
TestBed.createComponent(AppComponent);
6391
}).toThrowError(/NG0951: Child query result is required but no value is available/);
6492
});
6593

@@ -96,10 +124,25 @@ describe('queries as signals', () => {
96124
fixture.componentInstance.show = false;
97125
fixture.detectChanges();
98126
expect(fixture.componentInstance.foundEl()).toBe(1);
127+
});
99128

100-
// non-required query results are undefined before we run creation mode on the view queries
101-
const appCmpt = new AppComponent();
102-
expect(appCmpt.divEls().length).toBe(0);
129+
it('should return an empty array when reading children query in the constructor', () => {
130+
let result: readonly ElementRef[]|undefined;
131+
132+
@Component({
133+
standalone: true,
134+
template: `<div #el></div>`,
135+
})
136+
class AppComponent {
137+
divEls = viewChildren<ElementRef<HTMLDivElement>>('el');
138+
139+
constructor() {
140+
result = this.divEls();
141+
}
142+
}
143+
144+
TestBed.createComponent(AppComponent);
145+
expect(result).toEqual([]);
103146
});
104147

105148
it('should return the same array instance when there were no changes in results', () => {

packages/core/test/authoring/input_signal_spec.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,51 +37,61 @@ describe('input signal', () => {
3737
});
3838

3939
it('should work with computed expressions', () => {
40-
const signal = input(0);
41-
let computedCount = 0;
42-
const derived = computed(() => (computedCount++, signal() + 1000));
40+
TestBed.runInInjectionContext(() => {
41+
const signal = input(0);
42+
let computedCount = 0;
43+
const derived = computed(() => (computedCount++, signal() + 1000));
4344

44-
const node = signal[SIGNAL];
45-
expect(derived()).toBe(1000);
46-
expect(computedCount).toBe(1);
45+
const node = signal[SIGNAL];
46+
expect(derived()).toBe(1000);
47+
expect(computedCount).toBe(1);
4748

48-
node.applyValueToInputSignal(node, 1);
49-
expect(computedCount).toBe(1);
49+
node.applyValueToInputSignal(node, 1);
50+
expect(computedCount).toBe(1);
5051

51-
expect(derived()).toBe(1001);
52-
expect(computedCount).toBe(2);
52+
expect(derived()).toBe(1001);
53+
expect(computedCount).toBe(2);
54+
});
5355
});
5456

5557
it('should capture transform for later use in framework', () => {
56-
const signal = input(0, {transform: (v: number) => v + 1000});
57-
const node = signal[SIGNAL];
58+
TestBed.runInInjectionContext(() => {
59+
const signal = input(0, {transform: (v: number) => v + 1000});
60+
const node = signal[SIGNAL];
5861

59-
expect(node.transformFn?.(1)).toBe(1001);
62+
expect(node.transformFn?.(1)).toBe(1001);
63+
});
6064
});
6165

6266
it('should throw if there is no value for required inputs', () => {
63-
const signal = input.required();
64-
const node = signal[SIGNAL];
67+
TestBed.runInInjectionContext(() => {
68+
const signal = input.required();
69+
const node = signal[SIGNAL];
6570

66-
expect(() => signal()).toThrowError(/Input is required but no value is available yet\./);
71+
expect(() => signal()).toThrowError(/Input is required but no value is available yet\./);
6772

68-
node.applyValueToInputSignal(node, 1);
69-
expect(signal()).toBe(1);
73+
node.applyValueToInputSignal(node, 1);
74+
expect(signal()).toBe(1);
75+
});
7076
});
7177

7278
it('should throw if a `computed` depends on an uninitialized required input', () => {
73-
const signal = input.required<number>();
74-
const expr = computed(() => signal() + 1000);
75-
const node = signal[SIGNAL];
79+
TestBed.runInInjectionContext(() => {
80+
const signal = input.required<number>();
81+
const expr = computed(() => signal() + 1000);
82+
const node = signal[SIGNAL];
7683

77-
expect(() => expr()).toThrowError(/Input is required but no value is available yet\./);
84+
expect(() => expr()).toThrowError(/Input is required but no value is available yet\./);
7885

79-
node.applyValueToInputSignal(node, 1);
80-
expect(expr()).toBe(1001);
86+
node.applyValueToInputSignal(node, 1);
87+
expect(expr()).toBe(1001);
88+
});
8189
});
8290

8391
it('should have a toString implementation', () => {
84-
const signal = input(0);
85-
expect(signal + '').toBe('[Input Signal: 0]');
92+
TestBed.runInInjectionContext(() => {
93+
const signal = input(0);
94+
expect(signal + '').toBe('[Input Signal: 0]');
95+
});
8696
});
8797
});

0 commit comments

Comments
 (0)