diff --git a/.changeset/deep-crews-open.md b/.changeset/deep-crews-open.md new file mode 100644 index 0000000000..b46a786d5a --- /dev/null +++ b/.changeset/deep-crews-open.md @@ -0,0 +1,5 @@ +--- +'@tanstack/angular-query-experimental': minor +--- + +require Angular v19+ and use Angular component effect scheduling diff --git a/docs/framework/angular/angular-httpclient-and-other-data-fetching-clients.md b/docs/framework/angular/angular-httpclient-and-other-data-fetching-clients.md index 0784526cb5..7bd3764e0e 100644 --- a/docs/framework/angular/angular-httpclient-and-other-data-fetching-clients.md +++ b/docs/framework/angular/angular-httpclient-and-other-data-fetching-clients.md @@ -36,8 +36,6 @@ class ExampleComponent { ``` > Since Angular is moving towards RxJS as an optional dependency, it's expected that `HttpClient` will also support promises in the future. -> -> Support for observables in TanStack Query for Angular is planned. ## Comparison table diff --git a/docs/framework/angular/guides/caching.md b/docs/framework/angular/guides/caching.md index 32c6e6d58d..5de89421a5 100644 --- a/docs/framework/angular/guides/caching.md +++ b/docs/framework/angular/guides/caching.md @@ -27,7 +27,7 @@ Let's assume we are using the default `gcTime` of **5 minutes** and the default - When the request completes successfully, the cache's data under the `['todos']` key is updated with the new data, and both instances are updated with the new data. - Both instances of the `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos })` query are destroyed and no longer in use. - Since there are no more active instances of this query, a garbage collection timeout is set using `gcTime` to delete and garbage collect the query (defaults to **5 minutes**). -- Before the cache timeout has completed, another instance of `injectQuery(() => ({ queryKey: ['todos'], queyFn: fetchTodos })` mounts. The query immediately returns the available cached data while the `fetchTodos` function is being run in the background. When it completes successfully, it will populate the cache with fresh data. +- Before the cache timeout has completed, another instance of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos })` mounts. The query immediately returns the available cached data while the `fetchTodos` function is being run in the background. When it completes successfully, it will populate the cache with fresh data. - The final instance of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos })` gets destroyed. - No more instances of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos })` appear within **5 minutes**. - The cached data under the `['todos']` key is deleted and garbage collected. diff --git a/docs/framework/angular/guides/default-query-function.md b/docs/framework/angular/guides/default-query-function.md index 75e0fa3c48..4db44c9828 100644 --- a/docs/framework/angular/guides/default-query-function.md +++ b/docs/framework/angular/guides/default-query-function.md @@ -28,7 +28,10 @@ bootstrapApplication(MyAppComponent, { providers: [provideTanStackQuery(queryClient)], }) -export class PostsComponent { +@Component({ + // ... +}) +class PostsComponent { // All you have to do now is pass a key! postsQuery = injectQuery>(() => ({ queryKey: ['/posts'], @@ -36,11 +39,16 @@ export class PostsComponent { // ... } -export class PostComponent { +@Component({ + // ... +}) +class PostComponent { + postId = input(0) + // You can even leave out the queryFn and just go straight into options postQuery = injectQuery(() => ({ - enabled: this.postIdSignal() > 0, - queryKey: [`/posts/${this.postIdSignal()}`], + enabled: this.postId() > 0, + queryKey: [`/posts/${this.postId()}`], })) // ... } diff --git a/docs/framework/angular/guides/dependent-queries.md b/docs/framework/angular/guides/dependent-queries.md index 38afbd491f..8fc8123cec 100644 --- a/docs/framework/angular/guides/dependent-queries.md +++ b/docs/framework/angular/guides/dependent-queries.md @@ -10,14 +10,14 @@ replace: { 'useQuery': 'injectQuery', 'useQueries': 'injectQueries' } ```ts // Get the user userQuery = injectQuery(() => ({ - queryKey: ['user', email], - queryFn: getUserByEmail, + queryKey: ['user', this.email()], + queryFn: this.getUserByEmail, })) // Then get the user's projects projectsQuery = injectQuery(() => ({ queryKey: ['projects', this.userQuery.data()?.id], - queryFn: getProjectsByUser, + queryFn: this.getProjectsByUser, // The query will not execute until the user id exists enabled: !!this.userQuery.data()?.id, })) @@ -26,8 +26,23 @@ projectsQuery = injectQuery(() => ({ [//]: # 'Example' [//]: # 'Example2' +Dynamic parallel query - `injectQueries` can depend on a previous query also, here's how to achieve this: + ```ts -// injectQueries is under development for Angular Query +// Get the users ids +userIds = injectQuery(() => ({ + queryKey: ['users'], + queryFn: getUserData, + select: (users) => users.map((user) => user.id), +})) + +// Then get the users messages +userQueries = injectQueries(() => ({ + queries: (this.userIds() ?? []).map((userId) => ({ + queryKey: ['user', userId], + queryFn: () => getUserById(userId), + })), +})) ``` [//]: # 'Example2' diff --git a/docs/framework/angular/guides/disabling-queries.md b/docs/framework/angular/guides/disabling-queries.md index 35da0225de..2977491196 100644 --- a/docs/framework/angular/guides/disabling-queries.md +++ b/docs/framework/angular/guides/disabling-queries.md @@ -13,9 +13,9 @@ replace: { 'useQuery': 'injectQuery' } template: `
- @if (query.data()) { + @if (query.data(); as data) {
    - @for (todo of query.data(); track todo.id) { + @for (todo of data; track todo.id) {
  • {{ todo.title }}
  • }
@@ -70,7 +70,7 @@ export class TodosComponent { [//]: # 'Example3' ```angular-ts -import { skipToken, injectQuery } from '@tanstack/query-angular' +import { skipToken, injectQuery } from '@tanstack/angular-query-experimental' @Component({ selector: 'todos', diff --git a/docs/framework/angular/guides/does-this-replace-client-state.md b/docs/framework/angular/guides/does-this-replace-client-state.md index 3872115c94..0c16fb3568 100644 --- a/docs/framework/angular/guides/does-this-replace-client-state.md +++ b/docs/framework/angular/guides/does-this-replace-client-state.md @@ -7,5 +7,7 @@ replace: 'useQuery': 'injectQuery', 'useMutation': 'injectMutation', 'hook': 'function', + 'Redux, MobX or': 'NgRx Store or', + 'Redux, MobX, Zustand': 'NgRx Store, custom services with RxJS', } --- diff --git a/docs/framework/angular/guides/important-defaults.md b/docs/framework/angular/guides/important-defaults.md index 886792038b..b8af966642 100644 --- a/docs/framework/angular/guides/important-defaults.md +++ b/docs/framework/angular/guides/important-defaults.md @@ -4,7 +4,6 @@ title: Important Defaults ref: docs/framework/react/guides/important-defaults.md replace: { - 'React': 'Angular', 'react-query': 'angular-query', 'useQuery': 'injectQuery', 'useInfiniteQuery': 'injectInfiniteQuery', diff --git a/docs/framework/angular/guides/infinite-queries.md b/docs/framework/angular/guides/infinite-queries.md index 9fdde83e26..35371a5ff4 100644 --- a/docs/framework/angular/guides/infinite-queries.md +++ b/docs/framework/angular/guides/infinite-queries.md @@ -75,6 +75,8 @@ export class Example { template: ` `, }) export class Example { + projectsService = inject(ProjectsService) + query = injectInfiniteQuery(() => ({ queryKey: ['projects'], queryFn: async ({ pageParam }) => { diff --git a/docs/framework/angular/guides/initial-query-data.md b/docs/framework/angular/guides/initial-query-data.md index 28673844be..7cd6472c4f 100644 --- a/docs/framework/angular/guides/initial-query-data.md +++ b/docs/framework/angular/guides/initial-query-data.md @@ -81,7 +81,7 @@ result = injectQuery(() => ({ ```ts result = injectQuery(() => ({ queryKey: ['todo', this.todoId()], - queryFn: () => fetch('/todos'), + queryFn: () => fetch(`/todos/${this.todoId()}`), initialData: () => { // Use a todo from the 'todos' query as the initial data for this todo query return this.queryClient @@ -99,9 +99,11 @@ result = injectQuery(() => ({ queryKey: ['todos', this.todoId()], queryFn: () => fetch(`/todos/${this.todoId()}`), initialData: () => - queryClient.getQueryData(['todos'])?.find((d) => d.id === this.todoId()), + this.queryClient + .getQueryData(['todos']) + ?.find((d) => d.id === this.todoId()), initialDataUpdatedAt: () => - queryClient.getQueryState(['todos'])?.dataUpdatedAt, + this.queryClient.getQueryState(['todos'])?.dataUpdatedAt, })) ``` @@ -114,7 +116,7 @@ result = injectQuery(() => ({ queryFn: () => fetch(`/todos/${this.todoId()}`), initialData: () => { // Get the query state - const state = queryClient.getQueryState(['todos']) + const state = this.queryClient.getQueryState(['todos']) // If the query exists and has data that is no older than 10 seconds... if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { diff --git a/docs/framework/angular/guides/invalidations-from-mutations.md b/docs/framework/angular/guides/invalidations-from-mutations.md index 18f84e9ec3..74245f212b 100644 --- a/docs/framework/angular/guides/invalidations-from-mutations.md +++ b/docs/framework/angular/guides/invalidations-from-mutations.md @@ -2,7 +2,12 @@ id: invalidations-from-mutations title: Invalidations from Mutations ref: docs/framework/react/guides/invalidations-from-mutations.md -replace: { 'useMutation': 'injectMutation', 'hook': 'function' } +replace: + { + 'react-query': 'angular-query-experimental', + 'useMutation': 'injectMutation', + 'hook': 'function', + } --- [//]: # 'Example' @@ -17,11 +22,15 @@ mutation = injectMutation(() => ({ [//]: # 'Example2' ```ts +import { Component, inject } from '@angular/core' import { injectMutation, QueryClient, } from '@tanstack/angular-query-experimental' +@Component({ + // ... +}) export class TodosComponent { queryClient = inject(QueryClient) diff --git a/docs/framework/angular/guides/mutations.md b/docs/framework/angular/guides/mutations.md index f511808231..b769ad8ee1 100644 --- a/docs/framework/angular/guides/mutations.md +++ b/docs/framework/angular/guides/mutations.md @@ -243,12 +243,15 @@ queryClient.setMutationDefaults(['addTodo'], { retry: 3, }) -class someComponent { +@Component({ + // ... +}) +class SomeComponent { // Start mutation in some component: mutation = injectMutation(() => ({ mutationKey: ['addTodo'] })) someMethod() { - mutation.mutate({ title: 'title' }) + this.mutation.mutate({ title: 'title' }) } } diff --git a/docs/framework/angular/guides/paginated-queries.md b/docs/framework/angular/guides/paginated-queries.md index 4baa3d4cfb..d8a5900995 100644 --- a/docs/framework/angular/guides/paginated-queries.md +++ b/docs/framework/angular/guides/paginated-queries.md @@ -35,9 +35,9 @@ const result = injectQuery(() => ({ instantaneously while they are also re-fetched invisibly in the background.

- @if (query.status() === 'pending') { + @if (query.isPending()) {
Loading...
- } @else if (query.status() === 'error') { + } @else if (query.isError()) {
Error: {{ query.error().message }}
} @else { @@ -83,7 +83,7 @@ export class PaginationExampleComponent { effect(() => { // Prefetch the next page! if (!this.query.isPlaceholderData() && this.query.data()?.hasMore) { - this.#queryClient.prefetchQuery({ + this.queryClient.prefetchQuery({ queryKey: ['projects', this.page() + 1], queryFn: () => lastValueFrom(fetchProjects(this.page() + 1)), }) diff --git a/docs/framework/angular/guides/parallel-queries.md b/docs/framework/angular/guides/parallel-queries.md index f88756a2a8..a7e7c9b6e4 100644 --- a/docs/framework/angular/guides/parallel-queries.md +++ b/docs/framework/angular/guides/parallel-queries.md @@ -17,6 +17,9 @@ replace: [//]: # 'Example' ```ts +@Component({ + // ... +}) export class AppComponent { // The following queries will execute in parallel usersQuery = injectQuery(() => ({ queryKey: ['users'], queryFn: fetchUsers })) @@ -39,12 +42,14 @@ TanStack Query provides `injectQueries`, which you can use to dynamically execut [//]: # 'Example2' ```ts +@Component({ + // ... +}) export class AppComponent { users = signal>([]) - // Please note injectQueries is under development and this code does not work yet userQueries = injectQueries(() => ({ - queries: users().map((user) => { + queries: this.users().map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), diff --git a/docs/framework/angular/guides/placeholder-query-data.md b/docs/framework/angular/guides/placeholder-query-data.md index ba92fbc4ed..2298d7bd2c 100644 --- a/docs/framework/angular/guides/placeholder-query-data.md +++ b/docs/framework/angular/guides/placeholder-query-data.md @@ -7,6 +7,9 @@ ref: docs/framework/react/guides/placeholder-query-data.md [//]: # 'ExampleValue' ```ts +@Component({ + // ... +}) class TodosComponent { result = injectQuery(() => ({ queryKey: ['todos'], @@ -22,10 +25,15 @@ class TodosComponent { [//]: # 'ExampleFunction' ```ts -class TodosComponent { +@Component({ + // ... +}) +export class TodosComponent { + todoId = signal(1) + result = injectQuery(() => ({ - queryKey: ['todos', id()], - queryFn: () => fetch(`/todos/${id}`), + queryKey: ['todos', this.todoId()], + queryFn: () => fetch(`/todos/${this.todoId()}`), placeholderData: (previousData, previousQuery) => previousData, })) } @@ -35,6 +43,9 @@ class TodosComponent { [//]: # 'ExampleCache' ```ts +@Component({ + // ... +}) export class BlogPostComponent { postId = input.required() queryClient = inject(QueryClient) diff --git a/docs/framework/angular/guides/queries.md b/docs/framework/angular/guides/queries.md index 336da2e171..8192f927f0 100644 --- a/docs/framework/angular/guides/queries.md +++ b/docs/framework/angular/guides/queries.md @@ -19,6 +19,9 @@ replace: ```ts import { injectQuery } from '@tanstack/angular-query-experimental' +@Component({ + // ... +}) export class TodosComponent { info = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList })) } @@ -61,38 +64,7 @@ export class PostsComponent { ``` [//]: # 'Example3' - -If booleans aren't your thing, you can always use the `status` state as well: - [//]: # 'Example4' - -```angular-ts -@Component({ - selector: 'todos', - template: ` - @switch (todos.status()) { - @case ('pending') { - Loading... - } - @case ('error') { - Error: {{ todos.error()?.message }} - } - - @default { -
    - @for (todo of todos.data(); track todo.id) { -
  • {{ todo.title }}
  • - } @empty { -
  • No todos found
  • - } -
- } - } - `, -}) -class TodosComponent {} -``` - [//]: # 'Example4' [//]: # 'Materials' [//]: # 'Materials' diff --git a/docs/framework/angular/guides/query-functions.md b/docs/framework/angular/guides/query-functions.md index ae5f9e6c99..c8c3bd992a 100644 --- a/docs/framework/angular/guides/query-functions.md +++ b/docs/framework/angular/guides/query-functions.md @@ -8,16 +8,19 @@ ref: docs/framework/react/guides/query-functions.md ```ts injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchAllTodos })) -injectQuery(() => ({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) }) injectQuery(() => ({ - queryKey: ['todos', todoId], + queryKey: ['todos', todoId()], + queryFn: () => fetchTodoById(todoId()), +})) +injectQuery(() => ({ + queryKey: ['todos', todoId()], queryFn: async () => { - const data = await fetchTodoById(todoId) + const data = await fetchTodoById(todoId()) return data }, })) injectQuery(() => ({ - queryKey: ['todos', todoId], + queryKey: ['todos', todoId()], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), })) ``` @@ -26,8 +29,10 @@ injectQuery(() => ({ [//]: # 'Example2' ```ts +todoId = signal(1) + todos = injectQuery(() => ({ - queryKey: ['todos', todoId()], + queryKey: ['todos', this.todoId()], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') @@ -45,10 +50,12 @@ todos = injectQuery(() => ({ [//]: # 'Example3' ```ts +todoId = signal(1) + todos = injectQuery(() => ({ - queryKey: ['todos', todoId()], + queryKey: ['todos', this.todoId()], queryFn: async () => { - const response = await fetch('/todos/' + todoId) + const response = await fetch('/todos/' + this.todoId()) if (!response.ok) { throw new Error('Network response was not ok') } @@ -61,8 +68,11 @@ todos = injectQuery(() => ({ [//]: # 'Example4' ```ts +status = signal('active') +page = signal(1) + result = injectQuery(() => ({ - queryKey: ['todos', { status: status(), page: page() }], + queryKey: ['todos', { status: this.status(), page: this.page() }], queryFn: fetchTodoList, })) diff --git a/docs/framework/angular/guides/query-invalidation.md b/docs/framework/angular/guides/query-invalidation.md index 9cd8ea5809..e174d0a4b4 100644 --- a/docs/framework/angular/guides/query-invalidation.md +++ b/docs/framework/angular/guides/query-invalidation.md @@ -8,8 +8,12 @@ replace: { 'useQuery': 'injectQuery', 'hooks': 'functions' } [//]: # 'Example2' ```ts +import { Component, inject } from '@angular/core' import { injectQuery, QueryClient } from '@tanstack/angular-query-experimental' +@Component({ + // ... +}) class QueryInvalidationExample { queryClient = inject(QueryClient) @@ -72,7 +76,7 @@ todoListQuery = injectQuery(() => ({ })) // However, the following query below will NOT be invalidated -const todoListQuery = injectQuery(() => ({ +todoListQuery = injectQuery(() => ({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, })) diff --git a/docs/framework/angular/guides/query-options.md b/docs/framework/angular/guides/query-options.md index d63753bbfc..6d6ad6b3e9 100644 --- a/docs/framework/angular/guides/query-options.md +++ b/docs/framework/angular/guides/query-options.md @@ -31,26 +31,26 @@ export class QueriesService { // usage: -postId = input.required({ - transform: numberAttribute, -}) +postId = input.required({ transform: numberAttribute }) queries = inject(QueriesService) +queryClient = inject(QueryClient) postQuery = injectQuery(() => this.queries.post(this.postId())) -queryClient.prefetchQuery(this.queries.post(23)) -queryClient.setQueryData(this.queries.post(42).queryKey, newPost) +someMethod() { + this.queryClient.prefetchQuery(this.queries.post(23)) + this.queryClient.setQueryData(this.queries.post(42).queryKey, newPost) +} ``` [//]: # 'Example1' [//]: # 'Example2' ```ts -// Type inference still works, so query.data will be the return type of select instead of queryFn queries = inject(QueriesService) query = injectQuery(() => ({ - ...groupOptions(1), + ...this.queries.post(1), select: (data) => data.title, })) ``` diff --git a/docs/framework/angular/guides/query-retries.md b/docs/framework/angular/guides/query-retries.md index 45228d10bb..c6c73fb280 100644 --- a/docs/framework/angular/guides/query-retries.md +++ b/docs/framework/angular/guides/query-retries.md @@ -31,10 +31,10 @@ const result = injectQuery(() => ({ ```ts // Configure for all queries import { - QueryCache, QueryClient, - QueryClientProvider, + provideTanStackQuery, } from '@tanstack/angular-query-experimental' +import { bootstrapApplication } from '@angular/platform-browser' const queryClient = new QueryClient({ defaultOptions: { @@ -51,7 +51,7 @@ bootstrapApplication(AppComponent, { [//]: # 'Example2' -Though it is not recommended, you can obviously override the `retryDelay` function/integer in both the Provider and individual query options. If set to an integer instead of a function the delay will always be the same amount of time: +Though it is not recommended, you can obviously override the `retryDelay` function/integer in both the QueryClient default options and individual query options. If set to an integer instead of a function the delay will always be the same amount of time: [//]: # 'Example3' diff --git a/docs/framework/angular/guides/testing.md b/docs/framework/angular/guides/testing.md index 7648d7f6b3..91d9575dbc 100644 --- a/docs/framework/angular/guides/testing.md +++ b/docs/framework/angular/guides/testing.md @@ -7,9 +7,7 @@ Most Angular tests using TanStack Query will involve services or components that TanStack Query's `inject*` functions integrate with [`PendingTasks`](https://angular.dev/api/core/PendingTasks) which ensures the framework is aware of in-progress queries and mutations. -This means tests and SSR can wait until mutations and queries resolve. In unit tests you can use `ApplicationRef.whenStable()` or `fixture.whenStable()` to await query completion. This works for both Zone.js and Zoneless setups. - -> This integration requires Angular 19 or later. Earlier versions of Angular do not support `PendingTasks`. +This means tests and SSR can wait until mutations and queries resolve. In unit tests you can use `ApplicationRef.whenStable()` or `fixture.whenStable()` to await query completion. The examples below use a zoneless TestBed setup. ## TestBed setup @@ -31,7 +29,7 @@ TestBed.configureTestingModule({ > If your applications actual TanStack Query config is used in unit tests, make sure `withDevtools` is not accidentally included in test providers. This can cause slow tests. It is best to keep test and production configs separate. -If you share helpers, remember to call `queryClient.clear()` (or build a new instance) in `afterEach` so data from one test never bleeds into another. +If you share helpers, remember to call `queryClient.clear()` (or build a new instance) in `afterEach` so data from one test never bleeds into another. Prefer creating a fresh `QueryClient` per test: clearing only removes cached data, not custom defaults or listeners, so a reused client can leak configuration changes between specs and make failures harder to reason about. A new client keeps setup explicit and avoids any “invisible globals” influencing results. ## First query test diff --git a/docs/framework/angular/installation.md b/docs/framework/angular/installation.md index dffc092e7c..14b48432f8 100644 --- a/docs/framework/angular/installation.md +++ b/docs/framework/angular/installation.md @@ -7,7 +7,7 @@ title: Installation ### NPM -_Angular Query is compatible with Angular v16 and higher_ +_Angular Query is compatible with Angular v19 and higher_ ```bash npm i @tanstack/angular-query-experimental diff --git a/docs/framework/angular/overview.md b/docs/framework/angular/overview.md index be68c08e5a..2eba20cab0 100644 --- a/docs/framework/angular/overview.md +++ b/docs/framework/angular/overview.md @@ -13,7 +13,7 @@ We are in the process of getting to a stable API for TanStack Query on Angular. ## Supported Angular Versions -TanStack Query is compatible with Angular v16 and higher. +TanStack Query is compatible with Angular v19 and higher. TanStack Query (FKA React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes **fetching, caching, synchronizing and updating server state** in your web applications a breeze. @@ -73,11 +73,10 @@ import { lastValueFrom } from 'rxjs' template: ` @if (query.isPending()) { Loading... - } - @if (query.error()) { + } @else if (query.isError()) { An error has occurred: {{ query.error().message }} - } - @if (query.data(); as data) { + } @else if (query.isSuccess()) { + @let data = query.data();

{{ data.name }}

{{ data.description }}

👀 {{ data.subscribers_count }} diff --git a/docs/framework/angular/quick-start.md b/docs/framework/angular/quick-start.md index b711f92b2b..41b246498d 100644 --- a/docs/framework/angular/quick-start.md +++ b/docs/framework/angular/quick-start.md @@ -35,7 +35,7 @@ import { @NgModule({ declarations: [AppComponent], imports: [BrowserModule], - providers: [provideTanStackQuery(new QueryClient())], + providers: [provideTanStackQuery(new QueryClient()), provideHttpClient()], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/docs/framework/angular/reference/functions/injectInfiniteQuery.md b/docs/framework/angular/reference/functions/injectInfiniteQuery.md index 020ef039fa..7e09345bc8 100644 --- a/docs/framework/angular/reference/functions/injectInfiniteQuery.md +++ b/docs/framework/angular/reference/functions/injectInfiniteQuery.md @@ -22,7 +22,7 @@ Additional configuration. function injectInfiniteQuery(injectInfiniteQueryFn, options?): DefinedCreateInfiniteQueryResult; ``` -Defined in: [inject-infinite-query.ts:41](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L41) +Defined in: [inject-infinite-query.ts:43](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L43) Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" @@ -75,7 +75,7 @@ The infinite query result. function injectInfiniteQuery(injectInfiniteQueryFn, options?): CreateInfiniteQueryResult; ``` -Defined in: [inject-infinite-query.ts:65](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L65) +Defined in: [inject-infinite-query.ts:67](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L67) Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" @@ -128,7 +128,7 @@ The infinite query result. function injectInfiniteQuery(injectInfiniteQueryFn, options?): CreateInfiniteQueryResult; ``` -Defined in: [inject-infinite-query.ts:89](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L89) +Defined in: [inject-infinite-query.ts:91](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L91) Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" diff --git a/docs/framework/angular/reference/functions/injectMutation.md b/docs/framework/angular/reference/functions/injectMutation.md index 5b4690eb46..8b2adfd525 100644 --- a/docs/framework/angular/reference/functions/injectMutation.md +++ b/docs/framework/angular/reference/functions/injectMutation.md @@ -9,7 +9,7 @@ title: injectMutation function injectMutation(injectMutationFn, options?): CreateMutationResult; ``` -Defined in: [inject-mutation.ts:45](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L45) +Defined in: [inject-mutation.ts:44](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L44) Injects a mutation: an imperative function that can be invoked which typically performs server side effects. diff --git a/docs/framework/angular/reference/functions/injectQueries.md b/docs/framework/angular/reference/functions/injectQueries.md new file mode 100644 index 0000000000..d05f95e379 --- /dev/null +++ b/docs/framework/angular/reference/functions/injectQueries.md @@ -0,0 +1,40 @@ +--- +id: injectQueries +title: injectQueries +--- + +# Function: injectQueries() + +```ts +function injectQueries(optionsFn, injector?): Signal; +``` + +Defined in: [inject-queries.ts:278](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L278) + +## Type Parameters + +### T + +`T` *extends* `any`[] + +### TCombinedResult + +`TCombinedResult` = `T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<`Head`\>\[`"data"`\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<`Head`\>\[`"data"`\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>\>, `GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<`Head`\>\[`"data"`\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GenericGetDefinedOrUndefinedQueryResult`\<`Head`, ...\[...\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<..., ...\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<..., ...\>\>, `GenericGetDefinedOrUndefinedQueryResult`\<`Head`, ...\[...\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<..., ...\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<..., ...\>\>, `GenericGetDefinedOrUndefinedQueryResult`\<`Head`, ...\[...\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<..., ...\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<..., ...\>\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : \[`...{ [K in (...)]: (...) }[]`\] : \[...\{ \[K in string \| number \| symbol\]: GenericGetDefinedOrUndefinedQueryResult\\], InferDataAndError\<(...)\>\["data"\], CreateQueryResult\<(...)\[(...)\], (...)\[(...)\]\>, DefinedCreateQueryResult\<(...)\[(...)\], (...)\[(...)\]\>\> \}\[\]\] : \{ \[K in string \| number \| symbol\]: GenericGetDefinedOrUndefinedQueryResult\\], InferDataAndError\\]\>\["data"\], CreateQueryResult\\]\>\["data"\], InferDataAndError\\]\>\["error"\]\>, DefinedCreateQueryResult\\]\>\["data"\], InferDataAndError\\]\>\["error"\]\>\> \} + +## Parameters + +### optionsFn + +() => [`InjectQueriesOptions`](../interfaces/InjectQueriesOptions.md)\<`T`, `TCombinedResult`\> + +A function that returns queries' options. + +### injector? + +`Injector` + +The Angular injector to use. + +## Returns + +`Signal`\<`TCombinedResult`\> diff --git a/docs/framework/angular/reference/functions/injectQuery.md b/docs/framework/angular/reference/functions/injectQuery.md index 8fa6832b09..63ffbeb92b 100644 --- a/docs/framework/angular/reference/functions/injectQuery.md +++ b/docs/framework/angular/reference/functions/injectQuery.md @@ -9,11 +9,15 @@ Injects a query: a declarative dependency on an asynchronous source of data that **Basic example** ```ts +import { lastValueFrom } from 'rxjs' + class ServiceOrComponent { query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => - this.#http.get('https://api.github.com/repos/tanstack/query'), + lastValueFrom( + this.#http.get('https://api.github.com/repos/tanstack/query'), + ), })) } ``` @@ -54,17 +58,21 @@ https://tanstack.com/query/latest/docs/framework/angular/guides/queries function injectQuery(injectQueryFn, options?): DefinedCreateQueryResult; ``` -Defined in: [inject-query.ts:65](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L65) +Defined in: [inject-query.ts:74](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L74) Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. **Basic example** ```ts +import { lastValueFrom } from 'rxjs' + class ServiceOrComponent { query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => - this.#http.get('https://api.github.com/repos/tanstack/query'), + lastValueFrom( + this.#http.get('https://api.github.com/repos/tanstack/query'), + ), })) } ``` @@ -135,17 +143,21 @@ https://tanstack.com/query/latest/docs/framework/angular/guides/queries function injectQuery(injectQueryFn, options?): CreateQueryResult; ``` -Defined in: [inject-query.ts:116](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L116) +Defined in: [inject-query.ts:129](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L129) Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. **Basic example** ```ts +import { lastValueFrom } from 'rxjs' + class ServiceOrComponent { query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => - this.#http.get('https://api.github.com/repos/tanstack/query'), + lastValueFrom( + this.#http.get('https://api.github.com/repos/tanstack/query'), + ), })) } ``` @@ -216,17 +228,21 @@ https://tanstack.com/query/latest/docs/framework/angular/guides/queries function injectQuery(injectQueryFn, options?): CreateQueryResult; ``` -Defined in: [inject-query.ts:167](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L167) +Defined in: [inject-query.ts:184](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L184) Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. **Basic example** ```ts +import { lastValueFrom } from 'rxjs' + class ServiceOrComponent { query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => - this.#http.get('https://api.github.com/repos/tanstack/query'), + lastValueFrom( + this.#http.get('https://api.github.com/repos/tanstack/query'), + ), })) } ``` @@ -271,7 +287,7 @@ class ServiceOrComponent { #### injectQueryFn -() => [`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> +() => [`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> A function that returns query options. diff --git a/docs/framework/angular/reference/functions/queryOptions.md b/docs/framework/angular/reference/functions/queryOptions.md index 34640111ec..70f1b3fb0e 100644 --- a/docs/framework/angular/reference/functions/queryOptions.md +++ b/docs/framework/angular/reference/functions/queryOptions.md @@ -30,10 +30,10 @@ The query options to tag with the type from `queryFn`. ## Call Signature ```ts -function queryOptions(options): Omit, "queryFn"> & object & object; +function queryOptions(options): CreateQueryOptions & object & object; ``` -Defined in: [query-options.ts:76](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L76) +Defined in: [query-options.ts:71](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L71) Allows to share and re-use query options in a type-safe way. @@ -81,7 +81,7 @@ The query options to tag with the type from `queryFn`. ### Returns -`Omit`\<[`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` +[`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> & `object` & `object` The tagged query options. @@ -91,7 +91,7 @@ The tagged query options. function queryOptions(options): OmitKeyof, "queryFn"> & object & object; ``` -Defined in: [query-options.ts:108](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L108) +Defined in: [query-options.ts:103](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L103) Allows to share and re-use query options in a type-safe way. @@ -139,7 +139,7 @@ The query options to tag with the type from `queryFn`. ### Returns -`OmitKeyof`\<[`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` +`OmitKeyof`\<[`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` The tagged query options. @@ -149,7 +149,7 @@ The tagged query options. function queryOptions(options): CreateQueryOptions & object & object; ``` -Defined in: [query-options.ts:140](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L140) +Defined in: [query-options.ts:135](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L135) Allows to share and re-use query options in a type-safe way. @@ -197,6 +197,6 @@ The query options to tag with the type from `queryFn`. ### Returns -[`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> & `object` & `object` +[`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> & `object` & `object` The tagged query options. diff --git a/docs/framework/angular/reference/index.md b/docs/framework/angular/reference/index.md index c74d256bce..16da72ce20 100644 --- a/docs/framework/angular/reference/index.md +++ b/docs/framework/angular/reference/index.md @@ -9,26 +9,27 @@ title: "@tanstack/angular-query-experimental" - [BaseMutationNarrowing](interfaces/BaseMutationNarrowing.md) - [BaseQueryNarrowing](interfaces/BaseQueryNarrowing.md) -- [CreateBaseQueryOptions](interfaces/CreateBaseQueryOptions.md) - [CreateInfiniteQueryOptions](interfaces/CreateInfiniteQueryOptions.md) - [CreateMutationOptions](interfaces/CreateMutationOptions.md) -- [CreateQueryOptions](interfaces/CreateQueryOptions.md) - [InjectInfiniteQueryOptions](interfaces/InjectInfiniteQueryOptions.md) - [InjectIsFetchingOptions](interfaces/InjectIsFetchingOptions.md) - [InjectIsMutatingOptions](interfaces/InjectIsMutatingOptions.md) - [InjectMutationOptions](interfaces/InjectMutationOptions.md) - [InjectMutationStateOptions](interfaces/InjectMutationStateOptions.md) +- [InjectQueriesOptions](interfaces/InjectQueriesOptions.md) - [InjectQueryOptions](interfaces/InjectQueryOptions.md) - [QueryFeature](interfaces/QueryFeature.md) ## Type Aliases - [CreateBaseMutationResult](type-aliases/CreateBaseMutationResult.md) +- [CreateBaseQueryOptions](type-aliases/CreateBaseQueryOptions.md) - [CreateBaseQueryResult](type-aliases/CreateBaseQueryResult.md) - [CreateInfiniteQueryResult](type-aliases/CreateInfiniteQueryResult.md) - [CreateMutateAsyncFunction](type-aliases/CreateMutateAsyncFunction.md) - [CreateMutateFunction](type-aliases/CreateMutateFunction.md) - [CreateMutationResult](type-aliases/CreateMutationResult.md) +- [CreateQueryOptions](type-aliases/CreateQueryOptions.md) - [CreateQueryResult](type-aliases/CreateQueryResult.md) - [DefinedCreateInfiniteQueryResult](type-aliases/DefinedCreateInfiniteQueryResult.md) - [DefinedCreateQueryResult](type-aliases/DefinedCreateQueryResult.md) @@ -53,6 +54,7 @@ title: "@tanstack/angular-query-experimental" - [injectIsRestoring](functions/injectIsRestoring.md) - [injectMutation](functions/injectMutation.md) - [injectMutationState](functions/injectMutationState.md) +- [injectQueries](functions/injectQueries.md) - [injectQuery](functions/injectQuery.md) - [~~injectQueryClient~~](functions/injectQueryClient.md) - [mutationOptions](functions/mutationOptions.md) diff --git a/docs/framework/angular/reference/interfaces/BaseQueryNarrowing.md b/docs/framework/angular/reference/interfaces/BaseQueryNarrowing.md index bc7811b6ae..24a00f39cb 100644 --- a/docs/framework/angular/reference/interfaces/BaseQueryNarrowing.md +++ b/docs/framework/angular/reference/interfaces/BaseQueryNarrowing.md @@ -5,7 +5,7 @@ title: BaseQueryNarrowing # Interface: BaseQueryNarrowing\ -Defined in: [types.ts:57](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L57) +Defined in: [types.ts:45](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L45) ## Type Parameters @@ -25,7 +25,7 @@ Defined in: [types.ts:57](https://github.com/TanStack/query/blob/main/packages/a isError: (this) => this is CreateBaseQueryResult>; ``` -Defined in: [types.ts:65](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L65) +Defined in: [types.ts:53](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L53) #### Parameters @@ -45,7 +45,7 @@ Defined in: [types.ts:65](https://github.com/TanStack/query/blob/main/packages/a isPending: (this) => this is CreateBaseQueryResult>; ``` -Defined in: [types.ts:72](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L72) +Defined in: [types.ts:60](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L60) #### Parameters @@ -65,7 +65,7 @@ Defined in: [types.ts:72](https://github.com/TanStack/query/blob/main/packages/a isSuccess: (this) => this is CreateBaseQueryResult>; ``` -Defined in: [types.ts:58](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L58) +Defined in: [types.ts:46](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L46) #### Parameters diff --git a/docs/framework/angular/reference/interfaces/CreateInfiniteQueryOptions.md b/docs/framework/angular/reference/interfaces/CreateInfiniteQueryOptions.md index ab21d5bc32..1f8883c8d7 100644 --- a/docs/framework/angular/reference/interfaces/CreateInfiniteQueryOptions.md +++ b/docs/framework/angular/reference/interfaces/CreateInfiniteQueryOptions.md @@ -5,7 +5,7 @@ title: CreateInfiniteQueryOptions # Interface: CreateInfiniteQueryOptions\ -Defined in: [types.ts:81](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L81) +Defined in: [types.ts:69](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L69) ## Extends diff --git a/docs/framework/angular/reference/interfaces/CreateQueryOptions.md b/docs/framework/angular/reference/interfaces/CreateQueryOptions.md deleted file mode 100644 index 113fbbc5d2..0000000000 --- a/docs/framework/angular/reference/interfaces/CreateQueryOptions.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -id: CreateQueryOptions -title: CreateQueryOptions ---- - -# Interface: CreateQueryOptions\ - -Defined in: [types.ts:35](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L35) - -## Extends - -- `OmitKeyof`\<[`CreateBaseQueryOptions`](CreateBaseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryFnData`, `TQueryKey`\>, `"suspense"`\> - -## Type Parameters - -### TQueryFnData - -`TQueryFnData` = `unknown` - -### TError - -`TError` = `DefaultError` - -### TData - -`TData` = `TQueryFnData` - -### TQueryKey - -`TQueryKey` *extends* `QueryKey` = `QueryKey` diff --git a/docs/framework/angular/reference/interfaces/InjectInfiniteQueryOptions.md b/docs/framework/angular/reference/interfaces/InjectInfiniteQueryOptions.md index 3b552aa381..33880ffae2 100644 --- a/docs/framework/angular/reference/interfaces/InjectInfiniteQueryOptions.md +++ b/docs/framework/angular/reference/interfaces/InjectInfiniteQueryOptions.md @@ -5,7 +5,7 @@ title: InjectInfiniteQueryOptions # Interface: InjectInfiniteQueryOptions -Defined in: [inject-infinite-query.ts:25](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L25) +Defined in: [inject-infinite-query.ts:27](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L27) ## Properties @@ -15,7 +15,7 @@ Defined in: [inject-infinite-query.ts:25](https://github.com/TanStack/query/blob optional injector: Injector; ``` -Defined in: [inject-infinite-query.ts:31](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L31) +Defined in: [inject-infinite-query.ts:33](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L33) The `Injector` in which to create the infinite query. diff --git a/docs/framework/angular/reference/interfaces/InjectMutationOptions.md b/docs/framework/angular/reference/interfaces/InjectMutationOptions.md index 0638baa372..c313951b25 100644 --- a/docs/framework/angular/reference/interfaces/InjectMutationOptions.md +++ b/docs/framework/angular/reference/interfaces/InjectMutationOptions.md @@ -5,7 +5,7 @@ title: InjectMutationOptions # Interface: InjectMutationOptions -Defined in: [inject-mutation.ts:28](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L28) +Defined in: [inject-mutation.ts:27](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L27) ## Properties @@ -15,7 +15,7 @@ Defined in: [inject-mutation.ts:28](https://github.com/TanStack/query/blob/main/ optional injector: Injector; ``` -Defined in: [inject-mutation.ts:34](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L34) +Defined in: [inject-mutation.ts:33](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L33) The `Injector` in which to create the mutation. diff --git a/docs/framework/angular/reference/interfaces/InjectQueriesOptions.md b/docs/framework/angular/reference/interfaces/InjectQueriesOptions.md new file mode 100644 index 0000000000..c9f860b872 --- /dev/null +++ b/docs/framework/angular/reference/interfaces/InjectQueriesOptions.md @@ -0,0 +1,50 @@ +--- +id: InjectQueriesOptions +title: InjectQueriesOptions +--- + +# Interface: InjectQueriesOptions\ + +Defined in: [inject-queries.ts:257](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L257) + +## Type Parameters + +### T + +`T` *extends* `any`[] + +### TCombinedResult + +`TCombinedResult` = [`QueriesResults`](../type-aliases/QueriesResults.md)\<`T`\> + +## Properties + +### combine()? + +```ts +optional combine: (result) => TCombinedResult; +``` + +Defined in: [inject-queries.ts:266](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L266) + +#### Parameters + +##### result + +`T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<`Head`\>\[`"data"`\], `QueryObserverResult`\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>, `DefinedQueryObserverResult`\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<...\>\[`"data"`\], `QueryObserverResult`\<...\[...\], ...\[...\]\>, `DefinedQueryObserverResult`\<...\[...\], ...\[...\]\>\>, `GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<...\>\[`"data"`\], `QueryObserverResult`\<...\[...\], ...\[...\]\>, `DefinedQueryObserverResult`\<...\[...\], ...\[...\]\>\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...(...)[]`\] *extends* \[...\] ? \[..., ..., ...\] : ... *extends* ... ? ... : ... : \[...\{ \[K in (...) \| (...) \| (...)\]: GenericGetDefinedOrUndefinedQueryResult\<(...), (...), (...), (...)\> \}\[\]\] : \{ \[K in string \| number \| symbol\]: GenericGetDefinedOrUndefinedQueryResult\\], InferDataAndError\\]\>\["data"\], QueryObserverResult\\["data"\], InferDataAndError\<(...)\[(...)\]\>\["error"\]\>, DefinedQueryObserverResult\\["data"\], InferDataAndError\<(...)\[(...)\]\>\["error"\]\>\> \} + +#### Returns + +`TCombinedResult` + +*** + +### queries + +```ts +queries: + | readonly [{ [K in string | number | symbol]: GetCreateQueryOptionsForCreateQueries]> }] + | readonly [T extends [] ? [] : T extends [Head] ? [GetCreateQueryOptionsForCreateQueries] : T extends [Head, ...Tails[]] ? [...Tails[]] extends [] ? [] : [...Tails[]] extends [Head] ? [GetCreateQueryOptionsForCreateQueries, GetCreateQueryOptionsForCreateQueries] : [...Tails[]] extends [Head, ...Tails[]] ? [...Tails[]] extends [] ? [] : [...(...)[]] extends [...] ? [..., ..., ...] : ... extends ... ? ... : ... : readonly unknown[] extends [...Tails[]] ? [...Tails[]] : [...(...)[]] extends ...[] ? ...[] : ...[] : readonly unknown[] extends T ? T : T extends QueryObserverOptionsForCreateQueries[] ? QueryObserverOptionsForCreateQueries[] : QueryObserverOptionsForCreateQueries[]]; +``` + +Defined in: [inject-queries.ts:261](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L261) diff --git a/docs/framework/angular/reference/interfaces/InjectQueryOptions.md b/docs/framework/angular/reference/interfaces/InjectQueryOptions.md index eecbef2804..8f513c31b5 100644 --- a/docs/framework/angular/reference/interfaces/InjectQueryOptions.md +++ b/docs/framework/angular/reference/interfaces/InjectQueryOptions.md @@ -5,7 +5,7 @@ title: InjectQueryOptions # Interface: InjectQueryOptions -Defined in: [inject-query.ts:20](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L20) +Defined in: [inject-query.ts:25](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L25) ## Properties @@ -15,7 +15,7 @@ Defined in: [inject-query.ts:20](https://github.com/TanStack/query/blob/main/pac optional injector: Injector; ``` -Defined in: [inject-query.ts:26](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L26) +Defined in: [inject-query.ts:31](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L31) The `Injector` in which to create the query. diff --git a/docs/framework/angular/reference/interfaces/CreateBaseQueryOptions.md b/docs/framework/angular/reference/type-aliases/CreateBaseQueryOptions.md similarity index 63% rename from docs/framework/angular/reference/interfaces/CreateBaseQueryOptions.md rename to docs/framework/angular/reference/type-aliases/CreateBaseQueryOptions.md index 48a4b7dcc6..29bacc3ac0 100644 --- a/docs/framework/angular/reference/interfaces/CreateBaseQueryOptions.md +++ b/docs/framework/angular/reference/type-aliases/CreateBaseQueryOptions.md @@ -3,13 +3,13 @@ id: CreateBaseQueryOptions title: CreateBaseQueryOptions --- -# Interface: CreateBaseQueryOptions\ +# Type Alias: CreateBaseQueryOptions\ -Defined in: [types.ts:21](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L21) - -## Extends +```ts +type CreateBaseQueryOptions = QueryObserverOptions; +``` -- `QueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryData`, `TQueryKey`\> +Defined in: [types.ts:21](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L21) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/CreateBaseQueryResult.md b/docs/framework/angular/reference/type-aliases/CreateBaseQueryResult.md index 784f89c5e1..0ed7f2524b 100644 --- a/docs/framework/angular/reference/type-aliases/CreateBaseQueryResult.md +++ b/docs/framework/angular/reference/type-aliases/CreateBaseQueryResult.md @@ -6,10 +6,10 @@ title: CreateBaseQueryResult # Type Alias: CreateBaseQueryResult\ ```ts -type CreateBaseQueryResult = BaseQueryNarrowing & MapToSignals>; +type CreateBaseQueryResult = BaseQueryNarrowing & MapToSignals, MethodKeys>>; ``` -Defined in: [types.ts:98](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L98) +Defined in: [types.ts:86](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L86) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/CreateInfiniteQueryResult.md b/docs/framework/angular/reference/type-aliases/CreateInfiniteQueryResult.md index f4c01a674b..fc9a51d4b0 100644 --- a/docs/framework/angular/reference/type-aliases/CreateInfiniteQueryResult.md +++ b/docs/framework/angular/reference/type-aliases/CreateInfiniteQueryResult.md @@ -6,10 +6,10 @@ title: CreateInfiniteQueryResult # Type Alias: CreateInfiniteQueryResult\ ```ts -type CreateInfiniteQueryResult = BaseQueryNarrowing & MapToSignals>; +type CreateInfiniteQueryResult = BaseQueryNarrowing & MapToSignals, MethodKeys>>; ``` -Defined in: [types.ts:117](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L117) +Defined in: [types.ts:111](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L111) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/CreateMutationResult.md b/docs/framework/angular/reference/type-aliases/CreateMutationResult.md index b5573544d0..86c2056181 100644 --- a/docs/framework/angular/reference/type-aliases/CreateMutationResult.md +++ b/docs/framework/angular/reference/type-aliases/CreateMutationResult.md @@ -6,7 +6,7 @@ title: CreateMutationResult # Type Alias: CreateMutationResult\ ```ts -type CreateMutationResult = BaseMutationNarrowing & MapToSignals>; +type CreateMutationResult = BaseMutationNarrowing & MapToSignals, MethodKeys>>; ``` Defined in: [types.ts:266](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L266) diff --git a/docs/framework/angular/reference/type-aliases/CreateQueryOptions.md b/docs/framework/angular/reference/type-aliases/CreateQueryOptions.md new file mode 100644 index 0000000000..799c2d3b11 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/CreateQueryOptions.md @@ -0,0 +1,30 @@ +--- +id: CreateQueryOptions +title: CreateQueryOptions +--- + +# Type Alias: CreateQueryOptions\ + +```ts +type CreateQueryOptions = OmitKeyof, "suspense">; +``` + +Defined in: [types.ts:29](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L29) + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` diff --git a/docs/framework/angular/reference/type-aliases/CreateQueryResult.md b/docs/framework/angular/reference/type-aliases/CreateQueryResult.md index c532a87463..67d2f6cd86 100644 --- a/docs/framework/angular/reference/type-aliases/CreateQueryResult.md +++ b/docs/framework/angular/reference/type-aliases/CreateQueryResult.md @@ -9,7 +9,7 @@ title: CreateQueryResult type CreateQueryResult = CreateBaseQueryResult; ``` -Defined in: [types.ts:105](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L105) +Defined in: [types.ts:96](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L96) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/DefinedCreateInfiniteQueryResult.md b/docs/framework/angular/reference/type-aliases/DefinedCreateInfiniteQueryResult.md index 932114c7d1..6760031992 100644 --- a/docs/framework/angular/reference/type-aliases/DefinedCreateInfiniteQueryResult.md +++ b/docs/framework/angular/reference/type-aliases/DefinedCreateInfiniteQueryResult.md @@ -6,10 +6,10 @@ title: DefinedCreateInfiniteQueryResult # Type Alias: DefinedCreateInfiniteQueryResult\ ```ts -type DefinedCreateInfiniteQueryResult = MapToSignals; +type DefinedCreateInfiniteQueryResult = MapToSignals>; ``` -Defined in: [types.ts:123](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L123) +Defined in: [types.ts:120](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L120) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/DefinedCreateQueryResult.md b/docs/framework/angular/reference/type-aliases/DefinedCreateQueryResult.md index 60fa877491..9ef090ccf8 100644 --- a/docs/framework/angular/reference/type-aliases/DefinedCreateQueryResult.md +++ b/docs/framework/angular/reference/type-aliases/DefinedCreateQueryResult.md @@ -6,10 +6,10 @@ title: DefinedCreateQueryResult # Type Alias: DefinedCreateQueryResult\ ```ts -type DefinedCreateQueryResult = BaseQueryNarrowing & MapToSignals>; +type DefinedCreateQueryResult = BaseQueryNarrowing & MapToSignals, MethodKeys>>; ``` -Defined in: [types.ts:110](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L110) +Defined in: [types.ts:101](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L101) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/DefinedInitialDataOptions.md b/docs/framework/angular/reference/type-aliases/DefinedInitialDataOptions.md index 4bcea1da72..4c8ad5c0ab 100644 --- a/docs/framework/angular/reference/type-aliases/DefinedInitialDataOptions.md +++ b/docs/framework/angular/reference/type-aliases/DefinedInitialDataOptions.md @@ -6,10 +6,10 @@ title: DefinedInitialDataOptions # Type Alias: DefinedInitialDataOptions\ ```ts -type DefinedInitialDataOptions = Omit, "queryFn"> & object; +type DefinedInitialDataOptions = CreateQueryOptions & object; ``` -Defined in: [query-options.ts:40](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L40) +Defined in: [query-options.ts:39](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L39) ## Type Declaration @@ -21,12 +21,6 @@ initialData: | () => NonUndefinedGuard; ``` -### queryFn? - -```ts -optional queryFn: QueryFunction; -``` - ## Type Parameters ### TQueryFnData diff --git a/docs/framework/angular/reference/type-aliases/QueriesOptions.md b/docs/framework/angular/reference/type-aliases/QueriesOptions.md index 2def13c9c9..c8f95bb1a7 100644 --- a/docs/framework/angular/reference/type-aliases/QueriesOptions.md +++ b/docs/framework/angular/reference/type-aliases/QueriesOptions.md @@ -9,7 +9,7 @@ title: QueriesOptions type QueriesOptions = TDepth["length"] extends MAXIMUM_DEPTH ? QueryObserverOptionsForCreateQueries[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryOptionsForCreateQueries] : T extends [infer Head, ...(infer Tails)] ? QueriesOptions<[...Tails], [...TResults, GetCreateQueryOptionsForCreateQueries], [...TDepth, 1]> : ReadonlyArray extends T ? T : T extends QueryObserverOptionsForCreateQueries[] ? QueryObserverOptionsForCreateQueries[] : QueryObserverOptionsForCreateQueries[]; ``` -Defined in: [inject-queries.ts:144](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L144) +Defined in: [inject-queries.ts:178](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L178) QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param diff --git a/docs/framework/angular/reference/type-aliases/QueriesResults.md b/docs/framework/angular/reference/type-aliases/QueriesResults.md index 6d5ecf6dd4..b34b39cb60 100644 --- a/docs/framework/angular/reference/type-aliases/QueriesResults.md +++ b/docs/framework/angular/reference/type-aliases/QueriesResults.md @@ -9,7 +9,7 @@ title: QueriesResults type QueriesResults = TDepth["length"] extends MAXIMUM_DEPTH ? CreateQueryResult[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryResult] : T extends [infer Head, ...(infer Tails)] ? QueriesResults<[...Tails], [...TResults, GetCreateQueryResult], [...TDepth, 1]> : { [K in keyof T]: GetCreateQueryResult }; ``` -Defined in: [inject-queries.ts:186](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L186) +Defined in: [inject-queries.ts:220](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L220) QueriesResults reducer recursively maps type param to results diff --git a/docs/framework/angular/reference/type-aliases/UndefinedInitialDataOptions.md b/docs/framework/angular/reference/type-aliases/UndefinedInitialDataOptions.md index f1a48e74e6..339c1e5684 100644 --- a/docs/framework/angular/reference/type-aliases/UndefinedInitialDataOptions.md +++ b/docs/framework/angular/reference/type-aliases/UndefinedInitialDataOptions.md @@ -9,7 +9,7 @@ title: UndefinedInitialDataOptions type UndefinedInitialDataOptions = CreateQueryOptions & object; ``` -Defined in: [query-options.ts:13](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L13) +Defined in: [query-options.ts:12](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L12) ## Type Declaration diff --git a/docs/framework/angular/reference/type-aliases/UnusedSkipTokenOptions.md b/docs/framework/angular/reference/type-aliases/UnusedSkipTokenOptions.md index 9a65d5b3f3..bebe298be7 100644 --- a/docs/framework/angular/reference/type-aliases/UnusedSkipTokenOptions.md +++ b/docs/framework/angular/reference/type-aliases/UnusedSkipTokenOptions.md @@ -9,7 +9,7 @@ title: UnusedSkipTokenOptions type UnusedSkipTokenOptions = OmitKeyof, "queryFn"> & object; ``` -Defined in: [query-options.ts:25](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L25) +Defined in: [query-options.ts:24](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L24) ## Type Declaration diff --git a/docs/framework/angular/typescript.md b/docs/framework/angular/typescript.md index 0aa2ff2638..0dea29de35 100644 --- a/docs/framework/angular/typescript.md +++ b/docs/framework/angular/typescript.md @@ -12,6 +12,7 @@ replace: 'React Query': 'TanStack Query', '`success`': '`isSuccess()`', 'function:': 'function.', + 'separate function': 'separate function or a service', } --- @@ -52,7 +53,7 @@ class MyComponent { [//]: # 'TypeInference2' [//]: # 'TypeInference3' -In this example we pass Group[] to the type parameter of HttpClient's `get` method. +In this example we pass `Group[]` to the type parameter of HttpClient's `get` method. ```angular-ts @Component({ @@ -70,6 +71,7 @@ class MyComponent { ``` [//]: # 'TypeInference3' +[//]: # 'TypeInference4' [//]: # 'TypeNarrowing' ```angular-ts @@ -90,8 +92,9 @@ class MyComponent { } ``` -> TypeScript currently does not support discriminated unions on object methods. Narrowing on signal fields on objects such as query results only works on signals returning a boolean. Prefer using `isSuccess()` and similar boolean status signals over `status() === 'success'`. +> TypeScript currently does not support discriminated unions on object methods. Narrowing on signal fields on objects such as query results only works on signals returning a boolean. Prefer using `isSuccess()`, `isError()` and `isPending()` over `status() === 'success'`. +[//]: # 'TypeInference4' [//]: # 'TypeNarrowing' [//]: # 'TypingError' @@ -153,8 +156,7 @@ import '@tanstack/angular-query-experimental' declare module '@tanstack/angular-query-experimental' { interface Register { - // Use unknown so call sites must narrow explicitly. - defaultError: unknown + defaultError: AxiosError } } @@ -165,7 +167,7 @@ const query = injectQuery(() => ({ computed(() => { const error = query.error() - // ^? error: unknown | null + // ^? error: AxiosError | null }) ``` diff --git a/examples/angular/infinite-query-with-max-pages/src/app/components/example.component.ts b/examples/angular/infinite-query-with-max-pages/src/app/components/example.component.ts index 71c141e3e4..3232f64942 100644 --- a/examples/angular/infinite-query-with-max-pages/src/app/components/example.component.ts +++ b/examples/angular/infinite-query-with-max-pages/src/app/components/example.component.ts @@ -30,30 +30,25 @@ export class ExampleComponent { })) readonly nextButtonDisabled = computed( - () => !this.#hasNextPage() || this.#isFetchingNextPage(), + () => !this.query.hasNextPage() || this.query.isFetchingNextPage(), ) readonly nextButtonText = computed(() => - this.#isFetchingNextPage() + this.query.isFetchingNextPage() ? 'Loading more...' - : this.#hasNextPage() + : this.query.hasNextPage() ? 'Load newer' : 'Nothing more to load', ) readonly previousButtonDisabled = computed( - () => !this.#hasPreviousPage() || this.#isFetchingNextPage(), + () => !this.query.hasPreviousPage() || this.query.isFetchingPreviousPage(), ) readonly previousButtonText = computed(() => - this.#isFetchingPreviousPage() + this.query.isFetchingPreviousPage() ? 'Loading more...' - : this.#hasPreviousPage() + : this.query.hasPreviousPage() ? 'Load Older' : 'Nothing more to load', ) - - readonly #hasPreviousPage = this.query.hasPreviousPage - readonly #hasNextPage = this.query.hasNextPage - readonly #isFetchingPreviousPage = this.query.isFetchingPreviousPage - readonly #isFetchingNextPage = this.query.isFetchingNextPage } diff --git a/examples/angular/optimistic-updates/src/app/components/optimistic-updates.component.ts b/examples/angular/optimistic-updates/src/app/components/optimistic-updates.component.ts index 2b0b4cc1c4..b32a0f50dc 100644 --- a/examples/angular/optimistic-updates/src/app/components/optimistic-updates.component.ts +++ b/examples/angular/optimistic-updates/src/app/components/optimistic-updates.component.ts @@ -36,7 +36,7 @@ import { TasksService } from '../services/tasks.service'
    - @for (task of tasks.data(); track task) { + @for (task of tasks.data(); track $index) {
  • {{ task }}
  • }
diff --git a/packages/angular-query-experimental/README.md b/packages/angular-query-experimental/README.md index 6ed2dfa05a..7a60650d6d 100644 --- a/packages/angular-query-experimental/README.md +++ b/packages/angular-query-experimental/README.md @@ -29,7 +29,7 @@ Visit https://tanstack.com/query/latest/docs/framework/angular/overview # Quick Start -> The Angular adapter for TanStack Query requires Angular 16 or higher. +> The Angular adapter for TanStack Query requires Angular 19 or higher. 1. Install `angular-query` @@ -78,9 +78,10 @@ import { @NgModule({ declarations: [AppComponent], imports: [BrowserModule], - providers: [provideTanStackQuery(new QueryClient())], + providers: [provideTanStackQuery(new QueryClient()), provideHttpClient()], bootstrap: [AppComponent], }) +export class AppModule {} ``` 3. Inject query diff --git a/packages/angular-query-experimental/package.json b/packages/angular-query-experimental/package.json index 594ac9840a..11afc14ac4 100644 --- a/packages/angular-query-experimental/package.json +++ b/packages/angular-query-experimental/package.json @@ -31,11 +31,6 @@ "compile": "tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "npm-run-all --serial test:types:*", - "test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js --build", - "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js --build", - "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js --build", - "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build", - "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build", @@ -93,20 +88,22 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", + "@angular/platform-server": "^20.0.0", "@tanstack/query-test-utils": "workspace:*", "@testing-library/angular": "^18.0.0", "npm-run-all2": "^5.0.0", "rxjs": "^7.8.2", "vite-plugin-dts": "4.2.3", "vite-plugin-externalize-deps": "^0.9.0", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "zone.js": "^0.16.0" }, "optionalDependencies": { "@tanstack/query-devtools": "workspace:*" }, "peerDependencies": { - "@angular/common": ">=16.0.0", - "@angular/core": ">=16.0.0" + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0" }, "publishConfig": { "directory": "dist", diff --git a/packages/angular-query-experimental/src/__tests__/inject-devtools-panel.test.ts b/packages/angular-query-experimental/src/__tests__/inject-devtools-panel.test.ts index 3a1fc4bca8..048e063b40 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-devtools-panel.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-devtools-panel.test.ts @@ -1,13 +1,9 @@ -import { - ElementRef, - provideZonelessChangeDetection, - signal, -} from '@angular/core' +import { ElementRef, signal } from '@angular/core' import { TestBed } from '@angular/core/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { QueryClient } from '@tanstack/query-core' -import { provideTanStackQuery } from '../providers' import { injectDevtoolsPanel } from '../devtools-panel' +import { setupTanStackQueryTestBed } from './test-utils' const mockDevtoolsPanelInstance = { mount: vi.fn(), @@ -45,12 +41,8 @@ describe('injectDevtoolsPanel', () => { vi.clearAllMocks() queryClient = new QueryClient() mockElementRef = new ElementRef(document.createElement('div')) - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - { provide: ElementRef, useValue: signal(mockElementRef) }, - ], + setupTanStackQueryTestBed(queryClient, { + providers: [{ provide: ElementRef, useValue: signal(mockElementRef) }], }) }) diff --git a/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts b/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts index 7ec133adfb..c34c580ddc 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts @@ -1,42 +1,97 @@ -import { TestBed } from '@angular/core/testing' -import { afterEach, beforeEach, describe, expectTypeOf, test, vi } from 'vitest' -import { provideZonelessChangeDetection } from '@angular/core' -import { sleep } from '@tanstack/query-test-utils' -import { QueryClient, injectInfiniteQuery, provideTanStackQuery } from '..' +import { describe, expectTypeOf, it, test } from 'vitest' +import { injectInfiniteQuery } from '..' +import type { Signal } from '@angular/core' import type { InfiniteData } from '@tanstack/query-core' describe('injectInfiniteQuery', () => { - let queryClient: QueryClient - - beforeEach(() => { - queryClient = new QueryClient() - vi.useFakeTimers() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - ], + describe('Discriminated union return type', () => { + test('data should be possibly undefined by default', () => { + const query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], + queryFn: ({ pageParam }) => + Promise.resolve('data on page ' + pageParam), + initialPageParam: 0, + getNextPageParam: () => 12, + })) + + expectTypeOf(query.data).toEqualTypeOf< + Signal | Signal> + >() }) - }) - afterEach(() => { - vi.useRealTimers() - }) + test('data should be defined when query is success', () => { + const query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], + queryFn: ({ pageParam }) => + Promise.resolve('data on page ' + pageParam), + initialPageParam: 0, + getNextPageParam: () => 12, + })) + + if (query.isSuccess()) { + expectTypeOf(query.data).toEqualTypeOf< + Signal> + >() + } + }) + + test('error should be null when query is success', () => { + const query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], + queryFn: ({ pageParam }) => + Promise.resolve('data on page ' + pageParam), + initialPageParam: 0, + getNextPageParam: () => 12, + })) - test('should narrow type after isSuccess', () => { - const query = TestBed.runInInjectionContext(() => { - return injectInfiniteQuery(() => ({ + if (query.isSuccess()) { + expectTypeOf(query.error).toEqualTypeOf>() + } + }) + + test('data should be undefined when query is pending', () => { + const query = injectInfiniteQuery(() => ({ queryKey: ['infiniteQuery'], queryFn: ({ pageParam }) => - sleep(0).then(() => 'data on page ' + pageParam), + Promise.resolve('data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, })) + + if (query.isPending()) { + expectTypeOf(query.data).toEqualTypeOf>() + } }) - if (query.isSuccess()) { - const data = query.data() - expectTypeOf(data).toEqualTypeOf>() - } + test('error should be defined when query is error', () => { + const query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], + queryFn: ({ pageParam }) => + Promise.resolve('data on page ' + pageParam), + initialPageParam: 0, + getNextPageParam: () => 12, + })) + + if (query.isError()) { + expectTypeOf(query.error).toEqualTypeOf>() + } + }) + }) + + it('should provide the correct types to the select function', () => { + const query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], + queryFn: ({ pageParam }) => Promise.resolve('data on page ' + pageParam), + initialPageParam: 0, + getNextPageParam: () => 12, + select: (data) => { + expectTypeOf(data).toEqualTypeOf>() + return data + }, + })) + + expectTypeOf(query.data).toEqualTypeOf< + Signal | Signal> + >() }) }) diff --git a/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts b/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts index 7873d5261c..f587f6a2fe 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts @@ -1,9 +1,9 @@ import { TestBed } from '@angular/core/testing' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { Injector, provideZonelessChangeDetection } from '@angular/core' +import { ChangeDetectionStrategy, Component, Injector } from '@angular/core' import { sleep } from '@tanstack/query-test-utils' -import { QueryClient, injectInfiniteQuery, provideTanStackQuery } from '..' -import { expectSignals } from './test-utils' +import { QueryClient, injectInfiniteQuery } from '..' +import { expectSignals, setupTanStackQueryTestBed } from './test-utils' describe('injectInfiniteQuery', () => { let queryClient: QueryClient @@ -11,12 +11,7 @@ describe('injectInfiniteQuery', () => { beforeEach(() => { queryClient = new QueryClient() vi.useFakeTimers() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - ], - }) + setupTanStackQueryTestBed(queryClient) }) afterEach(() => { @@ -24,15 +19,24 @@ describe('injectInfiniteQuery', () => { }) test('should properly execute infinite query', async () => { - const query = TestBed.runInInjectionContext(() => { - return injectInfiniteQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectInfiniteQuery(() => ({ queryKey: ['infiniteQuery'], queryFn: ({ pageParam }) => sleep(10).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expectSignals(query, { data: undefined, @@ -76,6 +80,9 @@ describe('injectInfiniteQuery', () => { }) test('can be used outside injection context when passing an injector', () => { + const injector = TestBed.inject(Injector) + + // Call injectInfiniteQuery directly outside any component const query = injectInfiniteQuery( () => ({ queryKey: ['manualInjector'], @@ -85,10 +92,12 @@ describe('injectInfiniteQuery', () => { getNextPageParam: () => 12, }), { - injector: TestBed.inject(Injector), + injector: injector, }, ) + TestBed.tick() + expect(query.status()).toBe('pending') }) }) 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 329ef6d9e3..a7461dbc26 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,13 +1,9 @@ import { TestBed } from '@angular/core/testing' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { Injector, provideZonelessChangeDetection } from '@angular/core' +import { Injector } from '@angular/core' import { sleep } from '@tanstack/query-test-utils' -import { - QueryClient, - injectIsFetching, - injectQuery, - provideTanStackQuery, -} from '..' +import { QueryClient, injectIsFetching, injectQuery } from '..' +import { setupTanStackQueryTestBed } from './test-utils' describe('injectIsFetching', () => { let queryClient: QueryClient @@ -16,12 +12,7 @@ describe('injectIsFetching', () => { vi.useFakeTimers() queryClient = new QueryClient() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - ], - }) + setupTanStackQueryTestBed(queryClient) }) afterEach(() => { diff --git a/packages/angular-query-experimental/src/__tests__/inject-is-mutating.test.ts b/packages/angular-query-experimental/src/__tests__/inject-is-mutating.test.ts index 5a4694cb85..6d30b988f4 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-is-mutating.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-is-mutating.test.ts @@ -1,13 +1,9 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { TestBed } from '@angular/core/testing' -import { Injector, provideZonelessChangeDetection } from '@angular/core' +import { Injector } from '@angular/core' import { sleep } from '@tanstack/query-test-utils' -import { - QueryClient, - injectIsMutating, - injectMutation, - provideTanStackQuery, -} from '..' +import { QueryClient, injectIsMutating, injectMutation } from '..' +import { flushQueryUpdates, setupTanStackQueryTestBed } from './test-utils' describe('injectIsMutating', () => { let queryClient: QueryClient @@ -16,12 +12,7 @@ describe('injectIsMutating', () => { vi.useFakeTimers() queryClient = new QueryClient() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - ], - }) + setupTanStackQueryTestBed(queryClient) }) afterEach(() => { @@ -44,7 +35,7 @@ describe('injectIsMutating', () => { }) expect(isMutating()).toBe(0) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() expect(isMutating()).toBe(1) await vi.advanceTimersByTimeAsync(11) expect(isMutating()).toBe(0) diff --git a/packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts b/packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts index 8b747f66f6..e8c86c068f 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts @@ -1,4 +1,5 @@ import { + ChangeDetectionStrategy, Component, Injector, input, @@ -7,7 +8,6 @@ import { } from '@angular/core' import { TestBed } from '@angular/core/testing' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { By } from '@angular/platform-browser' import { sleep } from '@tanstack/query-test-utils' import { QueryClient, @@ -15,7 +15,7 @@ import { injectMutationState, provideTanStackQuery, } from '..' -import { setFixtureSignalInputs } from './test-utils' +import { registerSignalInput } from './test-utils' describe('injectMutationState', () => { let queryClient: QueryClient @@ -145,6 +145,7 @@ describe('injectMutationState', () => { {{ mutation.status }} } `, + changeDetection: ChangeDetectionStrategy.OnPush, }) class FakeComponent { name = input.required() @@ -157,23 +158,35 @@ describe('injectMutationState', () => { })) } - const fixture = TestBed.createComponent(FakeComponent) - const { debugElement } = fixture - setFixtureSignalInputs(fixture, { name: fakeName }) + registerSignalInput(FakeComponent, 'name') + + @Component({ + template: ``, + imports: [FakeComponent], + }) + class HostComponent { + protected readonly name = signal(fakeName) + } + + const fixture = TestBed.createComponent(HostComponent) + fixture.detectChanges() await vi.advanceTimersByTimeAsync(0) - let spans = debugElement - .queryAll(By.css('span')) - .map((span) => span.nativeNode.textContent) + const readSpans = () => + Array.from( + fixture.nativeElement.querySelectorAll( + 'span', + ) as NodeListOf, + ).map((span) => span.textContent) + + let spans = readSpans() expect(spans).toEqual(['pending', 'pending']) await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() - spans = debugElement - .queryAll(By.css('span')) - .map((span) => span.nativeNode.textContent) + spans = readSpans() expect(spans).toEqual(['success', 'error']) }) diff --git a/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts b/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts index 2adf0ee808..3daa657356 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts @@ -1,17 +1,19 @@ import { ApplicationRef, + ChangeDetectionStrategy, Component, Injector, + NgZone, input, provideZonelessChangeDetection, signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { By } from '@angular/platform-browser' import { sleep } from '@tanstack/query-test-utils' +import { firstValueFrom } from 'rxjs' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { QueryClient, injectMutation, provideTanStackQuery } from '..' -import { expectSignals, setFixtureSignalInputs } from './test-utils' +import { expectSignals, registerSignalInput } from './test-utils' describe('injectMutation', () => { let queryClient: QueryClient @@ -307,6 +309,7 @@ describe('injectMutation', () => { {{ mutation.data() }} `, + changeDetection: ChangeDetectionStrategy.OnPush, }) class FakeComponent { name = input.required() @@ -321,19 +324,32 @@ describe('injectMutation', () => { } } - const fixture = TestBed.createComponent(FakeComponent) - const { debugElement } = fixture - setFixtureSignalInputs(fixture, { name: 'value' }) + registerSignalInput(FakeComponent, 'name') - const button = debugElement.query(By.css('button')) - button.triggerEventHandler('click') + @Component({ + template: ``, + imports: [FakeComponent], + }) + class HostComponent { + protected readonly name = signal('value') + } + + const fixture = TestBed.createComponent(HostComponent) + fixture.detectChanges() + + const hostButton = fixture.nativeElement.querySelector( + 'button', + ) as HTMLButtonElement + hostButton.click() await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() - const text = debugElement.query(By.css('span')).nativeElement.textContent - expect(text).toEqual('value') - const mutation = mutationCache.find({ mutationKey: ['fake', 'value'] }) + const span = fixture.nativeElement.querySelector('span') as HTMLSpanElement + expect(span.textContent).toEqual('value') + const mutation = mutationCache.find({ + mutationKey: ['fake', 'value'], + }) expect(mutation).toBeDefined() expect(mutation!.options.mutationKey).toStrictEqual(['fake', 'value']) }) @@ -347,6 +363,7 @@ describe('injectMutation', () => { {{ mutation.data() }} `, + changeDetection: ChangeDetectionStrategy.OnPush, }) class FakeComponent { name = input.required() @@ -361,26 +378,43 @@ describe('injectMutation', () => { } } - const fixture = TestBed.createComponent(FakeComponent) - const { debugElement } = fixture - setFixtureSignalInputs(fixture, { name: 'value' }) + registerSignalInput(FakeComponent, 'name') - const button = debugElement.query(By.css('button')) - const span = debugElement.query(By.css('span')) + @Component({ + template: ``, + imports: [FakeComponent], + }) + class HostComponent { + protected readonly name = signal('value') - button.triggerEventHandler('click') + updateName(value: string): void { + this.name.set(value) + } + } + + const fixture = TestBed.createComponent(HostComponent) + fixture.detectChanges() + + let button = fixture.nativeElement.querySelector( + 'button', + ) as HTMLButtonElement + button.click() await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() - expect(span.nativeElement.textContent).toEqual('value') + let span = fixture.nativeElement.querySelector('span') as HTMLSpanElement + expect(span.textContent).toEqual('value') - setFixtureSignalInputs(fixture, { name: 'updatedValue' }) + fixture.componentInstance.updateName('updatedValue') + fixture.detectChanges() - button.triggerEventHandler('click') + button = fixture.nativeElement.querySelector('button') as HTMLButtonElement + button.click() await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() - expect(span.nativeElement.textContent).toEqual('updatedValue') + span = fixture.nativeElement.querySelector('span') as HTMLSpanElement + expect(span.textContent).toEqual('updatedValue') const mutations = mutationCache.findAll() expect(mutations.length).toBe(2) @@ -412,6 +446,36 @@ describe('injectMutation', () => { expect(boundaryFn).toHaveBeenCalledTimes(1) expect(boundaryFn).toHaveBeenCalledWith(err) }) + + test('should emit zone error when throwOnError is true and mutate is used', async () => { + const err = new Error('Expected mock error. All is well!') + const zone = TestBed.inject(NgZone) + const zoneErrorEmitSpy = vi.spyOn(zone.onError, 'emit') + const runSpy = vi.spyOn(zone, 'run').mockImplementation((callback: any) => { + try { + return callback() + } catch { + return undefined + } + }) + + const { mutate } = TestBed.runInInjectionContext(() => + injectMutation(() => ({ + mutationKey: ['fake'], + mutationFn: () => { + return sleep(0).then(() => Promise.reject(err)) + }, + throwOnError: true, + })), + ) + + mutate() + + await vi.runAllTimersAsync() + + expect(zoneErrorEmitSpy).toHaveBeenCalledWith(err) + expect(runSpy).toHaveBeenCalled() + }) }) test('should throw when throwOnError is true', async () => { @@ -625,12 +689,13 @@ describe('injectMutation', () => { const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ - mutationFn: async (data: string) => `final: ${data}`, // Synchronous resolution + mutationFn: async (data: string) => { + await sleep(50) + return `final: ${data}` + }, onMutate: async (variables) => { onMutateCalled = true - const previousData = queryClient.getQueryData(testQueryKey) queryClient.setQueryData(testQueryKey, `optimistic: ${variables}`) - return { previousData } }, onSuccess: (data) => { onSuccessCalled = true @@ -639,19 +704,30 @@ describe('injectMutation', () => { })), ) + // Run effects + TestBed.tick() + // Start mutation + expect(queryClient.getQueryData(testQueryKey)).toBe('initial') mutation.mutate('test') - // Synchronize pending effects - TestBed.tick() - - const stablePromise = app.whenStable() // Flush microtasks to allow TanStack Query's scheduled notifications to process await Promise.resolve() - await vi.advanceTimersByTimeAsync(1) - await stablePromise + // Check for optimistic update in the same macro task expect(onMutateCalled).toBe(true) + expect(queryClient.getQueryData(testQueryKey)).toBe('optimistic: test') + + // Check stability before the mutation completes, waiting for the next macro task + await vi.advanceTimersByTimeAsync(0) + expect(mutation.isPending()).toBe(true) + expect(await firstValueFrom(app.isStable)).toBe(false) + + // Wait for the mutation to complete + const stablePromise = app.whenStable() + await vi.advanceTimersByTimeAsync(60) + await stablePromise + expect(onSuccessCalled).toBe(true) expect(mutation.isSuccess()).toBe(true) expect(mutation.data()).toBe('final: test') diff --git a/packages/angular-query-experimental/src/__tests__/inject-queries.test-d.ts b/packages/angular-query-experimental/src/__tests__/inject-queries.test-d.ts index 62547fd9e0..6c2e373e44 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-queries.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-queries.test-d.ts @@ -1,6 +1,5 @@ import { describe, expectTypeOf, it } from 'vitest' -import { skipToken } from '..' -import { injectQueries } from '../inject-queries' +import { injectQueries, skipToken } from '..' import { queryOptions } from '../query-options' import type { CreateQueryOptions, CreateQueryResult, OmitKeyof } from '..' import type { Signal } from '@angular/core' @@ -175,3 +174,88 @@ describe('InjectQueries config object overload', () => { >() }) }) + +describe('InjectQueries combine', () => { + it('should provide the correct type for the combine function', () => { + injectQueries(() => ({ + queries: [ + { + queryKey: ['key'], + queryFn: () => Promise.resolve(1), + }, + { + queryKey: ['key2'], + queryFn: () => Promise.resolve(true), + }, + ], + combine: (results) => { + expectTypeOf(results[0].data).toEqualTypeOf() + expectTypeOf(results[0].refetch).toBeCallableWith() + expectTypeOf(results[1].data).toEqualTypeOf() + expectTypeOf(results[1].refetch).toBeCallableWith() + }, + })) + }) + + it('should provide the correct types on the combined result with initial data', () => { + injectQueries(() => ({ + queries: [ + { + queryKey: ['key'], + queryFn: () => Promise.resolve(1), + initialData: 1, + }, + ], + combine: (results) => { + expectTypeOf(results[0].data).toEqualTypeOf() + expectTypeOf(results[0].refetch).toBeCallableWith() + }, + })) + }) + + it('should provide the correct result type', () => { + const queryResults = injectQueries(() => ({ + queries: [ + { + queryKey: ['key'], + queryFn: () => Promise.resolve(1), + }, + { + queryKey: ['key2'], + queryFn: () => Promise.resolve(true), + }, + ], + combine: (results) => ({ + data: { + 1: results[0].data, + 2: results[1].data, + }, + fn: () => {}, + }), + })) + + expectTypeOf(queryResults).branded.toEqualTypeOf< + Signal<{ + data: { + 1: number | undefined + 2: boolean | undefined + } + fn: () => void + }> + >() + }) + + it('should provide the correct types on the result with initial data', () => { + const queryResults = injectQueries(() => ({ + queries: [ + { + queryKey: ['key'], + queryFn: () => Promise.resolve(1), + initialData: 1, + }, + ], + })) + + expectTypeOf(queryResults()[0].data()).toEqualTypeOf() + }) +}) diff --git a/packages/angular-query-experimental/src/__tests__/inject-queries.test.ts b/packages/angular-query-experimental/src/__tests__/inject-queries.test.ts index 3fb3d5a626..3007a8db86 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-queries.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-queries.test.ts @@ -1,28 +1,64 @@ -import { beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@testing-library/angular' import { + ApplicationRef, + ChangeDetectionStrategy, Component, + Injector, + computed, effect, - provideZonelessChangeDetection, + input, + signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' -import { queryKey } from '@tanstack/query-test-utils' -import { QueryClient, provideTanStackQuery } from '..' +import { queryKey, sleep } from '@tanstack/query-test-utils' +import { QueryClient, provideIsRestoring } from '..' import { injectQueries } from '../inject-queries' +import { registerSignalInput, setupTanStackQueryTestBed } from './test-utils' let queryClient: QueryClient beforeEach(() => { queryClient = new QueryClient() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - ], - }) + vi.useFakeTimers({ shouldAdvanceTime: true }) + setupTanStackQueryTestBed(queryClient) +}) + +afterEach(() => { + vi.useRealTimers() }) describe('injectQueries', () => { + it('throws NG0203 with descriptive error outside injection context', () => { + expect(() => { + injectQueries(() => ({ + queries: [ + { + queryKey: ['injectionContextError'], + queryFn: () => Promise.resolve(1), + }, + ], + })) + }).toThrowError(/NG0203(.*?)injectQueries/) + }) + + it('can be used outside injection context when passing an injector', () => { + const injector = TestBed.inject(Injector) + const queries = injectQueries( + () => ({ + queries: [ + { + queryKey: ['manualInjector'], + queryFn: () => Promise.resolve(1), + }, + ], + }), + injector, + ) + + expect(queries()[0].status()).toBe('pending') + }) + it('should return the correct states', async () => { const key1 = queryKey() const key2 = queryKey() @@ -32,17 +68,18 @@ describe('injectQueries', () => { template: `
- data1: {{ result()[0].data() ?? 'null' }}, data2: - {{ result()[1].data() ?? 'null' }} + data1: {{ queries()[0].data() ?? 'null' }}, data2: + {{ queries()[1].data() ?? 'null' }}
`, + changeDetection: ChangeDetectionStrategy.OnPush, }) class Page { toString(val: any) { return String(val) } - result = injectQueries(() => ({ + queries = injectQueries(() => ({ queries: [ { queryKey: key1, @@ -62,7 +99,7 @@ describe('injectQueries', () => { })) _pushResults = effect(() => { - const snapshot = this.result().map((q) => ({ data: q.data() })) + const snapshot = this.queries().map((q) => ({ data: q.data() })) results.push(snapshot) }) } @@ -76,4 +113,548 @@ describe('injectQueries', () => { expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }]) expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) }) + + it('should support combining results', async () => { + const key1 = queryKey() + const key2 = queryKey() + let count = 0 + + const results: Array<{ data: string; refetch: () => void }> = [] + + @Component({ + template: `
data: {{ queries().data }}
`, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: [ + { + queryKey: key1, + queryFn: async () => { + await new Promise((r) => setTimeout(r, 10)) + count++ + return count + }, + }, + { + queryKey: key2, + queryFn: async () => { + await new Promise((r) => setTimeout(r, 100)) + count++ + return count + }, + }, + ], + combine: (queryResults) => { + return { + refetch: () => queryResults.forEach((r) => r.refetch()), + data: queryResults.map((r) => r.data).join(','), + } + }, + })) + + _pushResults = effect(() => { + results.push(this.queries()) + }) + } + + const rendered = await render(Page) + const instance = rendered.fixture.componentInstance + await rendered.findByText('data: 1,2') + expect(instance.queries().data).toBe('1,2') + + instance.queries().refetch() + + await rendered.findByText('data: 3,4') + expect(instance.queries().data).toBe('3,4') + + expect(results).toHaveLength(5) + expect(results[0]).toMatchObject({ + data: ',', + refetch: expect.any(Function), + }) + expect(results[1]).toMatchObject({ + data: '1,', + refetch: expect.any(Function), + }) + expect(results[2]).toMatchObject({ + data: '1,2', + refetch: expect.any(Function), + }) + expect(results[3]).toMatchObject({ + data: '3,2', + refetch: expect.any(Function), + }) + expect(results[4]).toMatchObject({ + data: '3,4', + refetch: expect.any(Function), + }) + }) + + it('should handle mixed success and error query states', async () => { + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['mixed-error'], + retry: false, + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + throw new Error('mixed-error') + }, + }, + { + queryKey: ['mixed-success'], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 20)) + return 'mixed-success' + }, + }, + ], + })) + } + + const rendered = await render(Page) + await vi.advanceTimersByTimeAsync(25) + await Promise.resolve() + + const [errorQuery, successQuery] = rendered.fixture.componentInstance.queries() + expect(errorQuery.status()).toBe('error') + expect(errorQuery.error()?.message).toBe('mixed-error') + expect(successQuery.status()).toBe('success') + expect(successQuery.data()).toBe('mixed-success') + }) + + it('should cleanup pending tasks when component with active queries is destroyed', async () => { + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['destroy-query-1'], + queryFn: async () => { + await sleep(100) + return 'one' + }, + }, + { + queryKey: ['destroy-query-2'], + queryFn: async () => { + await sleep(100) + return 'two' + }, + }, + ], + })) + } + + // Use a fixture here on purpose: we need component teardown + whenStable() semantics. + const fixture = TestBed.createComponent(Page) + fixture.detectChanges() + expect(fixture.isStable()).toBe(false) + + fixture.destroy() + + const stablePromise = fixture.whenStable() + await vi.advanceTimersByTimeAsync(150) + await stablePromise + + expect(fixture.isStable()).toBe(true) + }) + + it('should react to enabled signal changes', async () => { + const enabled = signal(false) + const fetchSpy = vi.fn(() => sleep(10).then(() => 'enabled-data')) + + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + enabled = enabled + fetchSpy = fetchSpy + + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['enabled', this.enabled()], + queryFn: this.fetchSpy, + enabled: this.enabled(), + }, + ], + })) + } + + const rendered = await render(Page) + const query = rendered.fixture.componentInstance.queries()[0] + + expect(fetchSpy).not.toHaveBeenCalled() + expect(query.status()).toBe('pending') + + enabled.set(true) + rendered.fixture.detectChanges() + await vi.advanceTimersByTimeAsync(11) + await Promise.resolve() + + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(query.status()).toBe('success') + expect(query.data()).toBe('enabled-data') + }) + + it('should refetch only changed keys when queries length stays the same', async () => { + const ids = signal<[string, string]>(['a', 'b']) + const firstSpy = vi.fn((context: any) => + sleep(10).then(() => `first-${context.queryKey[1]}`), + ) + const secondSpy = vi.fn((context: any) => + sleep(10).then(() => `second-${context.queryKey[1]}`), + ) + + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + ids = ids + firstSpy = firstSpy + secondSpy = secondSpy + + queries = injectQueries(() => ({ + queries: [ + { + staleTime: Number.POSITIVE_INFINITY, + queryKey: ['first', this.ids()[0]], + queryFn: this.firstSpy, + }, + { + staleTime: Number.POSITIVE_INFINITY, + queryKey: ['second', this.ids()[1]], + queryFn: this.secondSpy, + }, + ], + })) + } + + const rendered = await render(Page) + await vi.advanceTimersByTimeAsync(11) + await Promise.resolve() + + let [firstQuery, secondQuery] = rendered.fixture.componentInstance.queries() + expect(firstQuery.data()).toBe('first-a') + expect(secondQuery.data()).toBe('second-b') + expect(firstSpy).toHaveBeenCalledTimes(1) + expect(secondSpy).toHaveBeenCalledTimes(1) + + ids.set(['c', 'b']) + rendered.fixture.detectChanges() + await vi.advanceTimersByTimeAsync(11) + await Promise.resolve() + + ;[firstQuery, secondQuery] = rendered.fixture.componentInstance.queries() + expect(firstQuery.data()).toBe('first-c') + expect(secondQuery.data()).toBe('second-b') + expect(firstSpy).toHaveBeenCalledTimes(2) + expect(secondSpy).toHaveBeenCalledTimes(1) + }) + + it('should support changes on the queries array', async () => { + const results: Array>> = [] + + @Component({ + template: `
data: {{ mapped() }}
`, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: queries().map((q) => ({ + queryKey: ['query', q], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 20 * q)) + return q + }, + })), + })) + + mapped = computed(() => { + const queryData = this.queries().map((q) => q.data()) + if (queryData.length === 0) return 'empty' + return queryData.join(',') + }) + + _pushResults = effect(() => { + const snapshot = this.queries().map((q) => ({ data: q.data() })) + results.push(snapshot) + }) + } + + const queries = signal([1, 2, 4]) + + const rendered = await render(Page) + const instance = rendered.fixture.componentInstance + + await rendered.findByText('data: 1,2,4') + expect(instance.mapped()).toBe('1,2,4') + + expect(results.length).toBe(4) + expect(results[0]).toMatchObject([ + { data: undefined }, + { data: undefined }, + { data: undefined }, + ]) + expect(results[1]).toMatchObject([ + { data: 1 }, + { data: undefined }, + { data: undefined }, + ]) + expect(results[2]).toMatchObject([ + { data: 1 }, + { data: 2 }, + { data: undefined }, + ]) + expect(results[3]).toMatchObject([{ data: 1 }, { data: 2 }, { data: 4 }]) + + queries.set([3, 4]) + await rendered.findByText('data: 3,4') + expect(instance.mapped()).toBe('3,4') + + const hasOptimisticTransition = results.some( + (snapshot) => + snapshot.length === 2 && + snapshot[0]?.data === undefined && + snapshot[1]?.data === 4, + ) + expect(hasOptimisticTransition).toBe(true) + expect(results[results.length - 1]).toMatchObject([{ data: 3 }, { data: 4 }]) + + queries.set([]) + await rendered.findByText('data: empty') + expect(instance.mapped()).toBe('empty') + + expect(results[results.length - 1]).toMatchObject([]) + }) + + it('should change the rendered component when the queries array changes', async () => { + const userIds = signal([1, 2]) + + @Component({ + template: ` +
    + @for (query of queries(); track $index) { + @if (query.data(); as data) { +
  • {{ data.value }}
  • + } + } +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + userIds = userIds + + queries = injectQueries(() => ({ + queries: this.userIds().map((id) => ({ + queryKey: ['user', id], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 20)) + return { value: String(id) } + }, + })), + })) + } + + const rendered = await render(Page) + + await rendered.findByText('1') + await rendered.findByText('2') + + userIds.set([3]) + rendered.fixture.detectChanges() + + await rendered.findByText('3') + expect(rendered.queryByText('1')).toBeNull() + expect(rendered.queryByText('2')).toBeNull() + }) + + it('should support required signal inputs', async () => { + @Component({ + selector: 'app-fake', + template: `{{ queries()[0].data() }}`, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class FakeComponent { + name = input.required() + + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['fake', this.name()], + queryFn: () => this.name(), + }, + ], + })) + } + + registerSignalInput(FakeComponent, 'name') + + @Component({ + template: ``, + imports: [FakeComponent], + }) + class HostComponent { + protected readonly name = signal('signal-input-required-test') + } + + const fixture = TestBed.createComponent(HostComponent) + fixture.detectChanges() + await vi.advanceTimersByTimeAsync(0) + + const result = fixture.nativeElement.querySelector('app-fake').textContent + expect(result).toEqual('signal-input-required-test') + }) + + it('should pause fetching while restoring and fetch once restoring is disabled', async () => { + const isRestoring = signal(true) + const fetchSpy = vi.fn(() => sleep(10).then(() => 'restored-data')) + setupTanStackQueryTestBed(queryClient, { + providers: [provideIsRestoring(isRestoring.asReadonly())], + }) + + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['restoring'], + queryFn: fetchSpy, + }, + ], + })) + } + + const fixture = TestBed.createComponent(Page) + fixture.detectChanges() + + expect(fetchSpy).not.toHaveBeenCalled() + expect(fixture.componentInstance.queries()[0].status()).toBe('pending') + + const stablePromise = fixture.whenStable() + await Promise.resolve() + await stablePromise + + isRestoring.set(false) + fixture.detectChanges() + + await vi.advanceTimersByTimeAsync(11) + await fixture.whenStable() + + const result = fixture.componentInstance.queries()[0] + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(result.status()).toBe('success') + expect(result.data()).toBe('restored-data') + }) + + it('should complete queries before whenStable resolves', async () => { + const app = TestBed.inject(ApplicationRef) + + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['query-1'], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return 1 + }, + }, + { + queryKey: ['query-2'], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 20)) + return 2 + }, + }, + ], + })) + } + + const fixture = TestBed.createComponent(Page) + fixture.detectChanges() + + const stablePromise = app.whenStable() + let stableResolved = false + void stablePromise.then(() => { + stableResolved = true + }) + + await Promise.resolve() + expect(stableResolved).toBe(false) + + await vi.advanceTimersByTimeAsync(25) + await stablePromise + + const result = fixture.componentInstance.queries() + expect(result[0].status()).toBe('success') + expect(result[1].status()).toBe('success') + expect(result[0].data()).toBe(1) + expect(result[1].data()).toBe(2) + }) + + it('should use latest query key for aliased refetch function', async () => { + const key = signal('one') + const fetchSpy = vi.fn(async (context: any) => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return context.queryKey[1] + }) + + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + key = key + fetchSpy = fetchSpy + + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['query', this.key()], + queryFn: this.fetchSpy, + enabled: false, + }, + ], + })) + } + + const rendered = await render(Page) + const query = rendered.fixture.componentInstance.queries()[0] + const refetch = query.refetch + + key.set('two') + rendered.fixture.detectChanges() + + const refetchPromise = refetch() + await vi.advanceTimersByTimeAsync(15) + await refetchPromise + + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(fetchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ['query', 'two'], + }), + ) + }) }) diff --git a/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts b/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts index 541ad65f14..353890bff4 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts @@ -33,6 +33,18 @@ describe('initialData', () => { expectTypeOf(data).toEqualTypeOf>() }) + it('should support selection function with select', () => { + const options = injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => '1', + select: (data) => { + expectTypeOf(data).toEqualTypeOf() + return parseInt(data) + }, + })) + expectTypeOf(options.data).toEqualTypeOf>() + }) + it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { const options = queryOptions({ queryKey: ['key'], diff --git a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts index 39d38140e3..e90054121b 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts @@ -1,7 +1,9 @@ import { ApplicationRef, + ChangeDetectionStrategy, Component, Injector, + NgZone, computed, effect, input, @@ -25,8 +27,14 @@ import { } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { lastValueFrom } from 'rxjs' -import { QueryCache, QueryClient, injectQuery, provideTanStackQuery } from '..' -import { setSignalInputs } from './test-utils' +import { + QueryCache, + QueryClient, + injectQuery, + provideIsRestoring, + provideTanStackQuery, +} from '..' +import { registerSignalInput } from './test-utils' import type { CreateQueryOptions, OmitKeyof, QueryFunction } from '..' describe('injectQuery', () => { @@ -50,102 +58,190 @@ describe('injectQuery', () => { test('should return the correct types', () => { const key = queryKey() - // unspecified query function should default to unknown - const noQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + // unspecified query function should default to unknown + noQueryFn = injectQuery(() => ({ queryKey: key, - })), - ) - expectTypeOf(noQueryFn.data()).toEqualTypeOf() - expectTypeOf(noQueryFn.error()).toEqualTypeOf() + })) - // it should infer the result type from the query function - const fromQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should infer the result type from the query function + fromQueryFn = injectQuery(() => ({ queryKey: key, queryFn: () => 'test', - })), - ) - expectTypeOf(fromQueryFn.data()).toEqualTypeOf() - expectTypeOf(fromQueryFn.error()).toEqualTypeOf() + })) - // it should be possible to specify the result type - const withResult = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should be possible to specify the result type + withResult = injectQuery(() => ({ queryKey: key, queryFn: () => 'test', - })), - ) - expectTypeOf(withResult.data()).toEqualTypeOf() - expectTypeOf(withResult.error()).toEqualTypeOf() + })) - // it should be possible to specify the error type - type CustomErrorType = { message: string } - const withError = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should be possible to specify the error type + withError = injectQuery(() => ({ queryKey: key, queryFn: () => 'test', - })), - ) - expectTypeOf(withError.data()).toEqualTypeOf() - expectTypeOf(withError.error()).toEqualTypeOf() + })) - // it should infer the result type from the configuration - const withResultInfer = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should infer the result type from the configuration + withResultInfer = injectQuery(() => ({ queryKey: key, queryFn: () => true, - })), - ) - expectTypeOf(withResultInfer.data()).toEqualTypeOf() - expectTypeOf(withResultInfer.error()).toEqualTypeOf() + })) - // it should be possible to specify a union type as result type - const unionTypeSync = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should be possible to specify a union type as result type + unionTypeSync = injectQuery(() => ({ queryKey: key, queryFn: () => (Math.random() > 0.5 ? ('a' as const) : ('b' as const)), - })), - ) - expectTypeOf(unionTypeSync.data()).toEqualTypeOf<'a' | 'b' | undefined>() - const unionTypeAsync = TestBed.runInInjectionContext(() => - injectQuery<'a' | 'b'>(() => ({ + })) + + unionTypeAsync = injectQuery<'a' | 'b'>(() => ({ queryKey: key, queryFn: () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'), - })), - ) - expectTypeOf(unionTypeAsync.data()).toEqualTypeOf<'a' | 'b' | undefined>() + })) - // it should error when the query function result does not match with the specified type - TestBed.runInInjectionContext(() => - // @ts-expect-error - injectQuery(() => ({ queryKey: key, queryFn: () => 'test' })), - ) + // it should infer the result type from a generic query function + fromGenericQueryFn = (() => { + function queryFn(): Promise { + return Promise.resolve({} as T) + } + return injectQuery(() => ({ + queryKey: key, + queryFn: () => queryFn(), + })) + })() - // it should infer the result type from a generic query function - /** - * - */ - function queryFn(): Promise { - return Promise.resolve({} as T) - } + // todo use query options? + fromGenericOptionsQueryFn = (() => { + function queryFn(): Promise { + return Promise.resolve({} as T) + } + return injectQuery(() => ({ + queryKey: key, + queryFn: () => queryFn(), + })) + })() + + fromMyDataArrayKeyQueryFn = (() => { + type MyData = number + type MyQueryKey = readonly ['my-data', number] + const getMyDataArrayKey: QueryFunction = ({ + queryKey: [, n], + }) => { + return n + 42 + } + return injectQuery(() => ({ + queryKey: ['my-data', 100] as const, + queryFn: getMyDataArrayKey, + })) + })() - const fromGenericQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should handle query-functions that return Promise + fromPromiseAnyQueryFn = injectQuery(() => ({ queryKey: key, - queryFn: () => queryFn(), - })), - ) + queryFn: () => fetch('return Promise').then((resp) => resp.json()), + })) + + fromGetMyDataStringKeyQueryFn = (() => { + type MyData = number + const getMyDataStringKey: QueryFunction = (context) => { + expectTypeOf(context.queryKey).toEqualTypeOf<['1']>() + return Number(context.queryKey[0]) + 42 + } + return injectQuery(() => ({ + queryKey: ['1'] as ['1'], + queryFn: getMyDataStringKey, + })) + })() + + // Wrapped queries + fromWrappedQuery = (() => { + const createWrappedQuery = < + TQueryKey extends [string, Record?], + TQueryFnData, + TError, + TData = TQueryFnData, + >( + qk: TQueryKey, + fetcher: (obj: TQueryKey[1], token: string) => Promise, + options?: OmitKeyof< + CreateQueryOptions, + 'queryKey' | 'queryFn' | 'initialData', + 'safely' + >, + ) => + injectQuery(() => ({ + queryKey: qk, + queryFn: () => fetcher(qk[1], 'token'), + ...options, + })) + return createWrappedQuery([''], () => Promise.resolve('1')) + })() + + fromWrappedFuncStyleQuery = (() => { + const createWrappedFuncStyleQuery = < + TQueryKey extends [string, Record?], + TQueryFnData, + TError, + TData = TQueryFnData, + >( + qk: TQueryKey, + fetcher: () => Promise, + options?: OmitKeyof< + CreateQueryOptions, + 'queryKey' | 'queryFn' | 'initialData', + 'safely' + >, + ) => injectQuery(() => ({ queryKey: qk, queryFn: fetcher, ...options })) + return createWrappedFuncStyleQuery([''], () => Promise.resolve(true)) + })() + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const { + noQueryFn, + fromQueryFn, + withResult, + withError, + withResultInfer, + unionTypeSync, + unionTypeAsync, + fromGenericQueryFn, + fromGenericOptionsQueryFn, + fromMyDataArrayKeyQueryFn, + fromPromiseAnyQueryFn, + fromGetMyDataStringKeyQueryFn, + fromWrappedQuery, + fromWrappedFuncStyleQuery, + } = fixture.componentInstance + + expectTypeOf(noQueryFn.data()).toEqualTypeOf() + expectTypeOf(noQueryFn.error()).toEqualTypeOf() + + expectTypeOf(fromQueryFn.data()).toEqualTypeOf() + expectTypeOf(fromQueryFn.error()).toEqualTypeOf() + + expectTypeOf(withResult.data()).toEqualTypeOf() + expectTypeOf(withResult.error()).toEqualTypeOf() + + expectTypeOf(withError.data()).toEqualTypeOf() + expectTypeOf(withError.error()).toEqualTypeOf<{ message: string } | null>() + + expectTypeOf(withResultInfer.data()).toEqualTypeOf() + expectTypeOf(withResultInfer.error()).toEqualTypeOf() + + expectTypeOf(unionTypeSync.data()).toEqualTypeOf<'a' | 'b' | undefined>() + expectTypeOf(unionTypeAsync.data()).toEqualTypeOf<'a' | 'b' | undefined>() + expectTypeOf(fromGenericQueryFn.data()).toEqualTypeOf() expectTypeOf(fromGenericQueryFn.error()).toEqualTypeOf() - // todo use query options? - const fromGenericOptionsQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key, - queryFn: () => queryFn(), - })), - ) expectTypeOf(fromGenericOptionsQueryFn.data()).toEqualTypeOf< string | undefined >() @@ -153,120 +249,36 @@ describe('injectQuery', () => { fromGenericOptionsQueryFn.error(), ).toEqualTypeOf() - type MyData = number - type MyQueryKey = readonly ['my-data', number] - - const getMyDataArrayKey: QueryFunction = ({ - queryKey: [, n], - }) => { - return n + 42 - } - - const fromMyDataArrayKeyQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: ['my-data', 100] as const, - queryFn: getMyDataArrayKey, - })), - ) expectTypeOf(fromMyDataArrayKeyQueryFn.data()).toEqualTypeOf< number | undefined >() - // it should handle query-functions that return Promise - const fromPromiseAnyQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key, - queryFn: () => fetch('return Promise').then((resp) => resp.json()), - })), - ) expectTypeOf(fromPromiseAnyQueryFn.data()).toEqualTypeOf() - - TestBed.runInInjectionContext(() => - effect(() => { - expect(fromMyDataArrayKeyQueryFn.data()).toBe(142) - }), - ) - - const getMyDataStringKey: QueryFunction = (context) => { - expectTypeOf(context.queryKey).toEqualTypeOf<['1']>() - return Number(context.queryKey[0]) + 42 - } - - const fromGetMyDataStringKeyQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: ['1'] as ['1'], - queryFn: getMyDataStringKey, - })), - ) expectTypeOf(fromGetMyDataStringKeyQueryFn.data()).toEqualTypeOf< number | undefined >() - - TestBed.runInInjectionContext(() => - effect(() => { - expect(fromGetMyDataStringKeyQueryFn.data()).toBe(43) - }), - ) - - // handles wrapped queries with custom fetcher passed as inline queryFn - const createWrappedQuery = < - TQueryKey extends [string, Record?], - TQueryFnData, - TError, - TData = TQueryFnData, - >( - qk: TQueryKey, - fetcher: ( - obj: TQueryKey[1], - token: string, - // return type must be wrapped with TQueryFnReturn - ) => Promise, - options?: OmitKeyof< - CreateQueryOptions, - 'queryKey' | 'queryFn' | 'initialData', - 'safely' - >, - ) => - injectQuery(() => ({ - queryKey: qk, - queryFn: () => fetcher(qk[1], 'token'), - ...options, - })) - const fromWrappedQuery = TestBed.runInInjectionContext(() => - createWrappedQuery([''], () => Promise.resolve('1')), - ) expectTypeOf(fromWrappedQuery.data()).toEqualTypeOf() - - // handles wrapped queries with custom fetcher passed directly to createQuery - const createWrappedFuncStyleQuery = < - TQueryKey extends [string, Record?], - TQueryFnData, - TError, - TData = TQueryFnData, - >( - qk: TQueryKey, - fetcher: () => Promise, - options?: OmitKeyof< - CreateQueryOptions, - 'queryKey' | 'queryFn' | 'initialData', - 'safely' - >, - ) => injectQuery(() => ({ queryKey: qk, queryFn: fetcher, ...options })) - const fromWrappedFuncStyleQuery = TestBed.runInInjectionContext(() => - createWrappedFuncStyleQuery([''], () => Promise.resolve(true)), - ) expectTypeOf(fromWrappedFuncStyleQuery.data()).toEqualTypeOf< boolean | undefined >() }) test('should return pending status initially', () => { - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ queryKey: ['key1'], queryFn: () => sleep(10).then(() => 'Some data'), })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(query.status()).toBe('pending') expect(query.isPending()).toBe(true) @@ -276,12 +288,21 @@ describe('injectQuery', () => { }) test('should resolve to success and update signal: injectQuery()', async () => { - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ queryKey: ['key2'], queryFn: () => sleep(10).then(() => 'result2'), })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query await vi.advanceTimersByTimeAsync(11) expect(query.status()).toBe('success') @@ -293,14 +314,23 @@ describe('injectQuery', () => { }) test('should reject and update signal', async () => { - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ retry: false, queryKey: ['key3'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query await vi.advanceTimersByTimeAsync(11) expect(query.status()).toBe('error') @@ -317,12 +347,23 @@ describe('injectQuery', () => { const key = signal(['key6', 'key7']) const spy = vi.fn(() => sleep(10).then(() => 'Some data')) - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: key(), - queryFn: spy, - })) + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, }) + class TestComponent { + key = key + spy = spy + query = injectQuery(() => ({ + queryKey: this.key(), + queryFn: this.spy, + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query await vi.advanceTimersByTimeAsync(0) expect(spy).toHaveBeenCalledTimes(1) @@ -331,7 +372,7 @@ describe('injectQuery', () => { expect(query.status()).toBe('success') key.set(['key8']) - TestBed.tick() + fixture.detectChanges() expect(spy).toHaveBeenCalledTimes(2) // should call queryFn with context containing the new queryKey @@ -347,13 +388,24 @@ describe('injectQuery', () => { const spy = vi.fn(() => sleep(10).then(() => 'Some data')) const enabled = signal(false) - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + enabled = enabled + spy = spy + query = injectQuery(() => ({ queryKey: ['key9'], - queryFn: spy, - enabled: enabled(), + queryFn: this.spy, + enabled: this.enabled(), })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(spy).not.toHaveBeenCalled() expect(query.status()).toBe('pending') @@ -366,26 +418,33 @@ describe('injectQuery', () => { }) test('should properly execute dependant queries', async () => { - const query1 = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: ['dependant1'], - queryFn: () => sleep(10).then(() => 'Some data'), - })) - }) - const dependentQueryFn = vi .fn() .mockImplementation(() => sleep(1000).then(() => 'Some data')) - const query2 = TestBed.runInInjectionContext(() => { - return injectQuery( + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query1 = injectQuery(() => ({ + queryKey: ['dependant1'], + queryFn: () => sleep(10).then(() => 'Some data'), + })) + + query2 = injectQuery( computed(() => ({ queryKey: ['dependant2'], queryFn: dependentQueryFn, - enabled: !!query1.data(), + enabled: !!this.query1.data(), })), ) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const { query1, query2 } = fixture.componentInstance expect(query1.data()).toStrictEqual(undefined) expect(query2.fetchStatus()).toStrictEqual('idle') @@ -410,13 +469,24 @@ describe('injectQuery', () => { const fetchFn = vi.fn(() => sleep(10).then(() => 'Some data')) const keySignal = signal('key11') - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: ['key10', keySignal()], - queryFn: fetchFn, + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + keySignal = keySignal + fetchFn = fetchFn + query = injectQuery(() => ({ + queryKey: ['key10', this.keySignal()], + queryFn: this.fetchFn, enabled: false, })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(fetchFn).not.toHaveBeenCalled() @@ -432,6 +502,7 @@ describe('injectQuery', () => { await vi.advanceTimersByTimeAsync(11) keySignal.set('key12') + fixture.detectChanges() void query.refetch().then(() => { expect(fetchFn).toHaveBeenCalledTimes(2) @@ -445,18 +516,58 @@ describe('injectQuery', () => { await vi.advanceTimersByTimeAsync(11) }) + test('should support selection function with select', async () => { + const app = TestBed.inject(ApplicationRef) + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ + queryKey: ['key13'], + queryFn: () => [{ id: 1 }, { id: 2 }], + select: (data) => data.map((item) => item.id), + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query + + // Wait for query to complete (even synchronous queryFn needs time to process) + const stablePromise = app.whenStable() + await Promise.resolve() + await vi.advanceTimersByTimeAsync(10) + await stablePromise + + expect(query.status()).toBe('success') + expect(query.data()).toEqual([1, 2]) + }) + describe('throwOnError', () => { test('should evaluate throwOnError when query is expected to throw', async () => { const boundaryFn = vi.fn() - TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + boundaryFn = boundaryFn + query = injectQuery(() => ({ queryKey: ['key12'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), retry: false, - throwOnError: boundaryFn, + throwOnError: this.boundaryFn, })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() await vi.advanceTimersByTimeAsync(11) expect(boundaryFn).toHaveBeenCalledTimes(1) @@ -469,41 +580,112 @@ describe('injectQuery', () => { }) test('should throw when throwOnError is true', async () => { - TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ + const zone = TestBed.inject(NgZone) + const zoneErrorPromise = new Promise((resolve) => { + const sub = zone.onError.subscribe((error) => { + sub.unsubscribe() + resolve(error as Error) + }) + }) + let handler: ((error: Error) => void) | null = null + const processErrorPromise = new Promise((resolve) => { + handler = (error: Error) => { + process.off('uncaughtException', handler!) + resolve(error) + } + process.on('uncaughtException', handler) + }) + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ queryKey: ['key13'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), throwOnError: true, })) - }) - - await expect(vi.runAllTimersAsync()).rejects.toThrow('Some error') + } + + TestBed.createComponent(TestComponent).detectChanges() + + try { + await vi.runAllTimersAsync() + await expect(zoneErrorPromise).resolves.toEqual(Error('Some error')) + await expect(processErrorPromise).resolves.toEqual(Error('Some error')) + } finally { + if (handler) { + process.off('uncaughtException', handler) + } + } }) test('should throw when throwOnError function returns true', async () => { - TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ + const zone = TestBed.inject(NgZone) + const zoneErrorPromise = new Promise((resolve) => { + const sub = zone.onError.subscribe((error) => { + sub.unsubscribe() + resolve(error as Error) + }) + }) + let handler: ((error: Error) => void) | null = null + const processErrorPromise = new Promise((resolve) => { + handler = (error: Error) => { + process.off('uncaughtException', handler!) + resolve(error) + } + process.on('uncaughtException', handler) + }) + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ queryKey: ['key14'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), throwOnError: () => true, })) - }) - - await expect(vi.runAllTimersAsync()).rejects.toThrow('Some error') + } + + TestBed.createComponent(TestComponent).detectChanges() + + try { + await vi.runAllTimersAsync() + await expect(zoneErrorPromise).resolves.toEqual(Error('Some error')) + await expect(processErrorPromise).resolves.toEqual(Error('Some error')) + } finally { + if (handler) { + process.off('uncaughtException', handler) + } + } }) }) test('should set state to error when queryFn returns reject promise', async () => { - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ retry: false, queryKey: ['key15'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(query.status()).toBe('pending') @@ -516,6 +698,7 @@ describe('injectQuery', () => { @Component({ selector: 'app-fake', template: `{{ query.data() }}`, + changeDetection: ChangeDetectionStrategy.OnPush, }) class FakeComponent { name = input.required() @@ -526,17 +709,225 @@ describe('injectQuery', () => { })) } - const fixture = TestBed.createComponent(FakeComponent) - setSignalInputs(fixture.componentInstance, { - name: 'signal-input-required-test', + registerSignalInput(FakeComponent, 'name') + + @Component({ + template: ``, + imports: [FakeComponent], + }) + class HostComponent { + protected readonly name = signal('signal-input-required-test') + } + + const fixture = TestBed.createComponent(HostComponent) + fixture.detectChanges() + await vi.advanceTimersByTimeAsync(0) + + const result = fixture.nativeElement.querySelector('app-fake').textContent + expect(result).toEqual('signal-input-required-test') + }) + + test('should support aliasing query.data on required signal inputs', async () => { + @Component({ + selector: 'app-fake', + template: `{{ data() }}`, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class FakeComponent { + name = input.required() + + query = injectQuery(() => ({ + queryKey: ['fake-alias', this.name()], + queryFn: () => this.name(), + })) + + data = this.query.data + } + + registerSignalInput(FakeComponent, 'name') + + @Component({ + template: ``, + imports: [FakeComponent], }) + class HostComponent { + protected readonly name = signal('signal-input-alias-test') + } + const fixture = TestBed.createComponent(HostComponent) fixture.detectChanges() await vi.advanceTimersByTimeAsync(0) - expect(fixture.componentInstance.query.data()).toEqual( - 'signal-input-required-test', + const result = fixture.nativeElement.querySelector('app-fake').textContent + expect(result).toEqual('signal-input-alias-test') + }) + + test('should allow reading the query data on effect registered before injection', () => { + const spy = vi.fn() + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + readEffect = effect(() => { + spy(this.query.data()) + }) + + query = injectQuery(() => ({ + queryKey: ['effect-before-injection'], + queryFn: () => sleep(0).then(() => 'Some data'), + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + expect(spy).toHaveBeenCalledWith(undefined) + }) + + test('should render with an initial value for input signal if available before change detection', () => { + const key1 = queryKey() + const key2 = queryKey() + + queryClient.setQueryData(key1, 'value 1') + queryClient.setQueryData(key2, 'value 2') + + @Component({ + selector: 'app-test', + template: '{{ query.data() }}', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + inputKey = input.required<[string]>() + query = injectQuery(() => ({ + queryKey: this.inputKey(), + queryFn: () => sleep(0).then(() => 'Some data'), + })) + } + registerSignalInput(TestComponent, 'inputKey') + + const fixture = TestBed.createComponent(TestComponent) + fixture.componentRef.setInput('inputKey', key1) + + const instance = fixture.componentInstance + const query = instance.query + + expect(() => instance.inputKey()).not.toThrow() + + expect(instance.inputKey()).toEqual(key1) + expect(query.data()).toEqual('value 1') + + fixture.componentRef.setInput('inputKey', key2) + + expect(instance.inputKey()).toEqual(key2) + expect(query.data()).toEqual('value 2') + }) + + test('should allow reading the query data on component ngOnInit with required signal input', async () => { + const spy = vi.fn() + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + key = input.required<[string]>() + query = injectQuery(() => ({ + queryKey: this.key(), + queryFn: () => Promise.resolve(() => 'Some data'), + })) + + initialStatus!: string + + ngOnInit() { + this.initialStatus = this.query.status() + + // effect should not have been called yet + expect(spy).not.toHaveBeenCalled() + } + + _spyEffect = effect(() => { + spy() + }) + } + + registerSignalInput(TestComponent, 'key') + + const fixture = TestBed.createComponent(TestComponent) + fixture.componentRef.setInput('key', ['ngOnInitTest']) + + fixture.detectChanges() + expect(spy).toHaveBeenCalled() + + const instance = fixture.componentInstance + expect(instance.initialStatus).toEqual('pending') + }) + + test('should update query data on the same macro task when query data changes', async () => { + const query = TestBed.runInInjectionContext(() => + injectQuery(() => ({ + queryKey: ['test'], + initialData: 'initial data', + })), ) + + // Run effects + TestBed.tick() + + expect(query.data()).toBe('initial data') + queryClient.setQueryData(['test'], 'new data') + + // Flush microtasks + await Promise.resolve() + + expect(query.data()).toBe('new data') + }) + + test('should pause fetching while restoring and fetch once restoring is disabled', async () => { + const isRestoring = signal(true) + const fetchSpy = vi.fn(() => sleep(10).then(() => 'restored-data')) + + TestBed.resetTestingModule() + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery(queryClient), + provideIsRestoring(isRestoring.asReadonly()), + ], + }) + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ + queryKey: ['restoring'], + queryFn: fetchSpy, + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + + const query = fixture.componentInstance.query + expect(fetchSpy).not.toHaveBeenCalled() + expect(query.status()).toBe('pending') + + const stablePromise = fixture.whenStable() + await Promise.resolve() + await stablePromise + + isRestoring.set(false) + fixture.detectChanges() + + await vi.advanceTimersByTimeAsync(11) + await fixture.whenStable() + + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(query.status()).toBe('success') + expect(query.data()).toBe('restored-data') }) describe('injection context', () => { @@ -550,15 +941,28 @@ describe('injectQuery', () => { }) test('can be used outside injection context when passing an injector', () => { - const query = injectQuery( - () => ({ - queryKey: ['manualInjector'], - queryFn: () => sleep(0).then(() => 'Some data'), - }), - { - injector: TestBed.inject(Injector), - }, - ) + const injector = TestBed.inject(Injector) + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery( + () => ({ + queryKey: ['manualInjector'], + queryFn: () => sleep(0).then(() => 'Some data'), + }), + { + injector: injector, + }, + ) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(query.status()).toBe('pending') }) @@ -566,22 +970,30 @@ describe('injectQuery', () => { test('should complete queries before whenStable() resolves', async () => { const app = TestBed.inject(ApplicationRef) - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ queryKey: ['pendingTasksTest'], queryFn: async () => { await sleep(50) return 'test data' }, - })), - ) + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(query.status()).toBe('pending') expect(query.data()).toBeUndefined() - const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(60) - await stablePromise + await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('test data') @@ -602,14 +1014,25 @@ describe('injectQuery', () => { const httpClient = TestBed.inject(HttpClient) const httpTestingController = TestBed.inject(HttpTestingController) - // Create a query using HttpClient - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + httpClient = httpClient + query = injectQuery(() => ({ queryKey: ['httpClientTest'], queryFn: () => - lastValueFrom(httpClient.get<{ message: string }>('/api/test')), - })), - ) + lastValueFrom( + this.httpClient.get<{ message: string }>('/api/test'), + ), + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query // Schedule the HTTP response setTimeout(() => { @@ -621,9 +1044,8 @@ describe('injectQuery', () => { expect(query.status()).toBe('pending') // Advance timers and wait for Angular to be "stable" - const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(20) - await stablePromise + await app.whenStable() // Query should be complete after whenStable() thanks to PendingTasks integration expect(query.status()).toBe('success') @@ -642,28 +1064,36 @@ describe('injectQuery', () => { }) const app = TestBed.inject(ApplicationRef) - let callCount = 0 - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + callCount = 0 + query = injectQuery(() => ({ queryKey: ['sync-stale'], staleTime: 1000, queryFn: () => { - callCount++ - return `sync-data-${callCount}` + this.callCount++ + return `sync-data-${this.callCount}` }, - })), - ) + })) + } - // Synchronize pending effects - TestBed.tick() + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const component = fixture.componentInstance + const query = component.query const stablePromise = app.whenStable() + await vi.advanceTimersToNextTimerAsync() await stablePromise expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-1') - expect(callCount).toBe(1) + expect(component.callCount).toBe(1) await query.refetch() await Promise.resolve() @@ -672,7 +1102,7 @@ describe('injectQuery', () => { expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-2') - expect(callCount).toBe(2) + expect(component.callCount).toBe(2) }) test('should handle enabled/disabled transitions with synchronous queryFn', async () => { @@ -686,34 +1116,48 @@ describe('injectQuery', () => { const app = TestBed.inject(ApplicationRef) const enabledSignal = signal(false) - let callCount = 0 - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + enabledSignal = enabledSignal + callCount = 0 + query = injectQuery(() => ({ queryKey: ['sync-enabled'], - enabled: enabledSignal(), + enabled: this.enabledSignal(), queryFn: () => { - callCount++ - return `sync-data-${callCount}` + this.callCount++ + return `sync-data-${this.callCount}` }, - })), - ) + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const component = fixture.componentInstance + const query = component.query // Initially disabled - TestBed.tick() + await vi.advanceTimersByTimeAsync(0) await app.whenStable() expect(query.status()).toBe('pending') expect(query.data()).toBeUndefined() - expect(callCount).toBe(0) + expect(component.callCount).toBe(0) // Enable the query enabledSignal.set(true) - TestBed.tick() + fixture.detectChanges() + + const stablePromise = app.whenStable() + await vi.advanceTimersToNextTimerAsync() + await stablePromise - await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-1') - expect(callCount).toBe(1) + expect(component.callCount).toBe(1) }) test('should handle query invalidation with synchronous data', async () => { @@ -727,39 +1171,47 @@ describe('injectQuery', () => { const app = TestBed.inject(ApplicationRef) const testKey = ['sync-invalidate'] - let callCount = 0 - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + callCount = 0 + query = injectQuery(() => ({ queryKey: testKey, queryFn: () => { - callCount++ - return `sync-data-${callCount}` + this.callCount++ + return `sync-data-${this.callCount}` }, - })), - ) + })) + } - // Synchronize pending effects - TestBed.tick() + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const component = fixture.componentInstance + const query = component.query + + const stablePromise = app.whenStable() + await vi.advanceTimersToNextTimerAsync() + await stablePromise - await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-1') - expect(callCount).toBe(1) + expect(component.callCount).toBe(1) // Invalidate the query queryClient.invalidateQueries({ queryKey: testKey }) - TestBed.tick() // Wait for the invalidation to trigger a refetch await Promise.resolve() await vi.advanceTimersByTimeAsync(10) - TestBed.tick() await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-2') - expect(callCount).toBe(2) + expect(component.callCount).toBe(2) }) }) }) diff --git a/packages/angular-query-experimental/src/__tests__/mutation-options.test.ts b/packages/angular-query-experimental/src/__tests__/mutation-options.test.ts index ab040037d5..553df12d36 100644 --- a/packages/angular-query-experimental/src/__tests__/mutation-options.test.ts +++ b/packages/angular-query-experimental/src/__tests__/mutation-options.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { provideZonelessChangeDetection } from '@angular/core' import { TestBed } from '@angular/core/testing' import { QueryClient } from '@tanstack/query-core' import { sleep } from '@tanstack/query-test-utils' @@ -8,8 +7,8 @@ import { injectMutation, injectMutationState, mutationOptions, - provideTanStackQuery, } from '..' +import { flushQueryUpdates, setupTanStackQueryTestBed } from './test-utils' describe('mutationOptions', () => { let queryClient: QueryClient @@ -17,12 +16,7 @@ describe('mutationOptions', () => { beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - ], - }) + setupTanStackQueryTestBed(queryClient) }) afterEach(() => { @@ -61,7 +55,7 @@ describe('mutationOptions', () => { mutation.mutate() expect(isMutating()).toBe(0) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() expect(isMutating()).toBe(1) await vi.advanceTimersByTimeAsync(51) expect(isMutating()).toBe(0) @@ -81,7 +75,7 @@ describe('mutationOptions', () => { mutation.mutate() expect(isMutating()).toBe(0) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() expect(isMutating()).toBe(1) await vi.advanceTimersByTimeAsync(51) expect(isMutating()).toBe(0) @@ -109,7 +103,7 @@ describe('mutationOptions', () => { mutation1.mutate() mutation2.mutate() expect(isMutating()).toBe(0) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() expect(isMutating()).toBe(2) await vi.advanceTimersByTimeAsync(51) expect(isMutating()).toBe(0) @@ -137,7 +131,7 @@ describe('mutationOptions', () => { mutation1.mutate() mutation2.mutate() expect(isMutating()).toBe(0) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() expect(isMutating()).toBe(1) await vi.advanceTimersByTimeAsync(51) expect(isMutating()).toBe(0) diff --git a/packages/angular-query-experimental/src/__tests__/pending-tasks-ssr-queries.test.ts b/packages/angular-query-experimental/src/__tests__/pending-tasks-ssr-queries.test.ts new file mode 100644 index 0000000000..86d1afe23c --- /dev/null +++ b/packages/angular-query-experimental/src/__tests__/pending-tasks-ssr-queries.test.ts @@ -0,0 +1,75 @@ +import { + ChangeDetectionStrategy, + Component, + destroyPlatform, + provideZonelessChangeDetection, +} from '@angular/core' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { + provideServerRendering, + renderApplication, +} from '@angular/platform-server' +import { bootstrapApplication } from '@angular/platform-browser' +import { sleep } from '@tanstack/query-test-utils' +import { QueryClient } from '@tanstack/query-core' +import { injectQueries } from '../inject-queries' +import { provideTanStackQuery } from '../providers' + +describe('PendingTasks SSR (injectQueries)', () => { + beforeEach(() => { + destroyPlatform() + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + @Component({ + selector: 'app-root', + template: '{{ queries()[0].data() }}', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['ssr-queries-test'], + queryFn: async () => { + await sleep(1000) + return 'queries-data-fetched-on-ssr' + }, + }, + ], + })) + } + + test('should wait for stability of queries', async () => { + const htmlPromise = renderApplication( + () => + bootstrapApplication(TestComponent, { + providers: [ + provideServerRendering(), + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }), + ), + ], + }), + { + url: '/', + document: + '', + }, + ) + + await vi.runAllTimersAsync() + const html = await htmlPromise + + expect(html).toContain('queries-data-fetched-on-ssr') + }) +}) + diff --git a/packages/angular-query-experimental/src/__tests__/pending-tasks-ssr.test.ts b/packages/angular-query-experimental/src/__tests__/pending-tasks-ssr.test.ts new file mode 100644 index 0000000000..40c972bc6f --- /dev/null +++ b/packages/angular-query-experimental/src/__tests__/pending-tasks-ssr.test.ts @@ -0,0 +1,72 @@ +import { + ChangeDetectionStrategy, + Component, + destroyPlatform, + provideZonelessChangeDetection, +} from '@angular/core' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' + +import { + provideServerRendering, + renderApplication, +} from '@angular/platform-server' +import { bootstrapApplication } from '@angular/platform-browser' + +import { sleep } from '@tanstack/query-test-utils' +import { QueryClient } from '@tanstack/query-core' +import { injectQuery } from '../inject-query' +import { provideTanStackQuery } from '../providers' + +describe('PendingTasks SSR', () => { + beforeEach(() => { + destroyPlatform() + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + @Component({ + selector: 'app-root', + template: '{{ query.data() }}', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ + queryKey: ['ssr-test'], + queryFn: async () => { + await sleep(1000) + return 'data-fetched-on-ssr' + }, + })) + } + + test('should wait for stability of queries', async () => { + const htmlPromise = renderApplication( + () => + bootstrapApplication(TestComponent, { + providers: [ + provideServerRendering(), + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }), + ), + ], + }), + { + url: '/', + document: + '', + }, + ) + + await vi.runAllTimersAsync() + const html = await htmlPromise + + expect(html).toContain('data-fetched-on-ssr') + }) +}) diff --git a/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts b/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts index 92f70aed9f..5448bbbcac 100644 --- a/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts +++ b/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts @@ -1,7 +1,7 @@ import { ApplicationRef, + ChangeDetectionStrategy, Component, - provideZonelessChangeDetection, } from '@angular/core' import { TestBed } from '@angular/core/testing' import { HttpClient, provideHttpClient } from '@angular/common/http' @@ -12,13 +12,8 @@ import { import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import { lastValueFrom } from 'rxjs' -import { - QueryClient, - injectMutation, - injectQuery, - onlineManager, - provideTanStackQuery, -} from '..' +import { QueryClient, injectMutation, injectQuery, onlineManager } from '..' +import { flushQueryUpdates, setupTanStackQueryTestBed } from './test-utils' describe('PendingTasks Integration', () => { let queryClient: QueryClient @@ -37,12 +32,7 @@ describe('PendingTasks Integration', () => { }, }) - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - ], - }) + setupTanStackQueryTestBed(queryClient) }) afterEach(() => { @@ -55,12 +45,21 @@ describe('PendingTasks Integration', () => { test('should handle synchronous queryFn with whenStable()', async () => { const app = TestBed.inject(ApplicationRef) - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ queryKey: ['sync'], queryFn: () => 'instant-data', // Resolves synchronously - })), - ) + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query // Should start as pending even with synchronous data expect(query.status()).toBe('pending') @@ -140,7 +139,6 @@ describe('PendingTasks Integration', () => { ) mutation.mutate() - TestBed.tick() const stablePromise = app.whenStable() @@ -183,18 +181,27 @@ describe('PendingTasks Integration', () => { test('should handle rapid refetches without task leaks', async () => { const app = TestBed.inject(ApplicationRef) - let callCount = 0 - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + callCount = 0 + query = injectQuery(() => ({ queryKey: ['rapid-refetch'], queryFn: async () => { - callCount++ + this.callCount++ await sleep(10) - return `data-${callCount}` + return `data-${this.callCount}` }, - })), - ) + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query // Trigger multiple rapid refetches query.refetch() @@ -209,6 +216,54 @@ describe('PendingTasks Integration', () => { expect(query.data()).toMatch(/^data-\d+$/) }) + test('should keep PendingTasks active when query starts offline (never reaches fetching)', async () => { + const app = TestBed.inject(ApplicationRef) + + onlineManager.setOnline(false) + + const query = TestBed.runInInjectionContext(() => + injectQuery(() => ({ + queryKey: ['start-offline'], + networkMode: 'online', // Default: won't fetch while offline + queryFn: async () => { + await sleep(10) + return 'online-data' + }, + })), + ) + + // Allow query to initialize + await Promise.resolve() + await flushQueryUpdates() + + // Query should initialize directly to 'paused' (never goes through 'fetching') + expect(query.status()).toBe('pending') + expect(query.fetchStatus()).toBe('paused') + + const stablePromise = app.whenStable() + let stableResolved = false + void stablePromise.then(() => { + stableResolved = true + }) + + await Promise.resolve() + + // PendingTasks should block stability even though we never hit 'fetching' + expect(stableResolved).toBe(false) + + // Bring the app back online so the query can fetch + onlineManager.setOnline(true) + + await vi.advanceTimersByTimeAsync(20) + await Promise.resolve() + + await stablePromise + + expect(stableResolved).toBe(true) + expect(query.status()).toBe('success') + expect(query.data()).toBe('online-data') + }) + test('should keep PendingTasks active while query retry is paused offline', async () => { const app = TestBed.inject(ApplicationRef) let attempt = 0 @@ -230,7 +285,7 @@ describe('PendingTasks Integration', () => { ) // Allow the initial attempt to start and fail - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() await Promise.resolve() // Wait for the first attempt to complete and start retry delay @@ -279,6 +334,7 @@ describe('PendingTasks Integration', () => { describe('Component Destruction', () => { @Component({ template: '', + changeDetection: ChangeDetectionStrategy.OnPush, }) class TestComponent { query = injectQuery(() => ({ @@ -298,36 +354,42 @@ describe('PendingTasks Integration', () => { } test('should cleanup pending tasks when component with active query is destroyed', async () => { - const app = TestBed.inject(ApplicationRef) const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() // Start the query expect(fixture.componentInstance.query.status()).toBe('pending') + expect(fixture.isStable()).toBe(false) // Destroy component while query is running fixture.destroy() // Angular should become stable even though component was destroyed - const stablePromise = app.whenStable() + const stablePromise = fixture.whenStable() await vi.advanceTimersByTimeAsync(150) - - await expect(stablePromise).resolves.toEqual(undefined) + await stablePromise + expect(fixture.isStable()).toBe(true) }) test('should cleanup pending tasks when component with active mutation is destroyed', async () => { - const app = TestBed.inject(ApplicationRef) const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() fixture.componentInstance.mutation.mutate('test') + fixture.detectChanges() + expect(fixture.isStable()).toBe(false) // Destroy component while mutation is running fixture.destroy() + fixture.detectChanges() + expect(fixture.isStable()).toBe(true) // Angular should become stable even though component was destroyed - const stablePromise = app.whenStable() - await vi.advanceTimersByTimeAsync(150) + const stablePromise = fixture.whenStable() + await vi.advanceTimersByTimeAsync(200) + await stablePromise - await expect(stablePromise).resolves.toEqual(undefined) + expect(fixture.isStable()).toBe(true) }) }) @@ -335,32 +397,37 @@ describe('PendingTasks Integration', () => { test('should handle multiple queries running simultaneously', async () => { const app = TestBed.inject(ApplicationRef) - const query1 = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query1 = injectQuery(() => ({ queryKey: ['concurrent-1'], queryFn: async () => { await sleep(30) return 'data-1' }, - })), - ) + })) - const query2 = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + query2 = injectQuery(() => ({ queryKey: ['concurrent-2'], queryFn: async () => { await sleep(50) return 'data-2' }, - })), - ) + })) - const query3 = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + query3 = injectQuery(() => ({ queryKey: ['concurrent-3'], queryFn: () => 'instant-data', // Synchronous - })), - ) + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const { query1, query2, query3 } = fixture.componentInstance // All queries should start expect(query1.status()).toBe('pending') @@ -469,14 +536,8 @@ describe('PendingTasks Integration', () => { describe('HttpClient Integration', () => { beforeEach(() => { - TestBed.resetTestingModule() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - provideHttpClient(), - provideHttpClientTesting(), - ], + setupTanStackQueryTestBed(queryClient, { + providers: [provideHttpClient(), provideHttpClientTesting()], }) }) diff --git a/packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts b/packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts index d06aef6723..1e039d33f4 100644 --- a/packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts +++ b/packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts @@ -1,10 +1,21 @@ -import { isSignal, signal } from '@angular/core' -import { describe, expect, test } from 'vitest' +import { + ChangeDetectionStrategy, + Component, + computed, + input, + isSignal, + provideZonelessChangeDetection, + signal, + untracked, +} from '@angular/core' +import { beforeEach, describe, expect, test } from 'vitest' +import { TestBed } from '@angular/core/testing' import { signalProxy } from '../signal-proxy' +import { registerSignalInput } from './test-utils' describe('signalProxy', () => { const inputSignal = signal({ fn: () => 'bar', baz: 'qux' }) - const proxy = signalProxy(inputSignal) + const proxy = signalProxy(inputSignal, ['fn']) test('should have computed fields', () => { expect(proxy.baz()).toEqual('qux') @@ -24,4 +35,63 @@ describe('signalProxy', () => { test('supports "Object.keys"', () => { expect(Object.keys(proxy)).toEqual(['fn', 'baz']) }) + + describe('in component fixture', () => { + @Component({ + selector: 'app-test', + template: '{{ proxy.baz() }}', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + number = input.required() + obj = computed(() => ({ + number: this.number(), + fn: () => untracked(this.number) + 1, + })) + proxy = signalProxy(this.obj, ['fn']) + shortNumber = this.proxy.number + shortFn = this.proxy.fn + } + registerSignalInput(TestComponent, 'number') + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideZonelessChangeDetection()], + }) + }) + + test('should generate fixed fields after initial change detection run', () => { + const fixture = TestBed.createComponent(TestComponent) + const instance = fixture.componentInstance + + expect(() => instance.shortNumber).not.throw() + expect(() => instance.shortNumber()).toThrow() + + fixture.componentRef.setInput('number', 1) + fixture.detectChanges() + + expect(isSignal(instance.proxy.number)).toBe(true) + expect(instance.proxy.number()).toBe(1) + expect(instance.shortNumber).toBe(instance.proxy.number) + + expect(instance.proxy.fn()).toBe(2) + expect(isSignal(instance.proxy.fn)).toBe(false) + expect(instance.shortFn).toBe(instance.proxy.fn) + }) + + test('should reflect updates on the proxy', () => { + const fixture = TestBed.createComponent(TestComponent) + const instance = fixture.componentInstance + fixture.componentRef.setInput('number', 0) + fixture.detectChanges() + + expect(instance.shortNumber()).toBe(0) + expect(instance.shortFn()).toBe(1) + + fixture.componentRef.setInput('number', 1) + + expect(instance.shortNumber()).toBe(1) + expect(instance.shortFn()).toBe(2) + }) + }) }) diff --git a/packages/angular-query-experimental/src/__tests__/test-utils.ts b/packages/angular-query-experimental/src/__tests__/test-utils.ts index 218cdea5f6..879dda82b6 100644 --- a/packages/angular-query-experimental/src/__tests__/test-utils.ts +++ b/packages/angular-query-experimental/src/__tests__/test-utils.ts @@ -1,8 +1,20 @@ -import { isSignal, untracked } from '@angular/core' -import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals' -import { expect } from 'vitest' -import type { InputSignal, Signal } from '@angular/core' -import type { ComponentFixture } from '@angular/core/testing' +import { + isSignal, + provideZonelessChangeDetection, + untracked, +} from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { expect, vi } from 'vitest' +import { provideTanStackQuery } from '..' +import type { QueryClient } from '@tanstack/query-core' +import type { + EnvironmentProviders, + Provider, + Signal, + Type, +} from '@angular/core' + +// cspell:ignore ɵcmp ɵdir // Evaluate all signals on an object and return the result function evaluateSignals>( @@ -35,43 +47,56 @@ export const expectSignals = >( expect(evaluateSignals(obj)).toMatchObject(expected) } -type ToSignalInputUpdatableMap = { - [K in keyof T as T[K] extends InputSignal - ? K - : never]: T[K] extends InputSignal ? Value : never +/** + * Reset Angular's TestBed and configure the standard TanStack Query providers for tests. + * Pass additional providers (including EnvironmentProviders) via the options argument. + */ +export function setupTanStackQueryTestBed( + queryClient: QueryClient, + options: { providers?: Array } = {}, +) { + TestBed.resetTestingModule() + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery(queryClient), + ...(options.providers ?? []), + ], + }) } -function componentHasSignalInputProperty( - component: object, - property: TProperty, -): component is { [key in TProperty]: InputSignal } { - return ( - component.hasOwnProperty(property) && (component as any)[property][SIGNAL] - ) +/** + * TanStack Query schedules notifyManager updates with setTimeout(0); when fake timers + * are enabled, advance them so PendingTasks sees the queued work. + */ +export async function flushQueryUpdates() { + await vi.advanceTimersByTimeAsync(0) } +const SIGNAL_BASED_INPUT_FLAG = 1 + /** - * Set required signal input value to component fixture - * @see https://github.com/angular/angular/issues/54013 + * Register a signal-based input on a test-only component/dir so Angular marks the + * `input.required()` member as bound before the initial change detection run. + * + * After migrating to Angular 21 we can use the CLI to compile and run Vitest tests + * and this helper should be obsolete. */ -export function setSignalInputs>( - component: T, - inputs: ToSignalInputUpdatableMap, +export function registerSignalInput( + type: Type, + inputName: keyof T & string, ) { - for (const inputKey in inputs) { - if (componentHasSignalInputProperty(component, inputKey)) { - signalSetFn(component[inputKey][SIGNAL], inputs[inputKey]) - } + const definition = (type as any).ɵcmp ?? (type as any).ɵdir + if (!definition) { + throw new Error(`Component ${type.name} is missing its definition`) } -} -export function setFixtureSignalInputs>( - componentFixture: ComponentFixture, - inputs: ToSignalInputUpdatableMap, - options: { detectChanges: boolean } = { detectChanges: true }, -) { - setSignalInputs(componentFixture.componentInstance, inputs) - if (options.detectChanges) { - componentFixture.detectChanges() + definition.inputs = { + ...(definition.inputs ?? {}), + [inputName]: [inputName, SIGNAL_BASED_INPUT_FLAG, null], + } + definition.declaredInputs = { + ...(definition.declaredInputs ?? {}), + [inputName]: inputName, } } diff --git a/packages/angular-query-experimental/src/__tests__/with-devtools.test.ts b/packages/angular-query-experimental/src/__tests__/with-devtools.test.ts index 6c6e128115..5699d34151 100644 --- a/packages/angular-query-experimental/src/__tests__/with-devtools.test.ts +++ b/packages/angular-query-experimental/src/__tests__/with-devtools.test.ts @@ -13,6 +13,7 @@ import { } from '@angular/core' import { provideTanStackQuery } from '../providers' import { withDevtools } from '../devtools' +import { flushQueryUpdates } from './test-utils' import type { DevtoolsButtonPosition, DevtoolsErrorType, @@ -138,7 +139,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() TestBed.tick() await vi.dynamicImportSettled() TestBed.tick() @@ -169,7 +170,7 @@ describe('withDevtools feature', () => { TestBed.inject(ENVIRONMENT_INITIALIZER) // Destroys injector TestBed.resetTestingModule() - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() await vi.dynamicImportSettled() expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() @@ -189,8 +190,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) - await vi.dynamicImportSettled() + await flushQueryUpdates() expect(mockTanstackQueryDevtools).toHaveBeenCalledTimes(1) @@ -206,7 +206,7 @@ describe('withDevtools feature', () => { ) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() expect(mockTanstackQueryDevtools).toHaveBeenCalledTimes(1) }) @@ -251,8 +251,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) - await vi.dynamicImportSettled() + await flushQueryUpdates() TestBed.tick() @@ -292,8 +291,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) - await vi.dynamicImportSettled() + await flushQueryUpdates() TestBed.tick() @@ -325,8 +323,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) - await vi.dynamicImportSettled() + await flushQueryUpdates() TestBed.tick() @@ -357,8 +354,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) - await vi.dynamicImportSettled() + await flushQueryUpdates() TestBed.tick() @@ -391,8 +387,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) - await vi.dynamicImportSettled() + await flushQueryUpdates() TestBed.tick() @@ -422,8 +417,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) - await vi.dynamicImportSettled() + await flushQueryUpdates() expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes(1) expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(0) @@ -449,7 +443,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() TestBed.tick() await vi.dynamicImportSettled() @@ -479,7 +473,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() expect(mockDevtoolsInstance.mount).not.toHaveBeenCalled() @@ -536,7 +530,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() expect(withDevtoolsFn).toHaveBeenCalledWith(mockService1, mockService2) }) @@ -557,7 +551,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() expect(withDevtoolsFn).toHaveBeenCalledWith() }) @@ -587,7 +581,7 @@ describe('withDevtools feature', () => { }) TestBed.inject(ENVIRONMENT_INITIALIZER) - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() const service = TestBed.inject(ReactiveService) diff --git a/packages/angular-query-experimental/src/__tests__/zonejs-adapter.test.ts b/packages/angular-query-experimental/src/__tests__/zonejs-adapter.test.ts new file mode 100644 index 0000000000..6c4f52714a --- /dev/null +++ b/packages/angular-query-experimental/src/__tests__/zonejs-adapter.test.ts @@ -0,0 +1,96 @@ +import 'zone.js' +import { + ChangeDetectionStrategy, + Component, + provideZoneChangeDetection, +} from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { sleep } from '@tanstack/query-test-utils' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + QueryClient, + injectMutation, + injectQuery, + provideTanStackQuery, +} from '..' + +describe('adapter with Zone.js', () => { + let queryClient: QueryClient + + beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + queryClient = new QueryClient() + + TestBed.resetTestingModule() + TestBed.configureTestingModule({ + providers: [ + provideZoneChangeDetection(), + provideTanStackQuery(queryClient), + ], + }) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('supports injectQuery in a Zone.js app', async () => { + @Component({ + selector: 'zone-query-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class ZoneQueryTestComponent { + query = injectQuery(() => ({ + queryKey: ['zone-query'], + queryFn: async () => { + await sleep(10) + return 'query-data' + }, + })) + } + + const fixture = TestBed.createComponent(ZoneQueryTestComponent) + fixture.detectChanges() + + const query = fixture.componentInstance.query + expect(query.status()).toBe('pending') + + const stablePromise = fixture.whenStable() + await vi.advanceTimersByTimeAsync(20) + await stablePromise + + expect(query.status()).toBe('success') + expect(query.data()).toBe('query-data') + }) + + it('supports injectMutation in a Zone.js app', async () => { + @Component({ + selector: 'zone-mutation-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class ZoneMutationTestComponent { + mutation = injectMutation(() => ({ + mutationKey: ['zone-mutation'], + mutationFn: async (value: string) => { + await sleep(10) + return `mutated-${value}` + }, + })) + } + + const fixture = TestBed.createComponent(ZoneMutationTestComponent) + fixture.detectChanges() + + const mutation = fixture.componentInstance.mutation + mutation.mutate('value') + + const stablePromise = fixture.whenStable() + await vi.advanceTimersByTimeAsync(20) + await stablePromise + + expect(mutation.status()).toBe('success') + expect(mutation.data()).toBe('mutated-value') + }) +}) diff --git a/packages/angular-query-experimental/src/create-base-query.ts b/packages/angular-query-experimental/src/create-base-query.ts index 4daede7684..697fe7ae2b 100644 --- a/packages/angular-query-experimental/src/create-base-query.ts +++ b/packages/angular-query-experimental/src/create-base-query.ts @@ -1,10 +1,11 @@ import { + DestroyRef, NgZone, - VERSION, + PendingTasks, computed, effect, inject, - signal, + linkedSignal, untracked, } from '@angular/core' import { @@ -14,9 +15,9 @@ import { } from '@tanstack/query-core' import { signalProxy } from './signal-proxy' import { injectIsRestoring } from './inject-is-restoring' -import { PENDING_TASKS } from './pending-tasks-compat' -import type { PendingTaskRef } from './pending-tasks-compat' +import type { MethodKeys } from './signal-proxy' import type { + DefaultedQueryObserverOptions, QueryKey, QueryObserver, QueryObserverResult, @@ -27,6 +28,7 @@ import type { CreateBaseQueryOptions } from './types' * Base implementation for `injectQuery` and `injectInfiniteQuery`. * @param optionsFn * @param Observer + * @param excludeFunctions */ export function createBaseQuery< TQueryFnData, @@ -43,11 +45,29 @@ export function createBaseQuery< TQueryKey >, Observer: typeof QueryObserver, + excludeFunctions: ReadonlyArray, ) { const ngZone = inject(NgZone) - const pendingTasks = inject(PENDING_TASKS) + const pendingTasks = inject(PendingTasks) const queryClient = inject(QueryClient) const isRestoring = injectIsRestoring() + const destroyRef = inject(DestroyRef) + + let destroyed = false + let taskCleanupRef: (() => void) | null = null + + const startPendingTask = () => { + if (!taskCleanupRef && !destroyed) { + taskCleanupRef = pendingTasks.add() + } + } + + const stopPendingTask = () => { + if (taskCleanupRef) { + taskCleanupRef() + taskCleanupRef = null + } + } /** * Signal that has the default options from query client applied @@ -63,113 +83,118 @@ export function createBaseQuery< return defaultedOptions }) - const observerSignal = (() => { - let instance: QueryObserver< + // Computed without deps to lazy initialize the observer + const observerSignal = computed(() => { + return new Observer(queryClient, untracked(defaultedOptionsSignal)) + }) + + effect(() => { + observerSignal().setOptions(defaultedOptionsSignal()) + }) + + const trackObserverResult = ( + result: QueryObserverResult, + notifyOnChangeProps?: DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey - > | null = null + >['notifyOnChangeProps'], + ) => { + const observer = untracked(observerSignal) + const trackedResult = observer.trackResult(result) + + if (!notifyOnChangeProps) { + autoTrackResultProperties(trackedResult) + } + + return trackedResult + } + + const autoTrackResultProperties = ( + result: QueryObserverResult, + ) => { + for (const key of Object.keys(result) as Array< + keyof QueryObserverResult + >) { + if (key === 'promise') continue + const value = result[key] + if (typeof value === 'function') continue + // Access value once so QueryObserver knows this prop is tracked. + void value + } + } + + const subscribeToObserver = () => { + const observer = untracked(observerSignal) + const initialState = observer.getCurrentResult() + if (initialState.fetchStatus !== 'idle') { + startPendingTask() + } + + return observer.subscribe((state) => { + if (state.fetchStatus !== 'idle') { + startPendingTask() + } else { + stopPendingTask() + } - return computed(() => { - return (instance ||= new Observer(queryClient, defaultedOptionsSignal())) + queueMicrotask(() => { + if (destroyed) return + notifyManager.batch(() => { + ngZone.run(() => { + if ( + state.isError && + !state.isFetching && + shouldThrowError(observer.options.throwOnError, [ + state.error, + observer.getCurrentQuery(), + ]) + ) { + ngZone.onError.emit(state.error) + throw state.error + } + const trackedState = trackObserverResult( + state, + observer.options.notifyOnChangeProps, + ) + resultSignal.set(trackedState) + }) + }) + }) }) - })() + } - const optimisticResultSignal = computed(() => - observerSignal().getOptimisticResult(defaultedOptionsSignal()), - ) - - const resultFromSubscriberSignal = signal | null>(null) - - effect( - (onCleanup) => { - const observer = observerSignal() + const resultSignal = linkedSignal({ + source: defaultedOptionsSignal, + computation: () => { + const observer = untracked(observerSignal) const defaultedOptions = defaultedOptionsSignal() - untracked(() => { - observer.setOptions(defaultedOptions) - }) - onCleanup(() => { - ngZone.run(() => resultFromSubscriberSignal.set(null)) - }) + const result = observer.getOptimisticResult(defaultedOptions) + return trackObserverResult(result, defaultedOptions.notifyOnChangeProps) }, - { - // Set allowSignalWrites to support Angular < v19 - // Set to undefined to avoid warning on newer versions - allowSignalWrites: VERSION.major < '19' || undefined, - }, - ) + }) effect((onCleanup) => { - // observer.trackResult is not used as this optimization is not needed for Angular - const observer = observerSignal() - let pendingTaskRef: PendingTaskRef | null = null - - const unsubscribe = isRestoring() - ? () => undefined - : untracked(() => - ngZone.runOutsideAngular(() => { - return observer.subscribe( - notifyManager.batchCalls((state) => { - ngZone.run(() => { - if (state.fetchStatus === 'fetching' && !pendingTaskRef) { - pendingTaskRef = pendingTasks.add() - } - - if (state.fetchStatus === 'idle' && pendingTaskRef) { - pendingTaskRef() - pendingTaskRef = null - } - - if ( - state.isError && - !state.isFetching && - shouldThrowError(observer.options.throwOnError, [ - state.error, - observer.getCurrentQuery(), - ]) - ) { - ngZone.onError.emit(state.error) - throw state.error - } - resultFromSubscriberSignal.set(state) - }) - }), - ) - }), - ) - + if (isRestoring()) { + return + } + const unsubscribe = untracked(() => subscribeToObserver()) onCleanup(() => { - if (pendingTaskRef) { - pendingTaskRef() - pendingTaskRef = null - } unsubscribe() + stopPendingTask() }) }) + destroyRef.onDestroy(() => { + destroyed = true + stopPendingTask() + }) + return signalProxy( - computed(() => { - const subscriberResult = resultFromSubscriberSignal() - const optimisticResult = optimisticResultSignal() - const result = subscriberResult ?? optimisticResult - - // Wrap methods to ensure observer has latest options before execution - const observer = observerSignal() - - const originalRefetch = result.refetch - return { - ...result, - refetch: ((...args: Parameters) => { - observer.setOptions(defaultedOptionsSignal()) - return originalRefetch(...args) - }) as typeof originalRefetch, - } - }), + resultSignal.asReadonly(), + excludeFunctions as Array>>, ) } diff --git a/packages/angular-query-experimental/src/index.ts b/packages/angular-query-experimental/src/index.ts index fc033224e5..18f7e3379b 100644 --- a/packages/angular-query-experimental/src/index.ts +++ b/packages/angular-query-experimental/src/index.ts @@ -39,7 +39,12 @@ export { injectMutation } from './inject-mutation' export type { InjectMutationStateOptions } from './inject-mutation-state' export { injectMutationState } from './inject-mutation-state' -export type { QueriesOptions, QueriesResults } from './inject-queries' +export type { + InjectQueriesOptions, + QueriesOptions, + QueriesResults, +} from './inject-queries' +export { injectQueries } from './inject-queries' export type { InjectQueryOptions } from './inject-query' export { injectQuery } from './inject-query' diff --git a/packages/angular-query-experimental/src/inject-infinite-query.ts b/packages/angular-query-experimental/src/inject-infinite-query.ts index ee6de03240..aa4a26cf4f 100644 --- a/packages/angular-query-experimental/src/inject-infinite-query.ts +++ b/packages/angular-query-experimental/src/inject-infinite-query.ts @@ -6,9 +6,11 @@ import { runInInjectionContext, } from '@angular/core' import { createBaseQuery } from './create-base-query' +import type { MethodKeys } from './signal-proxy' import type { DefaultError, InfiniteData, + InfiniteQueryObserverResult, QueryKey, QueryObserver, } from '@tanstack/query-core' @@ -110,8 +112,20 @@ export function injectInfiniteQuery< * @param options - Additional configuration. * @returns The infinite query result. */ -export function injectInfiniteQuery( - injectInfiniteQueryFn: () => CreateInfiniteQueryOptions, +export function injectInfiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + injectInfiniteQueryFn: () => CreateInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, options?: InjectInfiniteQueryOptions, ) { !options?.injector && assertInInjectionContext(injectInfiniteQuery) @@ -120,6 +134,13 @@ export function injectInfiniteQuery( createBaseQuery( injectInfiniteQueryFn, InfiniteQueryObserver as typeof QueryObserver, + methodsToExclude, ), ) } + +const methodsToExclude: Array> = [ + 'fetchNextPage', + 'fetchPreviousPage', + 'refetch', +] diff --git a/packages/angular-query-experimental/src/inject-mutation.ts b/packages/angular-query-experimental/src/inject-mutation.ts index 7eb605047f..4e08431548 100644 --- a/packages/angular-query-experimental/src/inject-mutation.ts +++ b/packages/angular-query-experimental/src/inject-mutation.ts @@ -1,6 +1,7 @@ import { Injector, NgZone, + PendingTasks, assertInInjectionContext, computed, effect, @@ -16,8 +17,6 @@ import { shouldThrowError, } from '@tanstack/query-core' import { signalProxy } from './signal-proxy' -import { PENDING_TASKS } from './pending-tasks-compat' -import type { PendingTaskRef } from './pending-tasks-compat' import type { DefaultError, MutationObserverResult } from '@tanstack/query-core' import type { CreateMutateFunction, @@ -59,7 +58,7 @@ export function injectMutation< !options?.injector && assertInInjectionContext(injectMutation) const injector = options?.injector ?? inject(Injector) const ngZone = injector.get(NgZone) - const pendingTasks = injector.get(PENDING_TASKS) + const pendingTasks = injector.get(PendingTasks) const queryClient = injector.get(QueryClient) /** @@ -82,6 +81,22 @@ export function injectMutation< }) })() + let destroyed = false + let taskCleanupRef: (() => void) | null = null + + const startPendingTask = () => { + if (!taskCleanupRef && !destroyed) { + taskCleanupRef = pendingTasks.add() + } + } + + const stopPendingTask = () => { + if (taskCleanupRef) { + taskCleanupRef() + taskCleanupRef = null + } + } + const mutateFnSignal = computed< CreateMutateFunction >(() => { @@ -125,24 +140,19 @@ export function injectMutation< effect( (onCleanup) => { - // observer.trackResult is not used as this optimization is not needed for Angular const observer = observerSignal() - let pendingTaskRef: PendingTaskRef | null = null untracked(() => { const unsubscribe = ngZone.runOutsideAngular(() => observer.subscribe( notifyManager.batchCalls((state) => { ngZone.run(() => { - // Track pending task when mutation is pending - if (state.isPending && !pendingTaskRef) { - pendingTaskRef = pendingTasks.add() - } + if (destroyed) return - // Clear pending task when mutation is no longer pending - if (!state.isPending && pendingTaskRef) { - pendingTaskRef() - pendingTaskRef = null + if (state.isPending) { + startPendingTask() + } else { + stopPendingTask() } if ( @@ -159,11 +169,8 @@ export function injectMutation< ), ) onCleanup(() => { - // Clean up any pending task on destroy - if (pendingTaskRef) { - pendingTaskRef() - pendingTaskRef = null - } + destroyed = true + stopPendingTask() unsubscribe() }) }) @@ -186,10 +193,9 @@ export function injectMutation< } }) - return signalProxy(resultSignal) as CreateMutationResult< - TData, - TError, - TVariables, - TOnMutateResult - > + return signalProxy(resultSignal, [ + 'mutate', + 'mutateAsync', + 'reset', + ]) as CreateMutationResult } diff --git a/packages/angular-query-experimental/src/inject-queries.ts b/packages/angular-query-experimental/src/inject-queries.ts index 2f201799e7..eb9a7af7f2 100644 --- a/packages/angular-query-experimental/src/inject-queries.ts +++ b/packages/angular-query-experimental/src/inject-queries.ts @@ -7,24 +7,27 @@ import { DestroyRef, Injector, NgZone, + PendingTasks, assertInInjectionContext, computed, effect, inject, + linkedSignal, runInInjectionContext, - signal, untracked, } from '@angular/core' import { signalProxy } from './signal-proxy' import { injectIsRestoring } from './inject-is-restoring' import type { DefaultError, + DefinedQueryObserverResult, OmitKeyof, QueriesObserverOptions, QueriesPlaceholderDataFunction, QueryFunction, QueryKey, QueryObserverOptions, + QueryObserverResult, ThrowOnError, } from '@tanstack/query-core' import type { @@ -33,6 +36,7 @@ import type { DefinedCreateQueryResult, } from './types' import type { Signal } from '@angular/core' +import type { MethodKeys } from './signal-proxy' // This defines the `CreateQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. // `placeholderData` function always gets undefined passed @@ -90,39 +94,42 @@ type GetCreateQueryOptionsForCreateQueries = : // Fallback QueryObserverOptionsForCreateQueries -// A defined initialData setting should return a DefinedCreateQueryResult rather than CreateQueryResult -type GetDefinedOrUndefinedQueryResult = T extends { - initialData?: infer TInitialData -} - ? unknown extends TInitialData - ? CreateQueryResult - : TInitialData extends TData - ? DefinedCreateQueryResult - : TInitialData extends () => infer TInitialDataResult - ? unknown extends TInitialDataResult - ? CreateQueryResult - : TInitialDataResult extends TData - ? DefinedCreateQueryResult - : CreateQueryResult - : CreateQueryResult - : CreateQueryResult - -type GetCreateQueryResult = - // Part 1: responsible for mapping explicit type parameter to function result, if object +// Generic wrapper that handles initialData logic for any result type pair +type GenericGetDefinedOrUndefinedQueryResult = + T extends { + initialData?: infer TInitialData + } + ? unknown extends TInitialData + ? TUndefined + : TInitialData extends TData + ? TDefined + : TInitialData extends () => infer TInitialDataResult + ? unknown extends TInitialDataResult + ? TUndefined + : TInitialDataResult extends TData + ? TDefined + : TUndefined + : TUndefined + : TUndefined + +// Infer TData and TError from query options +// Shared type between the results with and without the combine function +type InferDataAndError = + // Part 1: explicit type parameter as object { queryFnData, error, data } T extends { queryFnData: any; error?: infer TError; data: infer TData } - ? GetDefinedOrUndefinedQueryResult + ? { data: TData; error: TError } : T extends { queryFnData: infer TQueryFnData; error?: infer TError } - ? GetDefinedOrUndefinedQueryResult + ? { data: TQueryFnData; error: TError } : T extends { data: infer TData; error?: infer TError } - ? GetDefinedOrUndefinedQueryResult - : // Part 2: responsible for mapping explicit type parameter to function result, if tuple + ? { data: TData; error: TError } + : // Part 2: explicit type parameter as tuple [TQueryFnData, TError, TData] T extends [any, infer TError, infer TData] - ? GetDefinedOrUndefinedQueryResult + ? { data: TData; error: TError } : T extends [infer TQueryFnData, infer TError] - ? GetDefinedOrUndefinedQueryResult + ? { data: TQueryFnData; error: TError } : T extends [infer TQueryFnData] - ? GetDefinedOrUndefinedQueryResult - : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided + ? { data: TQueryFnData; error: unknown } + : // Part 3: infer from queryFn, select, throwOnError T extends { queryFn?: | QueryFunction @@ -130,13 +137,40 @@ type GetCreateQueryResult = select?: (data: any) => infer TData throwOnError?: ThrowOnError } - ? GetDefinedOrUndefinedQueryResult< - T, - unknown extends TData ? TQueryFnData : TData, - unknown extends TError ? DefaultError : TError - > + ? { + data: unknown extends TData ? TQueryFnData : TData + error: unknown extends TError ? DefaultError : TError + } : // Fallback - CreateQueryResult + { data: unknown; error: DefaultError } + +// Maps query options to Angular's signal-wrapped CreateQueryResult +type GetCreateQueryResult = GenericGetDefinedOrUndefinedQueryResult< + T, + InferDataAndError['data'], + CreateQueryResult< + InferDataAndError['data'], + InferDataAndError['error'] + >, + DefinedCreateQueryResult< + InferDataAndError['data'], + InferDataAndError['error'] + > +> + +// Maps query options to plain QueryObserverResult for combine function +type GetQueryObserverResult = GenericGetDefinedOrUndefinedQueryResult< + T, + InferDataAndError['data'], + QueryObserverResult< + InferDataAndError['data'], + InferDataAndError['error'] + >, + DefinedQueryObserverResult< + InferDataAndError['data'], + InferDataAndError['error'] + > +> /** * QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param @@ -201,6 +235,25 @@ export type QueriesResults< > : { [K in keyof T]: GetCreateQueryResult } +// Maps query options array to plain QueryObserverResult types for combine function +type RawQueriesResults< + T extends Array, + TResults extends Array = [], + TDepth extends ReadonlyArray = [], +> = TDepth['length'] extends MAXIMUM_DEPTH + ? Array + : T extends [] + ? [] + : T extends [infer Head] + ? [...TResults, GetQueryObserverResult] + : T extends [infer Head, ...infer Tails] + ? RawQueriesResults< + [...Tails], + [...TResults, GetQueryObserverResult], + [...TDepth, 1] + > + : { [K in keyof T]: GetQueryObserverResult } + export interface InjectQueriesOptions< T extends Array, TCombinedResult = QueriesResults, @@ -210,9 +263,14 @@ export interface InjectQueriesOptions< | readonly [ ...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries }, ] - combine?: (result: QueriesResults) => TCombinedResult + combine?: (result: RawQueriesResults) => TCombinedResult } +const methodsToExclude: Array> = ['refetch'] + +const hasPendingQueriesState = (results: Array): boolean => + results.some((result) => result.fetchStatus !== 'idle') + /** * @param optionsFn - A function that returns queries' options. * @param injector - The Angular injector to use. @@ -228,8 +286,24 @@ export function injectQueries< return runInInjectionContext(injector ?? inject(Injector), () => { const destroyRef = inject(DestroyRef) const ngZone = inject(NgZone) + const pendingTasks = inject(PendingTasks) const queryClient = inject(QueryClient) const isRestoring = injectIsRestoring() + let destroyed = false + let taskCleanupRef: (() => void) | null = null + + const startPendingTask = () => { + if (!taskCleanupRef && !destroyed) { + taskCleanupRef = pendingTasks.add() + } + } + + const stopPendingTask = () => { + if (taskCleanupRef) { + taskCleanupRef() + taskCleanupRef = null + } + } /** * Signal that has the default options from query client applied @@ -255,76 +329,125 @@ export function injectQueries< }) }) - const observerSignal = (() => { - let instance: QueriesObserver | null = null + const observerOptionsSignal = computed( + () => optionsSignal() as QueriesObserverOptions, + ) - return computed(() => { - return (instance ||= new QueriesObserver( - queryClient, - defaultedQueries(), - optionsSignal() as QueriesObserverOptions, - )) - }) - })() + // Computed without deps to lazy initialize the observer + const observerSignal = computed(() => { + return new QueriesObserver( + queryClient, + untracked(defaultedQueries), + untracked(observerOptionsSignal), + ) + }) const optimisticResultSignal = computed(() => observerSignal().getOptimisticResult( defaultedQueries(), - (optionsSignal() as QueriesObserverOptions).combine, + observerOptionsSignal().combine, ), ) // Do not notify on updates because of changes in the options because // these changes should already be reflected in the optimistic result. effect(() => { - observerSignal().setQueries( - defaultedQueries(), - optionsSignal() as QueriesObserverOptions, - ) + observerSignal().setQueries(defaultedQueries(), observerOptionsSignal()) }) - const optimisticCombinedResultSignal = computed(() => { - const [_optimisticResult, getCombinedResult, trackResult] = - optimisticResultSignal() - return getCombinedResult(trackResult()) + const optimisticResultSourceSignal = computed(() => { + const options = observerOptionsSignal() + return { queries: defaultedQueries(), combine: options.combine } }) - const resultFromSubscriberSignal = signal(null) + const resultSignal = linkedSignal({ + source: optimisticResultSourceSignal, + computation: () => { + const observer = untracked(observerSignal) + const [_optimisticResult, getCombinedResult, trackResult] = + observer.getOptimisticResult( + defaultedQueries(), + observerOptionsSignal().combine, + ) + return getCombinedResult(trackResult()) + }, + }) - effect(() => { + effect((onCleanup) => { const observer = observerSignal() - const [_optimisticResult, getCombinedResult] = optimisticResultSignal() - - untracked(() => { - const unsubscribe = isRestoring() - ? () => undefined - : ngZone.runOutsideAngular(() => - observer.subscribe( - notifyManager.batchCalls((state) => { - resultFromSubscriberSignal.set(getCombinedResult(state)) - }), - ), - ) - - destroyRef.onDestroy(unsubscribe) + const [optimisticResult, getCombinedResult] = optimisticResultSignal() + + if (isRestoring()) { + stopPendingTask() + return + } + + if (hasPendingQueriesState(optimisticResult)) { + startPendingTask() + } else { + stopPendingTask() + } + + const unsubscribe = untracked(() => + ngZone.runOutsideAngular(() => + observer.subscribe((state) => { + if (hasPendingQueriesState(state)) { + startPendingTask() + } else { + stopPendingTask() + } + + queueMicrotask(() => { + if (destroyed) return + notifyManager.batch(() => { + ngZone.run(() => { + resultSignal.set(getCombinedResult(state)) + }) + }) + }) + }), + ), + ) + + onCleanup(() => { + unsubscribe() + stopPendingTask() }) }) - const resultSignal = computed(() => { - const subscriberResult = resultFromSubscriberSignal() - const optimisticResult = optimisticCombinedResultSignal() - return subscriberResult ?? optimisticResult + // Angular does not use reactive getters on plain objects, so we wrap each + // QueryObserverResult in a signal-backed proxy to keep field-level tracking + // (`result.data()`, `result.status()`, etc.). + // Solid uses a related proxy approach in useQueries, but there it proxies + // object fields for store/resource reactivity rather than callable signals. + const createResultProxy = (index: number) => + signalProxy( + computed(() => (resultSignal() as Array)[index]!), + methodsToExclude, + ) + + // Keep this positional to match QueriesObserver semantics. + // Like Solid/Vue adapters, proxies are rebuilt from current observer output. + const proxiedResultsSignal = computed(() => + (resultSignal() as Array).map((_, index) => + createResultProxy(index), + ), + ) + + destroyRef.onDestroy(() => { + destroyed = true + stopPendingTask() }) return computed(() => { const result = resultSignal() const { combine } = optionsSignal() - return combine - ? result - : (result as QueriesResults).map((query) => - signalProxy(signal(query)), - ) + if (combine) { + return result + } + + return proxiedResultsSignal() as unknown as TCombinedResult }) }) as unknown as Signal } diff --git a/packages/angular-query-experimental/src/inject-query.ts b/packages/angular-query-experimental/src/inject-query.ts index 1dac0ab694..2161fb225a 100644 --- a/packages/angular-query-experimental/src/inject-query.ts +++ b/packages/angular-query-experimental/src/inject-query.ts @@ -6,7 +6,12 @@ import { runInInjectionContext, } from '@angular/core' import { createBaseQuery } from './create-base-query' -import type { DefaultError, QueryKey } from '@tanstack/query-core' +import type { MethodKeys } from './signal-proxy' +import type { + DefaultError, + QueryKey, + QueryObserverResult, +} from '@tanstack/query-core' import type { CreateQueryOptions, CreateQueryResult, @@ -31,11 +36,15 @@ export interface InjectQueryOptions { * * **Basic example** * ```ts + * import { lastValueFrom } from 'rxjs' + * * class ServiceOrComponent { * query = injectQuery(() => ({ * queryKey: ['repoData'], * queryFn: () => - * this.#http.get('https://api.github.com/repos/tanstack/query'), + * lastValueFrom( + * this.#http.get('https://api.github.com/repos/tanstack/query'), + * ), * })) * } * ``` @@ -82,11 +91,15 @@ export function injectQuery< * * **Basic example** * ```ts + * import { lastValueFrom } from 'rxjs' + * * class ServiceOrComponent { * query = injectQuery(() => ({ * queryKey: ['repoData'], * queryFn: () => - * this.#http.get('https://api.github.com/repos/tanstack/query'), + * lastValueFrom( + * this.#http.get('https://api.github.com/repos/tanstack/query'), + * ), * })) * } * ``` @@ -133,11 +146,15 @@ export function injectQuery< * * **Basic example** * ```ts + * import { lastValueFrom } from 'rxjs' + * * class ServiceOrComponent { * query = injectQuery(() => ({ * queryKey: ['repoData'], * queryFn: () => - * this.#http.get('https://api.github.com/repos/tanstack/query'), + * lastValueFrom( + * this.#http.get('https://api.github.com/repos/tanstack/query'), + * ), * })) * } * ``` @@ -184,11 +201,15 @@ export function injectQuery< * * **Basic example** * ```ts + * import { lastValueFrom } from 'rxjs' + * * class ServiceOrComponent { * query = injectQuery(() => ({ * queryKey: ['repoData'], * queryFn: () => - * this.#http.get('https://api.github.com/repos/tanstack/query'), + * lastValueFrom( + * this.#http.get('https://api.github.com/repos/tanstack/query'), + * ), * })) * } * ``` @@ -221,6 +242,8 @@ export function injectQuery( ) { !options?.injector && assertInInjectionContext(injectQuery) return runInInjectionContext(options?.injector ?? inject(Injector), () => - createBaseQuery(injectQueryFn, QueryObserver), + createBaseQuery(injectQueryFn, QueryObserver, methodsToExclude), ) as unknown as CreateQueryResult } + +const methodsToExclude: Array> = ['refetch'] diff --git a/packages/angular-query-experimental/src/pending-tasks-compat.ts b/packages/angular-query-experimental/src/pending-tasks-compat.ts deleted file mode 100644 index e156996993..0000000000 --- a/packages/angular-query-experimental/src/pending-tasks-compat.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { InjectionToken, inject } from '@angular/core' -import * as ng from '@angular/core' -import { noop } from '@tanstack/query-core' - -type PendingTasksCompat = { add: () => PendingTaskRef } - -export type PendingTaskRef = () => void - -export const PENDING_TASKS = new InjectionToken( - 'PENDING_TASKS', - { - factory: (): PendingTasksCompat => { - // Access via Reflect so bundlers stay quiet when the token is absent (Angular < 19). - const token = Reflect.get(ng, 'PendingTasks') as unknown as - | Parameters[0] - | undefined - - const svc: PendingTasksCompat | null = token - ? (inject(token, { optional: true }) as PendingTasksCompat | null) - : null - - // Without PendingTasks we fall back to a stable no-op shim. - return { - add: svc ? () => svc.add() : () => noop, - } - }, - }, -) diff --git a/packages/angular-query-experimental/src/query-options.ts b/packages/angular-query-experimental/src/query-options.ts index 069472b903..780e14ce1b 100644 --- a/packages/angular-query-experimental/src/query-options.ts +++ b/packages/angular-query-experimental/src/query-options.ts @@ -4,7 +4,6 @@ import type { InitialDataFunction, NonUndefinedGuard, OmitKeyof, - QueryFunction, QueryKey, SkipToken, } from '@tanstack/query-core' @@ -42,14 +41,10 @@ export type DefinedInitialDataOptions< TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = Omit< - CreateQueryOptions, - 'queryFn' -> & { +> = CreateQueryOptions & { initialData: | NonUndefinedGuard | (() => NonUndefinedGuard) - queryFn?: QueryFunction } /** diff --git a/packages/angular-query-experimental/src/signal-proxy.ts b/packages/angular-query-experimental/src/signal-proxy.ts index e2a9de345f..fa857dcda2 100644 --- a/packages/angular-query-experimental/src/signal-proxy.ts +++ b/packages/angular-query-experimental/src/signal-proxy.ts @@ -1,46 +1,61 @@ import { computed, untracked } from '@angular/core' import type { Signal } from '@angular/core' -export type MapToSignals = { - [K in keyof T]: T[K] extends Function ? T[K] : Signal +export type MethodKeys = { + [K in keyof T]: T[K] extends (...args: Array) => any ? K : never +}[keyof T] + +export type MapToSignals = never> = { + [K in keyof T]: K extends TExcludeFields ? T[K] : Signal } /** * Exposes fields of an object passed via an Angular `Signal` as `Computed` signals. * Functions on the object are passed through as-is. * @param inputSignal - `Signal` that must return an object. + * @param excludeFields - Array of function property names that should NOT be converted to signals. * @returns A proxy object with the same fields as the input object, but with each field wrapped in a `Computed` signal. */ -export function signalProxy>( - inputSignal: Signal, -) { - const internalState = {} as MapToSignals +export function signalProxy< + TInput extends Record, + const TExcludeFields extends ReadonlyArray> = [], +>(inputSignal: Signal, excludeFields: TExcludeFields) { + const internalState = {} as MapToSignals + const excludeFieldsArray = excludeFields as ReadonlyArray - return new Proxy>(internalState, { - get(target, prop) { - // first check if we have it in our internal state and return it - const computedField = target[prop] - if (computedField) return computedField + return new Proxy>( + internalState, + { + get(target, prop) { + // first check if we have it in our internal state and return it + const computedField = target[prop] + if (computedField) return computedField - // then, check if it's a function on the resultState and return it - const targetField = untracked(inputSignal)[prop] - if (typeof targetField === 'function') return targetField + // if it is an excluded function, return it without tracking + if (excludeFieldsArray.includes(prop as string)) { + const fn = (...args: Parameters) => + untracked(inputSignal)[prop](...args) + // @ts-expect-error + target[prop] = fn + return fn + } - // finally, create a computed field, store it and return it - // @ts-expect-error - return (target[prop] = computed(() => inputSignal()[prop])) - }, - has(_, prop) { - return !!untracked(inputSignal)[prop] - }, - ownKeys() { - return Reflect.ownKeys(untracked(inputSignal)) - }, - getOwnPropertyDescriptor() { - return { - enumerable: true, - configurable: true, - } + // otherwise, make a computed field + // @ts-expect-error + return (target[prop] = computed(() => inputSignal()[prop])) + }, + has(_, prop) { + return !!untracked(inputSignal)[prop] + }, + ownKeys() { + return Reflect.ownKeys(untracked(inputSignal)) + }, + getOwnPropertyDescriptor() { + return { + enumerable: true, + configurable: true, + } + }, }, - }) + ) } diff --git a/packages/angular-query-experimental/src/types.ts b/packages/angular-query-experimental/src/types.ts index d71bec248f..5c36a6bb27 100644 --- a/packages/angular-query-experimental/src/types.ts +++ b/packages/angular-query-experimental/src/types.ts @@ -16,31 +16,25 @@ import type { QueryObserverResult, } from '@tanstack/query-core' import type { Signal } from '@angular/core' -import type { MapToSignals } from './signal-proxy' +import type { MapToSignals, MethodKeys } from './signal-proxy' -export interface CreateBaseQueryOptions< +export type CreateBaseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> extends QueryObserverOptions< - TQueryFnData, - TError, - TData, - TQueryData, - TQueryKey -> {} +> = QueryObserverOptions -export interface CreateQueryOptions< +export type CreateQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> extends OmitKeyof< +> = OmitKeyof< CreateBaseQueryOptions, 'suspense' -> {} +> type CreateStatusBasedQueryResult< TStatus extends QueryObserverResult['status'], @@ -94,7 +88,10 @@ export type CreateBaseQueryResult< TError = DefaultError, TState = QueryObserverResult, > = BaseQueryNarrowing & - MapToSignals> + MapToSignals< + OmitKeyof, + MethodKeys> + > export type CreateQueryResult< TData = unknown, @@ -106,13 +103,19 @@ export type DefinedCreateQueryResult< TError = DefaultError, TState = DefinedQueryObserverResult, > = BaseQueryNarrowing & - MapToSignals> + MapToSignals< + OmitKeyof, + MethodKeys> + > export type CreateInfiniteQueryResult< TData = unknown, TError = DefaultError, > = BaseQueryNarrowing & - MapToSignals> + MapToSignals< + InfiniteQueryObserverResult, + MethodKeys> + > export type DefinedCreateInfiniteQueryResult< TData = unknown, @@ -121,7 +124,10 @@ export type DefinedCreateInfiniteQueryResult< TData, TError >, -> = MapToSignals +> = MapToSignals< + TDefinedInfiniteQueryObserver, + MethodKeys +> export interface CreateMutationOptions< TData = unknown, @@ -270,4 +276,7 @@ export type CreateMutationResult< TOnMutateResult >, > = BaseMutationNarrowing & - MapToSignals> + MapToSignals< + OmitKeyof, + MethodKeys> + > diff --git a/packages/angular-query-persist-client/package.json b/packages/angular-query-persist-client/package.json index 4ed0e7cc2b..083696df49 100644 --- a/packages/angular-query-persist-client/package.json +++ b/packages/angular-query-persist-client/package.json @@ -20,11 +20,6 @@ "compile": "tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "npm-run-all --serial test:types:*", - "test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js --build", - "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js --build", - "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js --build", - "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build", - "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1af1216e90..37ffa0372a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,7 +174,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -214,7 +214,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -260,7 +260,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -303,7 +303,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -343,7 +343,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -386,7 +386,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -426,7 +426,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -469,7 +469,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -512,7 +512,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -555,7 +555,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -595,7 +595,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -2000,7 +2000,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.0.0(8379d9408f101aa649c5ec9eae189324) + version: 20.0.0(e7d71ef5d6f07f95ae57395d944dd75a) '@angular/cli': specifier: ^20.0.0 version: 20.0.0(@types/node@22.15.3)(chokidar@4.0.3) @@ -2262,22 +2262,25 @@ importers: devDependencies: '@angular/common': specifier: ^20.0.0 - version: 20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + version: 20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) '@angular/compiler': specifier: ^20.0.0 version: 20.0.0 '@angular/core': specifier: ^20.0.0 - version: 20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0) + version: 20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0) '@angular/platform-browser': specifier: ^20.0.0 - version: 20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)) + version: 20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)) + '@angular/platform-server': + specifier: ^20.0.0 + version: 20.3.15(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/compiler@20.0.0)(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2) '@tanstack/query-test-utils': specifier: workspace:* version: link:../query-test-utils '@testing-library/angular': specifier: ^18.0.0 - version: 18.0.0(b638270d50b9f611fb362719c9f1adf5) + version: 18.0.0(c06d8d4c2ce1594f4574b3aa35a1592e) npm-run-all2: specifier: ^5.0.0 version: 5.0.2 @@ -2293,6 +2296,9 @@ importers: vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.8.3)(vite@6.4.1(@types/node@22.15.3)(jiti@2.5.1)(less@4.3.0)(lightningcss@1.30.1)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.1)(yaml@2.8.1)) + zone.js: + specifier: ^0.16.0 + version: 0.16.0 optionalDependencies: '@tanstack/query-devtools': specifier: workspace:* @@ -3023,6 +3029,16 @@ packages: '@angular/animations': optional: true + '@angular/platform-server@20.3.15': + resolution: {integrity: sha512-OB3/ztCREeZ0pe2P+43Nah9Xq2Y79fN6mbsOY1JwwYxkM8ZN1WkSP11xlHHwAcoquHP7uFPhXwJqgTHBqGqkcw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@angular/common': 20.3.15 + '@angular/compiler': 20.3.15 + '@angular/core': 20.3.15 + '@angular/platform-browser': 20.3.15 + rxjs: ^6.5.3 || ^7.4.0 + '@angular/router@20.0.0': resolution: {integrity: sha512-RQ7rU4NaZDSvvOfMZQmB50q7de+jrHYb+f0ExLKBvr80B1MK3oc9VvI2BzBkGfM4aGx71MMa0UizjOiT/31kqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -15372,6 +15388,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} @@ -15527,6 +15544,10 @@ packages: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} + xhr2@0.2.1: + resolution: {integrity: sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==} + engines: {node: '>= 6'} + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -15676,6 +15697,9 @@ packages: zone.js@0.15.0: resolution: {integrity: sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==} + zone.js@0.16.0: + resolution: {integrity: sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -15746,7 +15770,14 @@ snapshots: '@angular/core': 20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0) tslib: 2.8.1 - '@angular/build@20.0.0(8379d9408f101aa649c5ec9eae189324)': + '@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))': + dependencies: + '@angular/common': 20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/core': 20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0) + tslib: 2.8.1 + optional: true + + '@angular/build@20.0.0(e7d71ef5d6f07f95ae57395d944dd75a)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2000.0(chokidar@4.0.3) @@ -15781,6 +15812,7 @@ snapshots: optionalDependencies: '@angular/core': 20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0) '@angular/platform-browser': 20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-server': 20.3.15(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.0.0)(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) less: 4.3.0 lmdb: 3.3.0 postcss: 8.5.6 @@ -15829,6 +15861,12 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 + '@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2)': + dependencies: + '@angular/core': 20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0) + rxjs: 7.8.2 + tslib: 2.8.1 + '@angular/compiler-cli@20.0.0(@angular/compiler@20.0.0)(typescript@5.8.3)': dependencies: '@angular/compiler': 20.0.0 @@ -15857,6 +15895,14 @@ snapshots: '@angular/compiler': 20.0.0 zone.js: 0.15.0 + '@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)': + dependencies: + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + '@angular/compiler': 20.0.0 + zone.js: 0.16.0 + '@angular/forms@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': dependencies: '@angular/common': 20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) @@ -15873,6 +15919,35 @@ snapshots: optionalDependencies: '@angular/animations': 20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-browser@20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))': + dependencies: + '@angular/common': 20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/core': 20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0) + tslib: 2.8.1 + optionalDependencies: + '@angular/animations': 20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)) + + '@angular/platform-server@20.3.15(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.0.0)(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/compiler': 20.0.0 + '@angular/core': 20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/platform-browser': 20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)) + rxjs: 7.8.2 + tslib: 2.8.1 + xhr2: 0.2.1 + optional: true + + '@angular/platform-server@20.3.15(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/compiler@20.0.0)(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/compiler': 20.0.0 + '@angular/core': 20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0) + '@angular/platform-browser': 20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)) + rxjs: 7.8.2 + tslib: 2.8.1 + xhr2: 0.2.1 + '@angular/router@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': dependencies: '@angular/common': 20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) @@ -15881,6 +15956,14 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 + '@angular/router@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/core': 20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0) + '@angular/platform-browser': 20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)) + rxjs: 7.8.2 + tslib: 2.8.1 + '@arethetypeswrong/cli@0.15.3': dependencies: '@arethetypeswrong/core': 0.15.1 @@ -20108,6 +20191,15 @@ snapshots: '@testing-library/dom': 10.4.0 tslib: 2.8.1 + '@testing-library/angular@18.0.0(c06d8d4c2ce1594f4574b3aa35a1592e)': + dependencies: + '@angular/common': 20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/core': 20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0) + '@angular/platform-browser': 20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)) + '@angular/router': 20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@20.0.0(@angular/animations@20.0.0(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.0.0(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.0.0(@angular/compiler@20.0.0)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2) + '@testing-library/dom': 10.4.0 + tslib: 2.8.1 + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 @@ -31029,6 +31121,8 @@ snapshots: xdg-basedir@5.1.0: {} + xhr2@0.2.1: {} + xml-name-validator@4.0.0: {} xml-name-validator@5.0.0: {} @@ -31163,4 +31257,6 @@ snapshots: zone.js@0.15.0: {} + zone.js@0.16.0: {} + zwitch@2.0.4: {}