Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eight-ways-dig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/angular-store': patch
---

support input signals in angular store
4 changes: 2 additions & 2 deletions docs/framework/angular/reference/functions/injectAtom.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: injectAtom
function injectAtom<TValue>(atom, options?): WritableAtomSignal<TValue>;
```

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

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

### atom

`Atom`\<`TValue`\>
`Atom`\<`TValue`\> | () => `Atom`\<`TValue`\>

### options?

Expand Down
4 changes: 2 additions & 2 deletions docs/framework/angular/reference/functions/injectSelector.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function injectSelector<TState, TSelected>(
options?): Signal<TSelected>;
```

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

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

### source

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

### selector

Expand Down
20 changes: 10 additions & 10 deletions docs/framework/angular/reference/functions/injectStore.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ title: _injectStore
function _injectStore<TState, TActions, TSelected>(
store,
selector,
options?): [Signal<TSelected>, [TActions] extends [never] ? (updater) => void : TActions];
options?): WritableStoreSliceSignal<TState, TSelected, TActions>;
```

Defined in: [packages/angular-store/src/\_injectStore.ts:24](https://github.com/TanStack/store/blob/main/packages/angular-store/src/_injectStore.ts#L24)
Defined in: [packages/angular-store/src/\_injectStore.ts:34](https://github.com/TanStack/store/blob/main/packages/angular-store/src/_injectStore.ts#L34)

Experimental combined read+write injection function for stores, mirroring
injectAtom's pattern.

Returns `[signal, actions]` when the store has an actions factory, or
`[signal, setState]` for plain stores.
Returns a callable slice with methods when the store has an actions factory, or
with only the setState method for plain stores.

## Type Parameters

Expand All @@ -38,7 +38,7 @@ Returns `[signal, actions]` when the store has an actions factory, or

### store

`Store`\<`TState`, `TActions`\>
`Store`\<`TState`, `TActions`\> | () => `Store`\<`TState`, `TActions`\>

### selector

Expand All @@ -50,16 +50,16 @@ Returns `[signal, actions]` when the store has an actions factory, or

## Returns

\[`Signal`\<`TSelected`\>, \[`TActions`\] *extends* \[`never`\] ? (`updater`) => `void` : `TActions`\]
`WritableStoreSliceSignal`\<`TState`, `TSelected`, `TActions`\>

## Example

```ts
// Store with actions
readonly result = _injectStore(petStore, (s) => s.cats)
// result[0] is Signal<number>, result[1] is actions
readonly dogs = _injectStore(petStore, (s) => s.dogs)
// dogs() and dogs.addDog()

// Store without actions
readonly result = _injectStore(plainStore, (s) => s)
// result[0] is Signal<number>, result[1] is setState
readonly value = _injectStore(plainStore, (s) => s)
// value() and value.setState(...)
```
8 changes: 3 additions & 5 deletions examples/angular/store-actions/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const petStore = createStore(
</div>
<div>
<p>Dogs: {{ dogs() }}</p>
<button type="button" (click)="dogActions.addDog()">
<button type="button" (click)="dogs.addDog()">
Vote for dogs
</button>
</div>
Expand All @@ -59,10 +59,8 @@ export class AppComponent {
cats = injectSelector(petStore, (state) => state.cats)
addCat = petStore.actions.addCat

// _injectStore gives both the selected signal and actions in a single tuple
private dogResult = _injectStore(petStore, (state) => state.dogs)
dogs = this.dogResult[0]
dogActions = this.dogResult[1]
// _injectStore: callable slice for reads; action methods for writes
dogs = _injectStore(petStore, (state) => state.dogs)

total = injectSelector(petStore, (state) => state.cats + state.dogs)

Expand Down
4 changes: 3 additions & 1 deletion packages/angular-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "^2.4.5",
"@analogjs/vitest-angular": "^2.3.1",
"@angular/common": "^21.2.8",
"@angular/compiler": "^21.2.8",
"@angular/core": "^21.2.8",
"@angular/platform-browser": "^21.2.8",
"@angular/platform-browser-dynamic": "^21.2.8",
"@testing-library/angular": "^19.1.1",
"@testing-library/jest-dom": "^6.9.1",
"zone.js": "^0.16.1"
},
"peerDependencies": {
Expand Down
55 changes: 40 additions & 15 deletions packages/angular-store/src/_injectStore.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,66 @@
import { untracked } from '@angular/core'
import { injectSelector } from './injectSelector'
import type { Signal } from '@angular/core'
import type { Store, StoreActionMap } from '@tanstack/store'
import type { InjectSelectorOptions } from './injectSelector'

type WritableStoreSliceSignal<
TState,
TSelected,
TActions extends StoreActionMap,
> = Signal<TSelected> &
([TActions] extends [never]
? Pick<Store<TState>, 'setState'>
: TActions)

/**
* Experimental combined read+write injection function for stores, mirroring
* injectAtom's pattern.
*
* Returns `[signal, actions]` when the store has an actions factory, or
* `[signal, setState]` for plain stores.
*
* Returns a callable slice with methods when the store has an actions factory, or
* with only the setState method for plain stores.
*
* @example
* ```ts
* // Store with actions
* readonly result = _injectStore(petStore, (s) => s.cats)
* // result[0] is Signal<number>, result[1] is actions
* readonly dogs = _injectStore(petStore, (s) => s.dogs)
* // dogs() and dogs.addDog()
*
* // Store without actions
* readonly result = _injectStore(plainStore, (s) => s)
* // result[0] is Signal<number>, result[1] is setState
* readonly value = _injectStore(plainStore, (s) => s)
* // value() and value.setState(...)
* ```
*/
export function _injectStore<
TState,
TActions extends StoreActionMap,
TSelected = NoInfer<TState>,
>(
store: Store<TState, TActions>,
store: Store<TState, TActions> | (() => Store<TState, TActions>),
selector: (state: NoInfer<TState>) => TSelected,
options?: InjectSelectorOptions<TSelected>,
): [
Signal<TSelected>,
[TActions] extends [never] ? Store<TState>['setState'] : TActions,
] {
): WritableStoreSliceSignal<TState, TSelected, TActions> {
const selected = injectSelector(store, selector, options)
const actionsOrSetState =
(store.actions as StoreActionMap | undefined) ?? store.setState

return [selected, actionsOrSetState] as any
return new Proxy(selected, {
apply: () => selected(),
get(_target, prop, receiver) {
const inst = untracked(() =>
typeof store === 'function' ? store() : store,
)

const actions = inst.actions as StoreActionMap | undefined

if (actions != null && typeof actions === 'object') {
const method = Reflect.get(actions, prop, actions)
if (Object.hasOwn(actions, prop) && typeof method === 'function') {
return method
}
} else if (prop === 'setState' && typeof inst.setState === 'function') {
return inst.setState
}

return Reflect.get(selected, prop, receiver)
},
}) as WritableStoreSliceSignal<TState, TSelected, TActions>
}
19 changes: 17 additions & 2 deletions packages/angular-store/src/injectAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ export interface WritableAtomSignal<T> {
set: Atom<T>['set']
}


function createSetter<TValue>(
atom: Atom<TValue> | (() => Atom<TValue>),
): Atom<TValue>['set'] {
function set(value: TValue): void
function set(fn: (prevVal: TValue) => TValue): void
function set(
updaterOrValue: TValue | ((prevVal: TValue) => TValue),
): void {
const _atom = typeof atom === "function" ? atom() : atom
_atom.set(updaterOrValue as never)
}
return set as Atom<TValue>['set']
}

/**
* Returns a {@link WritableAtomSignal} that reads the current atom value when
* called and exposes a `.set` method for updates.
Expand All @@ -42,11 +57,11 @@ export interface WritableAtomSignal<T> {
* ```
*/
export function injectAtom<TValue>(
atom: Atom<TValue>,
atom: Atom<TValue> | (() => Atom<TValue>),
options?: InjectSelectorOptions<TValue>,
): WritableAtomSignal<TValue> {
const value = injectSelector(atom, undefined, options)
const atomSignal = (() => value()) as WritableAtomSignal<TValue>
atomSignal.set = atom.set
atomSignal.set = createSetter(atom)
return atomSignal
}
64 changes: 23 additions & 41 deletions packages/angular-store/src/injectSelector.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
DestroyRef,
Injector,
assertInInjectionContext,
effect,
inject,
linkedSignal,
runInInjectionContext,
Expand All @@ -23,10 +23,6 @@ export type SelectionSource<T> = {
}
}

function defaultCompare<T>(a: T, b: T) {
return a === b
}

function resolveInjector(
fn: (...args: Array<never>) => unknown,
injector?: Injector,
Expand All @@ -39,40 +35,6 @@ function resolveInjector(
return injector
}

function createReadonlySelectionSignal<TSource, TSelected>(
source: SelectionSource<TSource>,
selector: (state: NoInfer<TSource>) => TSelected,
options?: InjectSelectorOptions<TSelected>,
): Signal<TSelected> {
const injector = resolveInjector(
createReadonlySelectionSignal,
options?.injector,
)

return runInInjectionContext(injector, () => {
const destroyRef = inject(DestroyRef)
const compare = options?.compare ?? defaultCompare
const {
injector: _injector,
compare: _compare,
...signalOptions
} = options ?? {}
const slice = linkedSignal(() => selector(source.get()), {
...signalOptions,
equal: compare,
})

const { unsubscribe } = source.subscribe((state) => {
slice.set(selector(state))
})

destroyRef.onDestroy(() => {
unsubscribe()
})

return slice.asReadonly()
})
}

/**
* Selects a slice of state from an atom or store and returns it as an Angular
Expand All @@ -91,10 +53,30 @@ function createReadonlySelectionSignal<TSource, TSelected>(
* ```
*/
export function injectSelector<TState, TSelected = NoInfer<TState>>(
source: SelectionSource<TState>,
source: SelectionSource<TState> | (() => SelectionSource<TState>),
selector: (state: NoInfer<TState>) => TSelected = (d) =>
d as unknown as TSelected,
options?: InjectSelectorOptions<TSelected>,
): Signal<TSelected> {
return createReadonlySelectionSignal(source, selector, options)
const injector = resolveInjector(
injectSelector,
options?.injector,
)

return runInInjectionContext(injector, () => {
const _source = typeof source === "function" ? source : (() => source)

const slice = linkedSignal(() => selector(_source().get()), {
equal: options?.compare,
})

effect((onCleanup) => {
const { unsubscribe } = _source().subscribe((state) => {
slice.set(selector(state))
})
onCleanup(unsubscribe)
})

return slice.asReadonly()
})
}
Loading