From f36cd39ef4eb37583e495bd24a38860f9571d97c Mon Sep 17 00:00:00 2001 From: Arnoud de Vries <6420061+arnoud-dv@users.noreply.github.com> Date: Mon, 19 Feb 2024 02:11:29 +0100 Subject: [PATCH] fix(angular-query): support required input signal --- .../src/app/components/post.component.ts | 3 +- .../src/__tests__/inject-is-fetching.test.ts | 5 +- .../src/create-base-query.ts | 92 ++++++++++--------- .../src/lazy-init.ts | 34 +++++++ 4 files changed, 88 insertions(+), 46 deletions(-) create mode 100644 packages/angular-query-experimental/src/lazy-init.ts diff --git a/examples/angular/router/src/app/components/post.component.ts b/examples/angular/router/src/app/components/post.component.ts index 6d60a170bc..76a70d54ae 100644 --- a/examples/angular/router/src/app/components/post.component.ts +++ b/examples/angular/router/src/app/components/post.component.ts @@ -24,12 +24,11 @@ export default class PostComponent { #postsService = inject(PostsService) queryClient = injectQueryClient() - postId = input(0, { + postId = input.required({ transform: numberAttribute, }) postQuery = injectQuery(() => ({ - enabled: this.postId() > 0, queryKey: ['post', this.postId()], queryFn: () => { return lastValueFrom(this.#postsService.postById$(this.postId())) diff --git a/packages/angular-query-experimental/src/__tests__/inject-is-fetching.test.ts b/packages/angular-query-experimental/src/__tests__/inject-is-fetching.test.ts index 7868f3396e..5002428b22 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-is-fetching.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-is-fetching.test.ts @@ -1,4 +1,4 @@ -import { TestBed, fakeAsync, flush } from '@angular/core/testing' +import { TestBed, fakeAsync, flush, tick } from '@angular/core/testing' import { QueryClient } from '@tanstack/query-core' import { beforeEach, describe, expect } from 'vitest' import { injectIsFetching } from '../inject-is-fetching' @@ -25,6 +25,9 @@ describe('injectIsFetching', () => { })) return injectIsFetching() }) + + tick() + expect(isFetching()).toStrictEqual(1) flush() expect(isFetching()).toStrictEqual(0) diff --git a/packages/angular-query-experimental/src/create-base-query.ts b/packages/angular-query-experimental/src/create-base-query.ts index c5a0e11839..b7a2299dab 100644 --- a/packages/angular-query-experimental/src/create-base-query.ts +++ b/packages/angular-query-experimental/src/create-base-query.ts @@ -1,13 +1,16 @@ import { DestroyRef, - assertInInjectionContext, + Injector, computed, effect, inject, + runInInjectionContext, signal, + untracked, } from '@angular/core' import { notifyManager } from '@tanstack/query-core' import { signalProxy } from './signal-proxy' +import { lazyInit } from './lazy-init' import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core' import type { CreateBaseQueryOptions, CreateBaseQueryResult } from './types' @@ -33,53 +36,56 @@ export function createBaseQuery< Observer: typeof QueryObserver, queryClient: QueryClient, ): CreateBaseQueryResult { - assertInInjectionContext(createBaseQuery) - const destroyRef = inject(DestroyRef) + const injector = inject(Injector) - /** - * Signal that has the default options from query client applied - * computed() is used so signals can be inserted into the options - * making it reactive. Wrapping options in a function ensures embedded expressions - * are preserved and can keep being applied after signal changes - */ - const defaultedOptionsSignal = computed(() => { - const defaultedOptions = queryClient.defaultQueryOptions( - options(queryClient), - ) - defaultedOptions._optimisticResults = 'optimistic' - return defaultedOptions - }) + return lazyInit(() => { + return runInInjectionContext(injector, () => { + const destroyRef = inject(DestroyRef) + /** + * Signal that has the default options from query client applied + * computed() is used so signals can be inserted into the options + * making it reactive. Wrapping options in a function ensures embedded expressions + * are preserved and can keep being applied after signal changes + */ + const defaultedOptionsSignal = computed(() => { + const defaultedOptions = queryClient.defaultQueryOptions( + options(queryClient), + ) + defaultedOptions._optimisticResults = 'optimistic' + return defaultedOptions + }) - const observer = new Observer< - TQueryFnData, - TError, - TData, - TQueryData, - TQueryKey - >(queryClient, defaultedOptionsSignal()) + const observer = new Observer< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + >(queryClient, defaultedOptionsSignal()) - const resultSignal = signal( - observer.getOptimisticResult(defaultedOptionsSignal()), - ) + const resultSignal = signal( + observer.getOptimisticResult(defaultedOptionsSignal()), + ) - effect( - () => { - // Do not notify on updates because of changes in the options because - // these changes should already be reflected in the optimistic result. - const defaultedOptions = defaultedOptionsSignal() - observer.setOptions(defaultedOptions, { - listeners: false, + effect(() => { + const defaultedOptions = defaultedOptionsSignal() + observer.setOptions(defaultedOptions, { + // Do not notify on updates because of changes in the options because + // these changes should already be reflected in the optimistic result. + listeners: false, + }) + untracked(() => { + resultSignal.set(observer.getOptimisticResult(defaultedOptions)) + }) }) - resultSignal.set(observer.getOptimisticResult(defaultedOptions)) - }, - { allowSignalWrites: true }, - ) - // observer.trackResult is not used as this optimization is not needed for Angular - const unsubscribe = observer.subscribe( - notifyManager.batchCalls((val) => resultSignal.set(val)), - ) - destroyRef.onDestroy(unsubscribe) + // observer.trackResult is not used as this optimization is not needed for Angular + const unsubscribe = observer.subscribe( + notifyManager.batchCalls((val) => resultSignal.set(val)), + ) + destroyRef.onDestroy(unsubscribe) - return signalProxy(resultSignal) as CreateBaseQueryResult + return signalProxy(resultSignal) as CreateBaseQueryResult + }) + }) } diff --git a/packages/angular-query-experimental/src/lazy-init.ts b/packages/angular-query-experimental/src/lazy-init.ts new file mode 100644 index 0000000000..8c8fc4143d --- /dev/null +++ b/packages/angular-query-experimental/src/lazy-init.ts @@ -0,0 +1,34 @@ +export function lazyInit(initializer: () => T): T { + let object: T | null = null + + const initializeObject = () => { + if (!object) { + object = initializer() + } + } + + Promise.resolve().then(() => { + initializeObject() + }) + + return new Proxy({} as T, { + get(_, prop, receiver) { + initializeObject() + return Reflect.get(object as T, prop, receiver) + }, + has(_, prop) { + initializeObject() + return Reflect.has(object as T, prop) + }, + ownKeys() { + initializeObject() + return Reflect.ownKeys(object as T) + }, + getOwnPropertyDescriptor() { + return { + enumerable: true, + configurable: true, + } + }, + }) +}