Skip to content

Commit 213317c

Browse files
committed
support inputs on angular adapter
1 parent 83e2978 commit 213317c

11 files changed

Lines changed: 183 additions & 63 deletions

File tree

.changeset/eight-ways-dig.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/angular-store': patch
3+
---
4+
5+
support input signals in angular store

docs/framework/angular/reference/functions/injectAtom.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ title: injectAtom
99
function injectAtom<TValue>(atom, options?): WritableAtomSignal<TValue>;
1010
```
1111

12-
Defined in: [packages/angular-store/src/injectAtom.ts:44](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectAtom.ts#L44)
12+
Defined in: [packages/angular-store/src/injectAtom.ts:59](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectAtom.ts#L59)
1313

1414
Returns a [WritableAtomSignal](../interfaces/WritableAtomSignal.md) that reads the current atom value when
1515
called and exposes a `.set` method for updates.
@@ -27,7 +27,7 @@ atom.
2727

2828
### atom
2929

30-
`Atom`\<`TValue`\>
30+
`Atom`\<`TValue`\> | () => `Atom`\<`TValue`\>
3131

3232
### options?
3333

docs/framework/angular/reference/functions/injectSelector.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ function injectSelector<TState, TSelected>(
1212
options?): Signal<TSelected>;
1313
```
1414

15-
Defined in: [packages/angular-store/src/injectSelector.ts:93](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectSelector.ts#L93)
15+
Defined in: [packages/angular-store/src/injectSelector.ts:55](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectSelector.ts#L55)
1616

1717
Selects a slice of state from an atom or store and returns it as an Angular
1818
signal.
@@ -33,7 +33,7 @@ This is the primary Angular read hook for TanStack Store.
3333

3434
### source
3535

36-
[`SelectionSource`](../type-aliases/SelectionSource.md)\<`TState`\>
36+
[`SelectionSource`](../type-aliases/SelectionSource.md)\<`TState`\> | () => [`SelectionSource`](../type-aliases/SelectionSource.md)\<`TState`\>
3737

3838
### selector
3939

packages/angular-store/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@
5050
},
5151
"devDependencies": {
5252
"@analogjs/vite-plugin-angular": "^2.4.5",
53+
"@analogjs/vitest-angular": "^2.3.1",
5354
"@angular/common": "^21.2.8",
5455
"@angular/compiler": "^21.2.8",
5556
"@angular/core": "^21.2.8",
5657
"@angular/platform-browser": "^21.2.8",
57-
"@angular/platform-browser-dynamic": "^21.2.8",
58+
"@testing-library/angular": "^19.1.1",
59+
"@testing-library/jest-dom": "^6.9.1",
5860
"zone.js": "^0.16.1"
5961
},
6062
"peerDependencies": {

packages/angular-store/src/injectAtom.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ export interface WritableAtomSignal<T> {
2525
set: Atom<T>['set']
2626
}
2727

28+
29+
function createSetter<TValue>(
30+
atom: Atom<TValue> | (() => Atom<TValue>),
31+
): Atom<TValue>['set'] {
32+
function set(value: TValue): void
33+
function set(fn: (prevVal: TValue) => TValue): void
34+
function set(
35+
updaterOrValue: TValue | ((prevVal: TValue) => TValue),
36+
): void {
37+
const _atom = typeof atom === "function" ? atom() : atom
38+
_atom.set(updaterOrValue as never)
39+
}
40+
return set as Atom<TValue>['set']
41+
}
42+
2843
/**
2944
* Returns a {@link WritableAtomSignal} that reads the current atom value when
3045
* called and exposes a `.set` method for updates.
@@ -42,11 +57,11 @@ export interface WritableAtomSignal<T> {
4257
* ```
4358
*/
4459
export function injectAtom<TValue>(
45-
atom: Atom<TValue>,
60+
atom: Atom<TValue> | (() => Atom<TValue>),
4661
options?: InjectSelectorOptions<TValue>,
4762
): WritableAtomSignal<TValue> {
4863
const value = injectSelector(atom, undefined, options)
4964
const atomSignal = (() => value()) as WritableAtomSignal<TValue>
50-
atomSignal.set = atom.set
65+
atomSignal.set = createSetter(atom)
5166
return atomSignal
5267
}
Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
2-
DestroyRef,
32
Injector,
43
assertInInjectionContext,
4+
effect,
55
inject,
66
linkedSignal,
77
runInInjectionContext,
@@ -23,10 +23,6 @@ export type SelectionSource<T> = {
2323
}
2424
}
2525

26-
function defaultCompare<T>(a: T, b: T) {
27-
return a === b
28-
}
29-
3026
function resolveInjector(
3127
fn: (...args: Array<never>) => unknown,
3228
injector?: Injector,
@@ -39,40 +35,6 @@ function resolveInjector(
3935
return injector
4036
}
4137

42-
function createReadonlySelectionSignal<TSource, TSelected>(
43-
source: SelectionSource<TSource>,
44-
selector: (state: NoInfer<TSource>) => TSelected,
45-
options?: InjectSelectorOptions<TSelected>,
46-
): Signal<TSelected> {
47-
const injector = resolveInjector(
48-
createReadonlySelectionSignal,
49-
options?.injector,
50-
)
51-
52-
return runInInjectionContext(injector, () => {
53-
const destroyRef = inject(DestroyRef)
54-
const compare = options?.compare ?? defaultCompare
55-
const {
56-
injector: _injector,
57-
compare: _compare,
58-
...signalOptions
59-
} = options ?? {}
60-
const slice = linkedSignal(() => selector(source.get()), {
61-
...signalOptions,
62-
equal: compare,
63-
})
64-
65-
const { unsubscribe } = source.subscribe((state) => {
66-
slice.set(selector(state))
67-
})
68-
69-
destroyRef.onDestroy(() => {
70-
unsubscribe()
71-
})
72-
73-
return slice.asReadonly()
74-
})
75-
}
7638

7739
/**
7840
* Selects a slice of state from an atom or store and returns it as an Angular
@@ -91,10 +53,30 @@ function createReadonlySelectionSignal<TSource, TSelected>(
9153
* ```
9254
*/
9355
export function injectSelector<TState, TSelected = NoInfer<TState>>(
94-
source: SelectionSource<TState>,
56+
source: SelectionSource<TState> | (() => SelectionSource<TState>),
9557
selector: (state: NoInfer<TState>) => TSelected = (d) =>
9658
d as unknown as TSelected,
9759
options?: InjectSelectorOptions<TSelected>,
9860
): Signal<TSelected> {
99-
return createReadonlySelectionSignal(source, selector, options)
61+
const injector = resolveInjector(
62+
injectSelector,
63+
options?.injector,
64+
)
65+
66+
return runInInjectionContext(injector, () => {
67+
const _source = typeof source === "function" ? source : (() => source)
68+
69+
const slice = linkedSignal(() => selector(_source().get()), {
70+
equal: options?.compare,
71+
})
72+
73+
effect(() => {
74+
const { unsubscribe } = _source().subscribe((state) => {
75+
slice.set(selector(state))
76+
})
77+
return unsubscribe
78+
})
79+
80+
return slice.asReadonly()
81+
})
10082
}

packages/angular-store/tests/index.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import { describe, expect, test } from 'vitest'
2-
import { Component, effect } from '@angular/core'
2+
import {
3+
Component,
4+
computed,
5+
effect,
6+
input,
7+
inputBinding,
8+
signal,
9+
untracked,
10+
} from '@angular/core'
311
import { TestBed } from '@angular/core/testing'
412
import { By } from '@angular/platform-browser'
13+
import { render } from '@testing-library/angular'
514
import { Store, createAtom, createStore } from '@tanstack/store'
615
import {
716
_injectStore,
@@ -12,6 +21,10 @@ import {
1221
} from '../src/index'
1322
import type { Atom } from '@tanstack/store'
1423

24+
function createStableSignal<T>(fn: () => T): () => T {
25+
return computed(() => untracked(fn))
26+
}
27+
1528
describe('atom hooks', () => {
1629
test('injectSelector reads mutable atom state and rerenders when updated', () => {
1730
const atom = createAtom(0)
@@ -115,6 +128,34 @@ describe('atom hooks', () => {
115128

116129
expect(fixture.nativeElement.textContent).toContain('Value: 0')
117130
})
131+
132+
test('injectAtom supports atoms created from input signals', async () => {
133+
@Component({
134+
template: `<p>{{ doubled() }}</p>`,
135+
standalone: true,
136+
})
137+
class AtomFromInputChildCmp {
138+
value = input.required<number>()
139+
atom = createStableSignal(() => createAtom(this.value() * 2))
140+
doubled = injectAtom(this.atom)
141+
142+
constructor() {
143+
effect(() => {
144+
this.doubled.set(this.value() * 2)
145+
})
146+
}
147+
}
148+
149+
const value = signal(3)
150+
const { getByText, findByText } = await render(AtomFromInputChildCmp, {
151+
bindings: [inputBinding('value', value)],
152+
})
153+
154+
expect(getByText('6')).toBeInTheDocument()
155+
156+
value.set(4)
157+
expect(await findByText('8')).toBeInTheDocument()
158+
})
118159
})
119160

120161
describe('selector hooks', () => {
@@ -364,6 +405,32 @@ describe('selector hooks', () => {
364405
fixture.debugElement.query(By.css('p#derived')).nativeElement.textContent,
365406
).toContain('2')
366407
})
408+
409+
test('injectSelector supports selectors that read input signals', async () => {
410+
const selectorReadsInputStore = createStore({ cats: 2, dogs: 4 })
411+
412+
@Component({
413+
template: `<p>{{ count() }}</p>`,
414+
standalone: true,
415+
})
416+
class SelectorReadsInputChildCmp {
417+
animal = input.required<'cats' | 'dogs'>()
418+
count = injectSelector(
419+
selectorReadsInputStore,
420+
(state) => state[this.animal()],
421+
)
422+
}
423+
424+
const animal = signal<'cats' | 'dogs'>('cats')
425+
const { getByText, findByText } = await render(SelectorReadsInputChildCmp, {
426+
bindings: [inputBinding('animal', animal)],
427+
})
428+
429+
expect(getByText('2')).toBeInTheDocument()
430+
431+
animal.set('dogs')
432+
expect(await findByText('4')).toBeInTheDocument()
433+
})
367434
})
368435

369436
describe('injectStore', () => {
Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
1-
import '@analogjs/vite-plugin-angular/setup-vitest'
1+
import '@testing-library/jest-dom/vitest'
2+
import '@angular/compiler'
3+
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed'
24

3-
import {
4-
BrowserDynamicTestingModule,
5-
platformBrowserDynamicTesting,
6-
} from '@angular/platform-browser-dynamic/testing'
7-
import { getTestBed } from '@angular/core/testing'
8-
9-
getTestBed().initTestEnvironment(
10-
BrowserDynamicTestingModule,
11-
platformBrowserDynamicTesting(),
12-
)
5+
setupTestBed()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"noEmit": false,
5+
"types": ["vitest/globals", "node"]
6+
},
7+
"files": ["src/test-setup.ts"],
8+
"include": ["tests/**/*.ts"]
9+
}

packages/angular-store/vitest.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { defineConfig } from 'vitest/config'
2+
import angular from '@analogjs/vite-plugin-angular'
23
import packageJson from './package.json'
34

45
export default defineConfig({
6+
plugins: [angular({ tsconfig: './tsconfig.spec.json' })],
57
test: {
68
name: packageJson.name,
79
dir: './tests',

0 commit comments

Comments
 (0)