UNPKG

@tanstack/angular-db

Version:

Angular integration for @tanstack/db

336 lines (271 loc) 8.33 kB
--- name: angular-db description: > Angular bindings for TanStack DB. injectLiveQuery inject function with Angular signals (Signal<T>) for all return values. Reactive params pattern ({ params: () => T, query: ({ params, q }) => QueryBuilder }) for dynamic queries. Must be called in injection context. Angular 17+ control flow (@if, @for) and signal inputs supported. Import from @tanstack/angular-db (re-exports all of @tanstack/db). type: framework library: db framework: angular library_version: '0.6.0' requires: - db-core sources: - 'TanStack/db:docs/framework/angular/overview.md' - 'TanStack/db:packages/angular-db/src/index.ts' --- This skill builds on db-core. Read it first for collection setup, query builder, and mutation patterns. # TanStack DB — Angular ## Setup ```typescript import { Component } from '@angular/core' import { injectLiveQuery, eq, not } from '@tanstack/angular-db' @Component({ selector: 'app-todo-list', standalone: true, template: ` @if (query.isLoading()) { <div>Loading...</div> } @else { <ul> @for (todo of query.data(); track todo.id) { <li>{{ todo.text }}</li> } </ul> } `, }) export class TodoListComponent { query = injectLiveQuery((q) => q .from({ todos: todosCollection }) .where(({ todos }) => not(todos.completed)) .orderBy(({ todos }) => todos.created_at, 'asc'), ) } ``` `@tanstack/angular-db` re-exports everything from `@tanstack/db`. ## Inject Function ### injectLiveQuery Returns an object with Angular `Signal<T>` properties — call with `()` in templates: ```typescript // Static query — no reactive dependencies const query = injectLiveQuery((q) => q.from({ todo: todoCollection })) // query.data() → Array<T> // query.status() → CollectionStatus | 'disabled' // query.isLoading(), query.isReady(), query.isError() // query.isIdle(), query.isCleanedUp() (seldom used) // query.state() → Map<TKey, T> // query.collection() → Collection | null // Reactive params — re-runs when params change const query = injectLiveQuery({ params: () => ({ minPriority: this.minPriority() }), query: ({ params, q }) => q .from({ todo: todoCollection }) .where(({ todo }) => gt(todo.priority, params.minPriority)), }) // Config object const query = injectLiveQuery({ query: (q) => q.from({ todo: todoCollection }), gcTime: 60000, }) // Pre-created collection const query = injectLiveQuery(preloadedCollection) // Conditional query — return undefined/null to disable const query = injectLiveQuery({ params: () => ({ userId: this.userId() }), query: ({ params, q }) => { if (!params.userId) return undefined return q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.userId, params.userId)) }, }) ``` ## Angular-Specific Patterns ### Reactive params with signals ```typescript @Component({ selector: 'app-filtered-todos', standalone: true, template: `<div>{{ query.data().length }} todos</div>`, }) export class FilteredTodosComponent { minPriority = signal(5) query = injectLiveQuery({ params: () => ({ minPriority: this.minPriority() }), query: ({ params, q }) => q .from({ todos: todosCollection }) .where(({ todos }) => gt(todos.priority, params.minPriority)), }) } ``` When `params()` return value changes, the previous collection is disposed and a new query is created. ### Signal inputs (Angular 17+) ```typescript @Component({ selector: 'app-user-todos', standalone: true, template: `<div>{{ query.data().length }} todos</div>`, }) export class UserTodosComponent { userId = input.required<number>() query = injectLiveQuery({ params: () => ({ userId: this.userId() }), query: ({ params, q }) => q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.userId, params.userId)), }) } ``` ### Legacy @Input (Angular 16) ```typescript export class UserTodosComponent { @Input({ required: true }) userId!: number query = injectLiveQuery({ params: () => ({ userId: this.userId }), query: ({ params, q }) => q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.userId, params.userId)), }) } ``` ### Template syntax Angular 17+ control flow: ```html @if (query.isLoading()) { <div>Loading...</div> } @else { @for (todo of query.data(); track todo.id) { <li>{{ todo.text }}</li> } } ``` Angular 16 structural directives: ```html <div *ngIf="query.isLoading()">Loading...</div> <li *ngFor="let todo of query.data(); trackBy: trackById">{{ todo.text }}</li> ``` ## Includes (Hierarchical Data) When a query uses includes (subqueries in `select`), each child field is a live `Collection` by default. Subscribe to it with `injectLiveQuery` in a child component: ```typescript @Component({ selector: 'app-project-list', standalone: true, imports: [IssueListComponent], template: ` @for (project of query.data(); track project.id) { <div> {{ project.name }} <app-issue-list [issuesCollection]="project.issues" /> </div> } `, }) export class ProjectListComponent { query = injectLiveQuery((q) => q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, issues: q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) .select(({ i }) => ({ id: i.id, title: i.title })), })), ) } // Child component subscribes to the child Collection @Component({ selector: 'app-issue-list', standalone: true, template: ` @for (issue of query.data(); track issue.id) { <li>{{ issue.title }}</li> } `, }) export class IssueListComponent { issuesCollection = input.required<Collection>() query = injectLiveQuery(this.issuesCollection()) } ``` With `toArray()`, child results are plain arrays and the parent re-emits on child changes: ```typescript import { toArray, eq } from '@tanstack/angular-db' query = injectLiveQuery((q) => q.from({ p: projectsCollection }).select(({ p }) => ({ id: p.id, name: p.name, issues: toArray( q .from({ i: issuesCollection }) .where(({ i }) => eq(i.projectId, p.id)) .select(({ i }) => ({ id: i.id, title: i.title })), ), })), ) // project.issues is a plain array — no child component subscription needed ``` See db-core/live-queries/SKILL.md for full includes rules (correlation conditions, nested includes, aggregates). ## Common Mistakes ### CRITICAL Using injectLiveQuery outside injection context Wrong: ```typescript export class TodoComponent { ngOnInit() { this.query = injectLiveQuery((q) => q.from({ todo: todoCollection })) } } ``` Correct: ```typescript export class TodoComponent { query = injectLiveQuery((q) => q.from({ todo: todoCollection })) } ``` `injectLiveQuery` calls `assertInInjectionContext` internally — it must be called during construction (field initializer or constructor), not in lifecycle hooks. Source: packages/angular-db/src/index.ts ### HIGH Using query function for reactive values instead of params Wrong: ```typescript export class FilteredComponent { status = signal('active') query = injectLiveQuery((q) => q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.status, this.status())), ) } ``` Correct: ```typescript export class FilteredComponent { status = signal('active') query = injectLiveQuery({ params: () => ({ status: this.status() }), query: ({ params, q }) => q .from({ todo: todoCollection }) .where(({ todo }) => eq(todo.status, params.status)), }) } ``` The plain query function overload does not track Angular signal reads. Use the `params` pattern to make reactive values trigger query re-creation. Source: packages/angular-db/src/index.ts ### MEDIUM Forgetting to call signals in templates Wrong: ```html <div>{{ query.data.length }}</div> ``` Correct: ```html <div>{{ query.data().length }}</div> ``` All return values are Angular signals. Without `()`, you get the signal object, not the value. See also: db-core/live-queries/SKILL.md — for query builder API. See also: db-core/mutations-optimistic/SKILL.md — for mutation patterns.