Skip to content

Commit 5573d60

Browse files
committed
fix(angular-query-experimental): behaviors of injectQuery now work at runtime
Mostly
1 parent 53c901d commit 5573d60

File tree

4 files changed

+242
-91
lines changed

4 files changed

+242
-91
lines changed

packages/angular-query-experimental/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"@angular/platform-browser": "^19.1.0-next.0",
7474
"@angular/platform-browser-dynamic": "^19.1.0-next.0",
7575
"@microsoft/api-extractor": "^7.48.1",
76+
"@testing-library/angular": "^17.3.6",
7677
"eslint-plugin-jsdoc": "^50.5.0",
7778
"npm-run-all": "^4.1.5",
7879
"tsup": "8.0.2",
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { afterEach, describe, expect, it } from 'vitest'
2+
import { render, waitFor } from '@testing-library/angular'
3+
import {
4+
Component,
5+
effect,
6+
provideExperimentalZonelessChangeDetection,
7+
} from '@angular/core'
8+
import { TestBed } from '@angular/core/testing'
9+
import {
10+
QueryClient,
11+
injectQueries,
12+
provideTanStackQuery,
13+
} from '..'
14+
import { queryKey } from './test-utils'
15+
import type { CreateQueryResult } from '..'
16+
17+
let queryClient: QueryClient
18+
19+
beforeEach(() => {
20+
queryClient = new QueryClient()
21+
// vi.useFakeTimers()
22+
TestBed.configureTestingModule({
23+
providers: [
24+
provideExperimentalZonelessChangeDetection(),
25+
provideTanStackQuery(queryClient),
26+
],
27+
})
28+
})
29+
30+
afterEach(() => {
31+
// vi.useRealTimers()
32+
})
33+
34+
describe('useQueries', () => {
35+
it('should return the correct states', async () => {
36+
const key1 = queryKey()
37+
const key2 = queryKey()
38+
const results: Array<Array<CreateQueryResult>> = []
39+
40+
@Component({
41+
template: `
42+
<div>
43+
<div>
44+
data1: {{ toString(result()[0].data ?? 'null') }}, data2:
45+
{{ toString(result()[1].data ?? 'null') }}
46+
</div>
47+
</div>
48+
`,
49+
})
50+
class Page {
51+
toString(val: any) {
52+
return String(val)
53+
}
54+
result = injectQueries(() => ({
55+
queries: [
56+
{
57+
queryKey: key1,
58+
queryFn: async () => {
59+
await new Promise((r) => setTimeout(r, 10))
60+
return 1
61+
},
62+
},
63+
{
64+
queryKey: key2,
65+
queryFn: async () => {
66+
await new Promise((r) => setTimeout(r, 100))
67+
return 2
68+
},
69+
},
70+
],
71+
}))
72+
73+
_pushResults = effect(() => {
74+
results.push(this.result())
75+
})
76+
}
77+
78+
const rendered = await render(Page)
79+
80+
await waitFor(() => rendered.getByText('data1: 1, data2: 2'))
81+
82+
expect(results.length).toBe(3)
83+
expect(results[0]).toMatchObject([{ data: undefined }, { data: undefined }])
84+
expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }])
85+
expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }])
86+
})
87+
})

packages/angular-query-experimental/src/inject-queries.ts

Lines changed: 95 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,16 @@ import {
55
} from '@tanstack/query-core'
66
import {
77
DestroyRef,
8+
Injector,
89
NgZone,
910
computed,
1011
effect,
1112
inject,
13+
runInInjectionContext,
1214
signal,
15+
untracked,
1316
} from '@angular/core'
1417
import { assertInjector } from './util/assert-injector/assert-injector'
15-
import {
16-
CreateQueryOptions,
17-
CreateQueryResult,
18-
DefinedCreateQueryResult,
19-
} from './types'
20-
import type { Injector, Signal } from '@angular/core'
2118
import type {
2219
DefaultError,
2320
OmitKeyof,
@@ -26,9 +23,14 @@ import type {
2623
QueryFunction,
2724
QueryKey,
2825
QueryObserverOptions,
29-
QueryObserverResult,
3026
ThrowOnError,
3127
} from '@tanstack/query-core'
28+
import type {
29+
CreateQueryOptions,
30+
CreateQueryResult,
31+
DefinedCreateQueryResult,
32+
} from './types'
33+
import type { Signal } from '@angular/core'
3234

3335
// This defines the `CreateQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
3436
// `placeholderData` function always gets undefined passed
@@ -222,50 +224,90 @@ export function injectQueries<
222224
optionsFn: () => InjectQueriesOptions<T, TCombinedResult>,
223225
injector?: Injector,
224226
): Signal<TCombinedResult> {
225-
return 0 as never
226-
// return assertInjector(injectQueries, injector, () => {
227-
// const destroyRef = inject(DestroyRef)
228-
// const ngZone = inject(NgZone)
229-
// const queryClient = inject(QueryClient)
230-
//
231-
// const defaultedQueries = computed(() => {
232-
// return queries().map((opts) => {
233-
// const defaultedOptions = queryClient.defaultQueryOptions(opts)
234-
// // Make sure the results are already in fetching state before subscribing or updating options
235-
// defaultedOptions._optimisticResults = 'optimistic'
236-
//
237-
// return defaultedOptions as QueryObserverOptions
238-
// })
239-
// })
240-
//
241-
// const observer = new QueriesObserver<TCombinedResult>(
242-
// queryClient,
243-
// defaultedQueries(),
244-
// options as QueriesObserverOptions<TCombinedResult>,
245-
// )
246-
//
247-
// // Do not notify on updates because of changes in the options because
248-
// // these changes should already be reflected in the optimistic result.
249-
// effect(() => {
250-
// observer.setQueries(
251-
// defaultedQueries(),
252-
// options as QueriesObserverOptions<TCombinedResult>,
253-
// { listeners: false },
254-
// )
255-
// })
256-
//
257-
// const [, getCombinedResult] = observer.getOptimisticResult(
258-
// defaultedQueries(),
259-
// (options as QueriesObserverOptions<TCombinedResult>).combine,
260-
// )
261-
//
262-
// const result = signal(getCombinedResult() as any)
263-
//
264-
// const unsubscribe = ngZone.runOutsideAngular(() =>
265-
// observer.subscribe(notifyManager.batchCalls(result.set)),
266-
// )
267-
// destroyRef.onDestroy(unsubscribe)
268-
//
269-
// return result
270-
// })
227+
return assertInjector(injectQueries, injector, () => {
228+
const ngInjector = inject(Injector)
229+
const destroyRef = inject(DestroyRef)
230+
const ngZone = inject(NgZone)
231+
const queryClient = inject(QueryClient)
232+
233+
/**
234+
* Signal that has the default options from query client applied
235+
* computed() is used so signals can be inserted into the options
236+
* making it reactive. Wrapping options in a function ensures embedded expressions
237+
* are preserved and can keep being applied after signal changes
238+
*/
239+
const optionsSignal = computed(() => {
240+
return runInInjectionContext(injector ?? ngInjector, () => optionsFn())
241+
})
242+
243+
const defaultedQueries = computed(() => {
244+
return optionsSignal().queries.map((opts) => {
245+
const defaultedOptions = queryClient.defaultQueryOptions(opts)
246+
// Make sure the results are already in fetching state before subscribing or updating options
247+
defaultedOptions._optimisticResults = 'optimistic'
248+
249+
return defaultedOptions as QueryObserverOptions
250+
})
251+
})
252+
253+
const observerSignal = (() => {
254+
let instance: QueriesObserver<TCombinedResult> | null = null
255+
256+
return computed(() => {
257+
return (instance ||= new QueriesObserver<TCombinedResult>(
258+
queryClient,
259+
defaultedQueries(),
260+
optionsSignal() as QueriesObserverOptions<TCombinedResult>,
261+
))
262+
})
263+
})()
264+
265+
const optimisticResultSignal = computed(() =>
266+
observerSignal().getOptimisticResult(
267+
defaultedQueries(),
268+
(optionsSignal() as QueriesObserverOptions<TCombinedResult>).combine,
269+
),
270+
)
271+
272+
// Do not notify on updates because of changes in the options because
273+
// these changes should already be reflected in the optimistic result.
274+
effect(() => {
275+
observerSignal().setQueries(
276+
defaultedQueries(),
277+
optionsSignal() as QueriesObserverOptions<TCombinedResult>,
278+
{ listeners: false },
279+
)
280+
})
281+
282+
const optimisticCombinedResultSignal = computed(() => {
283+
const [_optimisticResult, getCombinedResult, trackResult] =
284+
optimisticResultSignal()
285+
return getCombinedResult(trackResult())
286+
})
287+
288+
const resultFromSubscriberSignal = signal<TCombinedResult | null>(null)
289+
290+
effect(() => {
291+
const observer = observerSignal()
292+
const [_optimisticResult, getCombinedResult] = optimisticResultSignal()
293+
294+
untracked(() => {
295+
const unsubscribe = ngZone.runOutsideAngular(() =>
296+
observer.subscribe(
297+
notifyManager.batchCalls((state) => {
298+
resultFromSubscriberSignal.set(getCombinedResult(state))
299+
}),
300+
),
301+
)
302+
303+
destroyRef.onDestroy(unsubscribe)
304+
})
305+
})
306+
307+
return computed(() => {
308+
const subscriberResult = resultFromSubscriberSignal()
309+
const optimisticResult = optimisticCombinedResultSignal()
310+
return subscriberResult ?? optimisticResult
311+
})
312+
}) as unknown as Signal<TCombinedResult>
271313
}

0 commit comments

Comments
 (0)