diff --git a/docs/config.json b/docs/config.json index ef151ed38f..8d14e3c7da 100644 --- a/docs/config.json +++ b/docs/config.json @@ -1046,6 +1046,10 @@ "label": "Basic", "to": "framework/angular/examples/basic" }, + { + "label": "Auto Refetching / Polling / Realtime", + "to": "framework/angular/examples/auto-refetching" + }, { "label": "Pagination", "to": "framework/angular/examples/pagination" diff --git a/examples/angular/auto-refetching/.devcontainer/devcontainer.json b/examples/angular/auto-refetching/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..365adf8f4c --- /dev/null +++ b/examples/angular/auto-refetching/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22" +} diff --git a/examples/angular/auto-refetching/.eslintrc.cjs b/examples/angular/auto-refetching/.eslintrc.cjs new file mode 100644 index 0000000000..cca134ce16 --- /dev/null +++ b/examples/angular/auto-refetching/.eslintrc.cjs @@ -0,0 +1,6 @@ +// @ts-check + +/** @type {import('eslint').Linter.Config} */ +const config = {} + +module.exports = config diff --git a/examples/angular/auto-refetching/README.md b/examples/angular/auto-refetching/README.md new file mode 100644 index 0000000000..571955a305 --- /dev/null +++ b/examples/angular/auto-refetching/README.md @@ -0,0 +1,6 @@ +# TanStack Query Angular auto-refetching example + +To run this example: + +- `npm install` or `yarn` or `pnpm i` or `bun i` +- `npm run start` or `yarn start` or `pnpm start` or `bun start` diff --git a/examples/angular/auto-refetching/angular.json b/examples/angular/auto-refetching/angular.json new file mode 100644 index 0000000000..2fd625bebe --- /dev/null +++ b/examples/angular/auto-refetching/angular.json @@ -0,0 +1,104 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "auto-refetching": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "outputPath": "dist/auto-refetching", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico", "src/assets"], + "styles": [], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "auto-refetching:build:production" + }, + "development": { + "buildTarget": "auto-refetching:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n", + "options": { + "buildTarget": "auto-refetching:build" + } + } + } + } + } +} diff --git a/examples/angular/auto-refetching/package.json b/examples/angular/auto-refetching/package.json new file mode 100644 index 0000000000..f4867de9a8 --- /dev/null +++ b/examples/angular/auto-refetching/package.json @@ -0,0 +1,28 @@ +{ + "name": "@tanstack/query-example-angular-auto-refetching", + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "private": true, + "dependencies": { + "@angular/common": "^19.1.0-next.0", + "@angular/compiler": "^19.1.0-next.0", + "@angular/core": "^19.1.0-next.0", + "@angular/platform-browser": "^19.1.0-next.0", + "@angular/platform-browser-dynamic": "^19.1.0-next.0", + "@tanstack/angular-query-experimental": "^5.62.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.3", + "zone.js": "^0.15.0" + }, + "devDependencies": { + "@angular/build": "^19.0.2", + "@angular/cli": "^19.0.2", + "@angular/compiler-cli": "^19.1.0-next.0", + "typescript": "5.7.2" + } +} diff --git a/examples/angular/auto-refetching/src/app/app.component.ts b/examples/angular/auto-refetching/src/app/app.component.ts new file mode 100644 index 0000000000..180391550b --- /dev/null +++ b/examples/angular/auto-refetching/src/app/app.component.ts @@ -0,0 +1,11 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { AutoRefetchingExampleComponent } from './components/auto-refetching.component' + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-root', + standalone: true, + template: ``, + imports: [AutoRefetchingExampleComponent], +}) +export class AppComponent {} diff --git a/examples/angular/auto-refetching/src/app/app.config.ts b/examples/angular/auto-refetching/src/app/app.config.ts new file mode 100644 index 0000000000..65a84a0c25 --- /dev/null +++ b/examples/angular/auto-refetching/src/app/app.config.ts @@ -0,0 +1,28 @@ +import { + provideHttpClient, + withFetch, + withInterceptors, +} from '@angular/common/http' +import { + QueryClient, + provideTanStackQuery, + withDevtools, +} from '@tanstack/angular-query-experimental' +import { mockInterceptor } from './interceptor/mock-api.interceptor' +import type { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [ + provideHttpClient(withFetch(), withInterceptors([mockInterceptor])), + provideTanStackQuery( + new QueryClient({ + defaultOptions: { + queries: { + gcTime: 1000 * 60 * 60 * 24, // 24 hours + }, + }, + }), + withDevtools(), + ), + ], +} diff --git a/examples/angular/auto-refetching/src/app/components/auto-refetching.component.html b/examples/angular/auto-refetching/src/app/components/auto-refetching.component.html new file mode 100644 index 0000000000..f0359aae9b --- /dev/null +++ b/examples/angular/auto-refetching/src/app/components/auto-refetching.component.html @@ -0,0 +1,35 @@ +
+

Auto Refetch with stale-time set to {{ intervalMs() }}ms

+

+ This example is best experienced on your own machine, where you can open + multiple tabs to the same localhost server and see your changes propagate + between the two. +

+ +

Todo List

+ + + +
+ +
+
diff --git a/examples/angular/auto-refetching/src/app/components/auto-refetching.component.ts b/examples/angular/auto-refetching/src/app/components/auto-refetching.component.ts new file mode 100644 index 0000000000..01e86f2c9d --- /dev/null +++ b/examples/angular/auto-refetching/src/app/components/auto-refetching.component.ts @@ -0,0 +1,46 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + signal, +} from '@angular/core' +import { + injectMutation, + injectQuery, +} from '@tanstack/angular-query-experimental' +import { NgStyle } from '@angular/common' +import { TasksService } from '../services/tasks.service' + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'auto-refetching-example', + standalone: true, + templateUrl: './auto-refetching.component.html', + imports: [NgStyle], +}) +export class AutoRefetchingExampleComponent { + #tasksService = inject(TasksService) + + intervalMs = signal(1000) + + tasks = injectQuery(() => this.#tasksService.allTasks(this.intervalMs())) + + addMutation = injectMutation(() => this.#tasksService.addTask()) + clearMutation = injectMutation(() => this.#tasksService.clearAllTasks()) + + clearTasks() { + this.clearMutation.mutate() + } + + inputChange($event: Event) { + const target = $event.target as HTMLInputElement + this.intervalMs.set(Number(target.value)) + } + + addItem($event: Event) { + const target = $event.target as HTMLInputElement + const value = target.value + this.addMutation.mutate(value) + target.value = '' + } +} diff --git a/examples/angular/auto-refetching/src/app/interceptor/mock-api.interceptor.ts b/examples/angular/auto-refetching/src/app/interceptor/mock-api.interceptor.ts new file mode 100644 index 0000000000..273e0d5a67 --- /dev/null +++ b/examples/angular/auto-refetching/src/app/interceptor/mock-api.interceptor.ts @@ -0,0 +1,46 @@ +/** + * MockApiInterceptor is used to simulate API responses for `/api/tasks` endpoints. + * It handles the following operations: + * - GET: Fetches all tasks from localStorage. + * - POST: Adds a new task to localStorage. + * - DELETE: Clears all tasks from localStorage. + * Simulated responses include a delay to mimic network latency. + */ +import { HttpResponse } from '@angular/common/http' +import { delay, of } from 'rxjs' +import type { + HttpEvent, + HttpHandlerFn, + HttpInterceptorFn, + HttpRequest, +} from '@angular/common/http' +import type { Observable } from 'rxjs' + +export const mockInterceptor: HttpInterceptorFn = ( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> => { + const respondWith = (status: number, body: any) => + of(new HttpResponse({ status, body })).pipe(delay(100)) + if (req.url === '/api/tasks') { + switch (req.method) { + case 'GET': + return respondWith( + 200, + JSON.parse(localStorage.getItem('tasks') || '[]'), + ) + case 'POST': + const tasks = JSON.parse(localStorage.getItem('tasks') || '[]') + tasks.push(req.body) + localStorage.setItem('tasks', JSON.stringify(tasks)) + return respondWith(201, { + status: 'success', + task: req.body, + }) + case 'DELETE': + localStorage.removeItem('tasks') + return respondWith(200, { status: 'success' }) + } + } + return next(req) +} diff --git a/examples/angular/auto-refetching/src/app/services/tasks.service.ts b/examples/angular/auto-refetching/src/app/services/tasks.service.ts new file mode 100644 index 0000000000..7fd8ce9a29 --- /dev/null +++ b/examples/angular/auto-refetching/src/app/services/tasks.service.ts @@ -0,0 +1,59 @@ +import { HttpClient } from '@angular/common/http' +import { Injectable, inject } from '@angular/core' +import { + QueryClient, + mutationOptions, + queryOptions, +} from '@tanstack/angular-query-experimental' + +import { lastValueFrom } from 'rxjs' + +@Injectable({ + providedIn: 'root', +}) +export class TasksService { + #queryClient = inject(QueryClient) // Manages query state and caching + #http = inject(HttpClient) // Handles HTTP requests + + /** + * Fetches all tasks from the API. + * Returns an observable containing an array of task strings. + */ + allTasks = (intervalMs: number) => + queryOptions({ + queryKey: ['tasks'], + queryFn: () => { + return lastValueFrom(this.#http.get>('/api/tasks')) + }, + refetchInterval: intervalMs, + }) + + /** + * Creates a mutation for adding a task. + * On success, invalidates and refetches the "tasks" query cache to update the task list. + */ + addTask() { + return mutationOptions({ + mutationFn: (task: string) => + lastValueFrom(this.#http.post('/api/tasks', task)), + mutationKey: ['tasks'], + onSuccess: () => { + this.#queryClient.invalidateQueries({ queryKey: ['tasks'] }) + }, + }) + } + + /** + * Creates a mutation for clearing all tasks. + * On success, invalidates and refetches the "tasks" query cache to ensure consistency. + */ + clearAllTasks() { + return mutationOptions({ + mutationFn: () => lastValueFrom(this.#http.delete('/api/tasks')), + mutationKey: ['clearTasks'], + onSuccess: () => { + this.#queryClient.invalidateQueries({ queryKey: ['tasks'] }) + }, + }) + } +} diff --git a/examples/angular/auto-refetching/src/favicon.ico b/examples/angular/auto-refetching/src/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/auto-refetching/src/favicon.ico differ diff --git a/examples/angular/auto-refetching/src/index.html b/examples/angular/auto-refetching/src/index.html new file mode 100644 index 0000000000..d775852c72 --- /dev/null +++ b/examples/angular/auto-refetching/src/index.html @@ -0,0 +1,13 @@ + + + + + TanStack Query Angular auto-refetching example + + + + + + + + diff --git a/examples/angular/auto-refetching/src/main.ts b/examples/angular/auto-refetching/src/main.ts new file mode 100644 index 0000000000..b3963de6e2 --- /dev/null +++ b/examples/angular/auto-refetching/src/main.ts @@ -0,0 +1,13 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent, appConfig) + .then(() => { + // an simple endpoint for getting current list + localStorage.setItem( + 'tasks', + JSON.stringify(['Item 1', 'Item 2', 'Item 3']), + ) + }) + .catch((err) => console.error(err)) diff --git a/examples/angular/auto-refetching/tsconfig.app.json b/examples/angular/auto-refetching/tsconfig.app.json new file mode 100644 index 0000000000..5b9d3c5ecb --- /dev/null +++ b/examples/angular/auto-refetching/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/auto-refetching/tsconfig.json b/examples/angular/auto-refetching/tsconfig.json new file mode 100644 index 0000000000..d0d73c8beb --- /dev/null +++ b/examples/angular/auto-refetching/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictStandalone": true, + "strictTemplates": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5973feb493..009d230c58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,49 @@ importers: specifier: ^2.0.4 version: 2.0.5(@types/node@22.9.3)(jsdom@25.0.1)(less@4.2.1)(lightningcss@1.27.0)(sass@1.81.0)(terser@5.31.6) + examples/angular/auto-refetching: + dependencies: + '@angular/common': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) + '@angular/compiler': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + '@angular/core': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) + '@angular/platform-browser': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + '@angular/platform-browser-dynamic': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))) + '@tanstack/angular-query-experimental': + specifier: ^5.62.2 + version: link:../../../packages/angular-query-experimental + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + tslib: + specifier: ^2.6.3 + version: 2.8.1 + zone.js: + specifier: ^0.15.0 + version: 0.15.0 + devDependencies: + '@angular/build': + specifier: ^19.0.2 + version: 19.0.2(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.9.3)(chokidar@4.0.1)(less@4.2.1)(lightningcss@1.27.0)(postcss@8.4.49)(tailwindcss@3.4.7)(terser@5.31.6)(typescript@5.7.2) + '@angular/cli': + specifier: ^19.0.2 + version: 19.0.2(@types/node@22.9.3)(chokidar@4.0.1) + '@angular/compiler-cli': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2) + typescript: + specifier: 5.7.2 + version: 5.7.2 + examples/angular/basic: dependencies: '@angular/common': @@ -8948,7 +8991,6 @@ packages: critters@0.0.24: resolution: {integrity: sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q==} - deprecated: Ownership of Critters has moved to the Nuxt team, who will be maintaining the project going forward. If you'd like to keep using Critters, please switch to the actively-maintained fork at https://github.com/danielroe/beasties croner@8.1.0: resolution: {integrity: sha512-sz990XOUPR8dG/r5BRKMBd15MYDDUu8oeSaxFD5DqvNgHSZw8Psd1s689/IGET7ezxRMiNlCIyGeY1Gvxp/MLg==} @@ -17021,7 +17063,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) '@inquirer/confirm': 5.0.2(@types/node@22.9.3) - '@vitejs/plugin-basic-ssl': 1.1.0(vite@5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.80.7)(terser@5.31.6)) + '@vitejs/plugin-basic-ssl': 1.1.0(vite@5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.81.0)(terser@5.31.6)) beasties: 0.1.0 browserslist: 4.24.2 esbuild: 0.24.0 @@ -23419,9 +23461,9 @@ snapshots: dependencies: vite: 5.1.7(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.27.0)(sass@1.71.1)(terser@5.29.1) - '@vitejs/plugin-basic-ssl@1.1.0(vite@5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.80.7)(terser@5.31.6))': + '@vitejs/plugin-basic-ssl@1.1.0(vite@5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.81.0)(terser@5.31.6))': dependencies: - vite: 5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.80.7)(terser@5.31.6) + vite: 5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.81.0)(terser@5.31.6) '@vitejs/plugin-basic-ssl@1.1.0(vite@5.4.6(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.27.0)(sass@1.77.6)(terser@5.31.6))': dependencies: