@tanstack/angular-db 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +279 -0
  2. package/package.json +62 -0
  3. package/src/index.ts +194 -0
package/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # @tanstack/angular-db
2
+
3
+ Angular hooks for TanStack DB. See [TanStack/db](https://github.com/TanStack/db) for more details.
4
+
5
+ Installation
6
+
7
+ npm install @tanstack/angular-db @tanstack/db
8
+
9
+ Usage
10
+
11
+ Basic Setup
12
+
13
+ First, create a collection:
14
+
15
+ import { createCollection, localOnlyCollectionOptions } from "@tanstack/db"
16
+
17
+ interface Todo {
18
+ id: number
19
+ text: string
20
+ completed: boolean
21
+ projectID: number
22
+ created_at: Date
23
+ }
24
+
25
+ export const todosCollection = createCollection(
26
+ localOnlyCollectionOptions<Todo>({
27
+ getKey: (todo: Todo) => todo.id,
28
+ initialData: [
29
+ {
30
+ id: 1,
31
+ text: "Learn Angular",
32
+ completed: false,
33
+ projectID: 1,
34
+ created_at: new Date(),
35
+ },
36
+ ],
37
+ })
38
+ )
39
+
40
+ Using injectLiveQuery in Components
41
+
42
+ Direct Collection Usage
43
+
44
+ The simplest way to use injectLiveQuery is to pass a collection directly:
45
+
46
+ import { Component } from "@angular/core"
47
+ import { injectLiveQuery } from "@tanstack/angular-db"
48
+ import { todosCollection } from "./collections/todos-collection"
49
+
50
+ @Component({
51
+ selector: "app-all-todos",
52
+ template: `
53
+ @if (allTodos.isReady()) {
54
+ <div>Total todos: {{ allTodos.data().length }}</div>
55
+ @for (todo of allTodos.data(); track todo.id) {
56
+ <div>{{ todo.text }}</div>
57
+ }
58
+ } @else {
59
+ <div>Loading todos...</div>
60
+ }
61
+ `,
62
+ })
63
+ export class AllTodosComponent {
64
+ // Direct collection usage - gets all items
65
+ allTodos = injectLiveQuery(todosCollection)
66
+ }
67
+
68
+ Static Query Functions
69
+
70
+ You can create filtered queries using a query function. Note: The query function is evaluated once and is not reactive to signal changes:
71
+
72
+ import { Component } from "@angular/core"
73
+ import { injectLiveQuery } from "@tanstack/angular-db"
74
+ import { eq } from "@tanstack/db"
75
+ import { todosCollection } from "./collections/todos-collection"
76
+
77
+ @Component({
78
+ selector: "app-todos",
79
+ template: `
80
+ @if (todoQuery.isReady()) {
81
+ @for (todo of todoQuery.data(); track todo.id) {
82
+ <div class="todo-item">
83
+ {{ todo.text }}
84
+ <button (click)="toggleTodo(todo.id)">
85
+ {{ todo.completed ? "Undo" : "Complete" }}
86
+ </button>
87
+ </div>
88
+ }
89
+ } @else {
90
+ <div>Loading todos...</div>
91
+ }
92
+ `,
93
+ })
94
+ export class TodosComponent {
95
+ // Static query - filters for incomplete todos
96
+ // This will not react to signal changes within the function
97
+ todoQuery = injectLiveQuery((q) =>
98
+ q
99
+ .from({ todo: todosCollection })
100
+ .where(({ todo }) => eq(todo.completed, false))
101
+ )
102
+
103
+ toggleTodo(id: number) {
104
+ todosCollection.utils.begin()
105
+ todosCollection.utils.write({
106
+ type: 'update',
107
+ key: id,
108
+ value: { completed: true }
109
+ })
110
+ todosCollection.utils.commit()
111
+ }
112
+ }
113
+
114
+ Reactive Queries with Parameters
115
+
116
+ For queries that need to react to component state changes, use the reactive parameters overload:
117
+
118
+ import { Component, signal } from "@angular/core"
119
+ import { injectLiveQuery } from "@tanstack/angular-db"
120
+ import { eq } from "@tanstack/db"
121
+ import { todosCollection } from "./collections/todos-collection"
122
+
123
+ @Component({
124
+ selector: "app-project-todos",
125
+ template: `
126
+ <select (change)="selectedProjectId.set(+$any($event).target.value)">
127
+ <option [value]="1">Project 1</option>
128
+ <option [value]="2">Project 2</option>
129
+ <option [value]="3">Project 3</option>
130
+ </select>
131
+
132
+ @if (todoQuery.isReady()) {
133
+ <div>Todos for project {{ selectedProjectId() }}:</div>
134
+ @for (todo of todoQuery.data(); track todo.id) {
135
+ <div class="todo-item">
136
+ {{ todo.text }}
137
+ </div>
138
+ }
139
+ } @else {
140
+ <div>Loading todos...</div>
141
+ }
142
+ `,
143
+ })
144
+ export class ProjectTodosComponent {
145
+ selectedProjectId = signal(1)
146
+
147
+ // Reactive query - automatically recreates when selectedProjectId changes
148
+ todoQuery = injectLiveQuery({
149
+ params: () => ({ projectID: this.selectedProjectId() }),
150
+ query: ({ params, q }) =>
151
+ q
152
+ .from({ todo: todosCollection })
153
+ .where(({ todo }) => eq(todo.completed, false))
154
+ .where(({ todo }) => eq(todo.projectID, params.projectID)),
155
+ })
156
+ }
157
+
158
+ Advanced Configuration
159
+
160
+ You can also pass a full configuration object:
161
+
162
+ import { Component } from "@angular/core"
163
+ import { injectLiveQuery } from "@tanstack/angular-db"
164
+ import { eq } from "@tanstack/db"
165
+ import { todosCollection } from "./collections/todos-collection"
166
+
167
+ @Component({
168
+ selector: "app-configured-todos",
169
+ template: `
170
+ @if (todoQuery.isReady()) {
171
+ @for (todo of todoQuery.data(); track todo.id) {
172
+ <div>{{ todo.text }}</div>
173
+ }
174
+ }
175
+ `,
176
+ })
177
+ export class ConfiguredTodosComponent {
178
+ todoQuery = injectLiveQuery({
179
+ query: (q) =>
180
+ q
181
+ .from({ todo: todosCollection })
182
+ .where(({ todo }) => eq(todo.completed, false))
183
+ .select(({ todo }) => ({
184
+ id: todo.id,
185
+ text: todo.text,
186
+ })),
187
+ startSync: true,
188
+ gcTime: 5000,
189
+ })
190
+ }
191
+
192
+ Important Notes
193
+
194
+ Reactivity Behavior
195
+
196
+ - Direct collection: Automatically reactive to collection changes
197
+ - Static query function: Query is built once and is not reactive to signals read within the function
198
+ - Reactive parameters: Query rebuilds when any signal read in params() changes
199
+ - Collection configuration: Static, not reactive to external signals
200
+
201
+ Lifecycle Management
202
+
203
+ - injectLiveQuery automatically handles subscription cleanup when the component is destroyed
204
+ - Each call to injectLiveQuery creates a new collection instance (no caching/reuse)
205
+ - Collections are started immediately and will sync according to their configuration
206
+
207
+ Template Usage
208
+
209
+ Use Angular's new control flow syntax for best performance:
210
+
211
+ @if (query.isReady()) {
212
+ @for (item of query.data(); track item.id) {
213
+ <div>{{ item.text }}</div>
214
+ }
215
+ } @else if (query.isError()) {
216
+ <div>Error loading data</div>
217
+ } @else {
218
+ <div>Loading...</div>
219
+ }
220
+
221
+ API
222
+
223
+ injectLiveQuery()
224
+
225
+ Angular injection function for TanStack DB live queries. Must be called within an injection context (e.g., component constructor, inject(), or field initializer).
226
+
227
+ Overloads
228
+
229
+ // Direct collection - reactive to collection changes
230
+ function injectLiveQuery<TResult, TKey, TUtils>(
231
+ collection: Collection<TResult, TKey, TUtils>
232
+ ): LiveQueryResult<TResult>
233
+
234
+ // Static query function - NOT reactive to signals within function
235
+ function injectLiveQuery<TContext>(
236
+ queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>
237
+ ): LiveQueryResult<TContext>
238
+
239
+ // Reactive query with parameters - recreates when params() signals change
240
+ function injectLiveQuery<TContext, TParams>(options: {
241
+ params: () => TParams
242
+ query: (args: {
243
+ params: TParams
244
+ q: InitialQueryBuilder
245
+ }) => QueryBuilder<TContext>
246
+ }): LiveQueryResult<TContext>
247
+
248
+ // Collection configuration - static configuration
249
+ function injectLiveQuery<TContext>(
250
+ config: LiveQueryCollectionConfig<TContext>
251
+ ): LiveQueryResult<TContext>
252
+
253
+ Returns
254
+
255
+ An object with Angular signals:
256
+
257
+ - data: Signal<Array<T>> - Array of query results, automatically updates
258
+ - state: Signal<Map<Key, T>> - Map of results by key, automatically updates
259
+ - collection: Signal<Collection> - The underlying collection instance
260
+ - status: Signal<CollectionStatus> - Current status ('idle' | 'loading' | 'ready' | 'error' | 'cleaned-up')
261
+ - isLoading: Signal<boolean> - true when status is 'loading' or 'initialCommit'
262
+ - isReady: Signal<boolean> - true when status is 'ready'
263
+ - isIdle: Signal<boolean> - true when status is 'idle'
264
+ - isError: Signal<boolean> - true when status is 'error'
265
+ - isCleanedUp: Signal<boolean> - true when status is 'cleaned-up'
266
+
267
+ Parameters
268
+
269
+ - collection - Existing collection to observe directly
270
+ - queryFn - Function that builds a static query using the query builder
271
+ - options.params - Reactive function that returns parameters; triggers query rebuild when accessed signals change
272
+ - options.query - Function that builds a query using parameters and query builder
273
+ - config - Configuration object for creating a live query collection
274
+
275
+ Requirements
276
+
277
+ - Angular 16+ (requires signals support)
278
+ - Must be called within an Angular injection context
279
+ - Automatically handles cleanup when the injector is destroyed
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@tanstack/angular-db",
3
+ "description": "Angular integration for @tanstack/db",
4
+ "version": "0.0.1",
5
+ "author": "Ethan McDaniel",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/TanStack/db.git",
10
+ "directory": "packages/angular-db"
11
+ },
12
+ "homepage": "https://tanstack.com/db",
13
+ "keywords": [
14
+ "optimistic",
15
+ "angular",
16
+ "typescript"
17
+ ],
18
+ "packageManager": "pnpm@10.6.3",
19
+ "dependencies": {
20
+ "@tanstack/db": "workspace:*"
21
+ },
22
+ "devDependencies": {
23
+ "@angular/common": "^19.0.0",
24
+ "@angular/core": "^19.0.0",
25
+ "@angular/platform-browser-dynamic": "^19.0.0",
26
+ "@vitest/coverage-istanbul": "^3.0.9",
27
+ "rxjs": "^7.8.0",
28
+ "zone.js": "^0.14.0"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "import": {
33
+ "types": "./dist/esm/index.d.ts",
34
+ "default": "./dist/esm/index.js"
35
+ },
36
+ "require": {
37
+ "types": "./dist/cjs/index.d.cts",
38
+ "default": "./dist/cjs/index.cjs"
39
+ }
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "src"
46
+ ],
47
+ "main": "dist/cjs/index.cjs",
48
+ "module": "dist/esm/index.js",
49
+ "peerDependencies": {
50
+ "@angular/core": ">=16.0.0",
51
+ "rxjs": ">=6.0.0"
52
+ },
53
+ "scripts": {
54
+ "build": "vite build",
55
+ "dev": "vite build --watch",
56
+ "test": "npx vitest --run",
57
+ "lint": "eslint . --fix"
58
+ },
59
+ "sideEffects": false,
60
+ "type": "module",
61
+ "types": "dist/esm/index.d.ts"
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,194 @@
1
+ import {
2
+ DestroyRef,
3
+ assertInInjectionContext,
4
+ computed,
5
+ effect,
6
+ inject,
7
+ signal,
8
+ } from "@angular/core"
9
+ import { createLiveQueryCollection } from "@tanstack/db"
10
+ import type {
11
+ ChangeMessage,
12
+ Collection,
13
+ CollectionStatus,
14
+ Context,
15
+ GetResult,
16
+ InitialQueryBuilder,
17
+ LiveQueryCollectionConfig,
18
+ QueryBuilder,
19
+ } from "@tanstack/db"
20
+ import type { Signal } from "@angular/core"
21
+
22
+ /**
23
+ * The result of calling `injectLiveQuery`.
24
+ * Contains reactive signals for the query state and data.
25
+ */
26
+ export interface InjectLiveQueryResult<
27
+ TResult = any,
28
+ TKey extends string | number = string | number,
29
+ TUtils extends Record<string, any> = Record<string, never>,
30
+ > {
31
+ /** A signal containing the complete state map of results keyed by their ID */
32
+ state: Signal<Map<TKey, TResult>>
33
+ /** A signal containing the results as an array */
34
+ data: Signal<Array<TResult>>
35
+ /** A signal containing the underlying collection instance */
36
+ collection: Signal<Collection<TResult, TKey, TUtils>>
37
+ /** A signal containing the current status of the collection */
38
+ status: Signal<CollectionStatus>
39
+ /** A signal indicating whether the collection is currently loading */
40
+ isLoading: Signal<boolean>
41
+ /** A signal indicating whether the collection is ready */
42
+ isReady: Signal<boolean>
43
+ /** A signal indicating whether the collection is idle */
44
+ isIdle: Signal<boolean>
45
+ /** A signal indicating whether the collection has an error */
46
+ isError: Signal<boolean>
47
+ /** A signal indicating whether the collection has been cleaned up */
48
+ isCleanedUp: Signal<boolean>
49
+ }
50
+
51
+ export function injectLiveQuery<
52
+ TContext extends Context,
53
+ TParams extends any,
54
+ >(options: {
55
+ params: () => TParams
56
+ query: (args: {
57
+ params: TParams
58
+ q: InitialQueryBuilder
59
+ }) => QueryBuilder<TContext>
60
+ }): InjectLiveQueryResult<GetResult<TContext>>
61
+ export function injectLiveQuery<TContext extends Context>(
62
+ queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>
63
+ ): InjectLiveQueryResult<GetResult<TContext>>
64
+ export function injectLiveQuery<TContext extends Context>(
65
+ config: LiveQueryCollectionConfig<TContext>
66
+ ): InjectLiveQueryResult<GetResult<TContext>>
67
+ export function injectLiveQuery<
68
+ TResult extends object,
69
+ TKey extends string | number,
70
+ TUtils extends Record<string, any>,
71
+ >(
72
+ liveQueryCollection: Collection<TResult, TKey, TUtils>
73
+ ): InjectLiveQueryResult<TResult, TKey, TUtils>
74
+ export function injectLiveQuery(opts: any) {
75
+ assertInInjectionContext(injectLiveQuery)
76
+ const destroyRef = inject(DestroyRef)
77
+
78
+ const collection = computed(() => {
79
+ // Check if it's an existing collection
80
+ const isExistingCollection =
81
+ opts &&
82
+ typeof opts === `object` &&
83
+ typeof opts.subscribeChanges === `function` &&
84
+ typeof opts.startSyncImmediate === `function` &&
85
+ typeof opts.id === `string`
86
+
87
+ if (isExistingCollection) {
88
+ return opts
89
+ }
90
+
91
+ if (typeof opts === `function`) {
92
+ return createLiveQueryCollection({
93
+ query: opts,
94
+ startSync: true,
95
+ gcTime: 0,
96
+ })
97
+ }
98
+
99
+ // Check if it's reactive query options
100
+ const isReactiveQueryOptions =
101
+ opts &&
102
+ typeof opts === `object` &&
103
+ typeof opts.query === `function` &&
104
+ typeof opts.params === `function`
105
+
106
+ if (isReactiveQueryOptions) {
107
+ const { params, query } = opts
108
+ const currentParams = params()
109
+ return createLiveQueryCollection({
110
+ query: (q) => query({ params: currentParams, q }),
111
+ startSync: true,
112
+ gcTime: 0,
113
+ })
114
+ }
115
+
116
+ // Handle LiveQueryCollectionConfig objects
117
+ if (opts && typeof opts === `object` && typeof opts.query === `function`) {
118
+ return createLiveQueryCollection(opts)
119
+ }
120
+
121
+ throw new Error(`Invalid options provided to injectLiveQuery`)
122
+ })
123
+
124
+ const state = signal(new Map<string | number, any>())
125
+ const data = signal<Array<any>>([])
126
+ const status = signal<CollectionStatus>(`idle`)
127
+
128
+ const syncDataFromCollection = (
129
+ currentCollection: Collection<any, any, any>
130
+ ) => {
131
+ const newState = new Map(currentCollection.entries())
132
+ const newData = Array.from(currentCollection.values())
133
+
134
+ state.set(newState)
135
+ data.set(newData)
136
+ status.set(currentCollection.status)
137
+ }
138
+
139
+ let unsub: (() => void) | null = null
140
+ const cleanup = () => {
141
+ unsub?.()
142
+ unsub = null
143
+ }
144
+
145
+ effect((onCleanup) => {
146
+ const currentCollection = collection()
147
+
148
+ if (!currentCollection) {
149
+ return
150
+ }
151
+
152
+ cleanup()
153
+
154
+ // Initialize immediately with current state
155
+ syncDataFromCollection(currentCollection)
156
+
157
+ // Start sync if idle
158
+ if (currentCollection.status === `idle`) {
159
+ currentCollection.startSyncImmediate()
160
+ // Update status after starting sync
161
+ status.set(currentCollection.status)
162
+ }
163
+
164
+ // Subscribe to changes
165
+ unsub = currentCollection.subscribeChanges(
166
+ (_: Array<ChangeMessage<any>>) => {
167
+ syncDataFromCollection(currentCollection)
168
+ }
169
+ )
170
+
171
+ // Handle ready state
172
+ currentCollection.onFirstReady(() => {
173
+ status.set(currentCollection.status)
174
+ })
175
+
176
+ onCleanup(cleanup)
177
+ })
178
+
179
+ destroyRef.onDestroy(cleanup)
180
+
181
+ return {
182
+ state,
183
+ data,
184
+ collection,
185
+ status,
186
+ isLoading: computed(
187
+ () => status() === `loading` || status() === `initialCommit`
188
+ ),
189
+ isReady: computed(() => status() === `ready`),
190
+ isIdle: computed(() => status() === `idle`),
191
+ isError: computed(() => status() === `error`),
192
+ isCleanedUp: computed(() => status() === `cleaned-up`),
193
+ }
194
+ }