From c8bea5664b39fb354848c4c225cf83d7f75be267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjam=C3=ADn=20Vicente?= Date: Wed, 11 Mar 2026 19:44:18 -0300 Subject: [PATCH 1/5] chore(angular): drop unnecessary types --- packages/angular-store/src/index.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/angular-store/src/index.ts b/packages/angular-store/src/index.ts index 746bfed..e1f8484 100644 --- a/packages/angular-store/src/index.ts +++ b/packages/angular-store/src/index.ts @@ -9,27 +9,11 @@ import { import type { Atom, ReadonlyAtom } from '@tanstack/store' import type { CreateSignalOptions, Signal } from '@angular/core' -type StoreContext = Record - export * from '@tanstack/store' export function injectStore>( - store: Atom, - selector?: (state: NoInfer) => TSelected, - options?: CreateSignalOptions & { injector?: Injector }, -): Signal -export function injectStore>( - store: Atom | ReadonlyAtom, - selector?: (state: NoInfer) => TSelected, - options?: CreateSignalOptions & { injector?: Injector }, -): Signal -export function injectStore< - TState extends StoreContext, - TSelected = NoInfer, ->( store: Atom | ReadonlyAtom, - selector: (state: NoInfer) => TSelected = (d) => - d as unknown as TSelected, + selector: (state: NoInfer) => TSelected = (d) => d as unknown as TSelected, options: CreateSignalOptions & { injector?: Injector } = { equal: shallow, }, From cd1efa497a87cfc6fbce95ac156cb9b53de9f4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjam=C3=ADn=20Vicente?= Date: Wed, 11 Mar 2026 20:02:52 -0300 Subject: [PATCH 2/5] fix(angular): support store signals --- packages/angular-store/src/index.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/angular-store/src/index.ts b/packages/angular-store/src/index.ts index e1f8484..c36f43f 100644 --- a/packages/angular-store/src/index.ts +++ b/packages/angular-store/src/index.ts @@ -1,7 +1,7 @@ import { - DestroyRef, Injector, assertInInjectionContext, + effect, inject, linkedSignal, runInInjectionContext, @@ -12,7 +12,7 @@ import type { CreateSignalOptions, Signal } from '@angular/core' export * from '@tanstack/store' export function injectStore>( - store: Atom | ReadonlyAtom, + storeOrStoreSignal: Atom | ReadonlyAtom | (() => Atom | ReadonlyAtom), selector: (state: NoInfer) => TSelected = (d) => d as unknown as TSelected, options: CreateSignalOptions & { injector?: Injector } = { equal: shallow, @@ -25,18 +25,20 @@ export function injectStore>( } return runInInjectionContext(options.injector, () => { - const destroyRef = inject(DestroyRef) - const slice = linkedSignal(() => selector(store.get()), options) + const storeSignal = typeof storeOrStoreSignal === "function" + ? storeOrStoreSignal + : () => storeOrStoreSignal; - const { unsubscribe } = store.subscribe((s) => { - slice.set(selector(s)) - }) + const slice = linkedSignal(() => selector(storeSignal().get()), options); - destroyRef.onDestroy(() => { - unsubscribe() - }) + effect((onCleanup) => { + const { unsubscribe } = storeSignal().subscribe((s) => { + slice.set(selector(s)); + }); + onCleanup(() => unsubscribe()); + }); - return slice.asReadonly() + return slice.asReadonly(); }) } From e9efd72f614d892965652eaa8c30a052b0f11448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjam=C3=ADn=20Vicente?= Date: Wed, 11 Mar 2026 20:39:25 -0300 Subject: [PATCH 3/5] tests(angular): test against input signal support --- packages/angular-store/package.json | 1 + packages/angular-store/tests/index.test.ts | 146 ++++++++++++++++++++- packages/angular-store/tests/test-setup.ts | 14 +- packages/angular-store/tsconfig.spec.json | 9 ++ packages/angular-store/vite.config.ts | 2 + pnpm-lock.yaml | 121 ++++++++++++++++- 6 files changed, 279 insertions(+), 14 deletions(-) create mode 100644 packages/angular-store/tsconfig.spec.json diff --git a/packages/angular-store/package.json b/packages/angular-store/package.json index 8c32c0e..7c6d55b 100644 --- a/packages/angular-store/package.json +++ b/packages/angular-store/package.json @@ -52,6 +52,7 @@ }, "devDependencies": { "@analogjs/vite-plugin-angular": "^2.2.3", + "@analogjs/vitest-angular": "^2.3.1", "@angular/common": "^21.1.2", "@angular/compiler": "^21.1.2", "@angular/compiler-cli": "^21.1.2", diff --git a/packages/angular-store/tests/index.test.ts b/packages/angular-store/tests/index.test.ts index 88da8df..d72a02f 100644 --- a/packages/angular-store/tests/index.test.ts +++ b/packages/angular-store/tests/index.test.ts @@ -1,10 +1,25 @@ import { describe, expect, test } from 'vitest' -import { Component, effect } from '@angular/core' +import { + Component, + computed, + effect, + input, + OnInit, + signal, + untracked, + inputBinding, +} from '@angular/core' import { TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' import { createStore } from '@tanstack/store' import { injectStore } from '../src/index' +function createStableSignal(fn: () => T): () => T { + return computed(() => untracked(fn)) +} + +const selectorReadsInputStore = createStore({ cats: 2, dogs: 4 }) + describe('injectStore', () => { test(`allows us to select state using a selector`, () => { const store = createStore({ select: 0, ignored: 1 }) @@ -92,6 +107,135 @@ describe('injectStore', () => { expect(element.textContent).toContain('Store: 10') expect(count).toEqual(2) }) + + test('supports a store created inside a stable signal', () => { + const count = signal(1) + + const storeVal = TestBed.runInInjectionContext(() => { + const store = createStableSignal(() => createStore({ value: count() })) + const storeVal = injectStore(() => store(), (state) => state.value) + + effect(() => { + store().setState(() => ({ value: count() })) + }) + + return storeVal + }) + + expect(storeVal()).toBe(1) + + count.set(5) + TestBed.tick() + + expect(storeVal()).toBe(5) + }) + + test('supports a store created from input signals', () => { + @Component({ + template: `

{{ storeVal() }}

`, + standalone: true, + }) + class StoreFromInputChildCmp { + value = input.required() + store = createStableSignal(() => + createStore({ doubled: this.value() * 2 }), + ) + storeVal = injectStore(() => this.store(), (state) => state.doubled) + + constructor() { + effect(() => { + this.store().setState(() => ({ doubled: this.value() * 2 })) + }) + } + } + + const value = signal(3) + const fixture = TestBed.createComponent(StoreFromInputChildCmp, { + bindings: [inputBinding('value', value)], + }) + fixture.detectChanges() + + const debugElement = fixture.debugElement + + expect( + debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, + ).toContain('6') + + value.set(4) + fixture.detectChanges() + + expect( + debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, + ).toContain('8') + }) + + test('supports selectors that read input signals', () => { + @Component({ + selector: 'app-selector-reads-input', + template: `

{{ count() }}

`, + standalone: true, + }) + class SelectorReadsInputChildCmp { + animal = input.required<'cats' | 'dogs'>() + count = injectStore( + selectorReadsInputStore, + (state) => state[this.animal()], + ) + } + + const animal = signal<'cats' | 'dogs'>('cats') + const fixture = TestBed.createComponent(SelectorReadsInputChildCmp, { + bindings: [inputBinding('animal', animal)], + }) + fixture.detectChanges() + + const debugElement = fixture.debugElement + + expect( + debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, + ).toContain('2') + + animal.set('dogs') + fixture.detectChanges() + + expect( + debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, + ).toContain('4') + }) + + test('makes the selected store value available on ngOnInit', () => { + let didAssertOnInit = false + + @Component({ + template: ``, + standalone: true, + }) + class StoreFromInputOnInitCmp implements OnInit { + value = input.required() + store = createStableSignal(() => + createStore({ doubled: this.value() * 2 }), + ) + storeVal = injectStore(() => this.store(), (state) => state.doubled) + + constructor() { + effect(() => { + this.store().setState(() => ({ doubled: this.value() * 2 })) + }) + } + + ngOnInit() { + expect(this.storeVal()).toBe(14) + didAssertOnInit = true + } + } + + const value = signal(7) + const fixture = TestBed.createComponent(StoreFromInputOnInitCmp, { + bindings: [inputBinding('value', value)], + }) + fixture.detectChanges() + expect(didAssertOnInit).toBe(true) + }) }) describe('dataType', () => { diff --git a/packages/angular-store/tests/test-setup.ts b/packages/angular-store/tests/test-setup.ts index cb5fd34..ce038ed 100644 --- a/packages/angular-store/tests/test-setup.ts +++ b/packages/angular-store/tests/test-setup.ts @@ -1,12 +1,4 @@ -import '@analogjs/vite-plugin-angular/setup-vitest' +import '@angular/compiler'; +import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed'; -import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting, -} from '@angular/platform-browser-dynamic/testing' -import { getTestBed } from '@angular/core/testing' - -getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting(), -) +setupTestBed(); diff --git a/packages/angular-store/tsconfig.spec.json b/packages/angular-store/tsconfig.spec.json new file mode 100644 index 0000000..500e10c --- /dev/null +++ b/packages/angular-store/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "types": ["vitest/globals", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["tests/**/*.ts"] +} diff --git a/packages/angular-store/vite.config.ts b/packages/angular-store/vite.config.ts index 07256bd..03b94af 100644 --- a/packages/angular-store/vite.config.ts +++ b/packages/angular-store/vite.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'vitest/config' import packageJson from './package.json' +import angular from '@analogjs/vite-plugin-angular'; export default defineConfig({ + plugins: [angular()], test: { name: packageJson.name, dir: './tests', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 397710e..0839ceb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -287,6 +287,9 @@ importers: '@analogjs/vite-plugin-angular': specifier: ^2.2.3 version: 2.3.0(01de8ebf21c131e3ceb8a4beb5642786) + '@analogjs/vitest-angular': + specifier: ^2.3.1 + version: 2.3.1(@analogjs/vite-plugin-angular@2.3.0(01de8ebf21c131e3ceb8a4beb5642786))(@angular-devkit/architect@0.2102.0(chokidar@5.0.0))(@angular-devkit/schematics@21.2.0(chokidar@5.0.0))(vitest@4.0.18(@types/node@25.2.1)(jiti@2.6.1)(jsdom@25.0.1)(less@4.5.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(zone.js@0.15.1) '@angular/common': specifier: ^21.1.2 version: 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) @@ -531,6 +534,18 @@ packages: '@angular/build': optional: true + '@analogjs/vitest-angular@2.3.1': + resolution: {integrity: sha512-wbTLgeWDR9qPohE5vzGi4GJ0oHC/GmAhkzEMbt6xoAHbhvMsRrqsiiku03tejHcPqErMlsBotIeR/huAbJferQ==} + peerDependencies: + '@analogjs/vite-plugin-angular': '*' + '@angular-devkit/architect': '>=0.1500.0 < 0.2200.0' + '@angular-devkit/schematics': '>=17.0.0' + vitest: ^1.3.1 || ^2.0.0 || ^3.0.0 || ^4.0.0 + zone.js: '>=0.14.0' + peerDependenciesMeta: + zone.js: + optional: true + '@angular-devkit/architect@0.2102.0': resolution: {integrity: sha512-kYFwTNzToG2SJMxj2f41w3QRtdqlrFuF+bpZrtIaHOP078Ktld8EPIp9KqB0Y46Vvs69ifby5Q1/wPD9wA3iaw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} @@ -8228,6 +8243,15 @@ snapshots: '@angular-devkit/build-angular': 21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.0(@angular/animations@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@25.2.1)(chokidar@5.0.0)(jiti@2.6.1)(ng-packagr@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(tsx@4.21.0)(typescript@5.9.3)(vitest@4.0.18(@types/node@25.2.1)(jiti@2.6.1)(jsdom@25.0.1)(less@4.5.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) '@angular/build': 21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.0(@angular/animations@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@25.2.1)(chokidar@5.0.0)(jiti@2.6.1)(less@4.5.1)(ng-packagr@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.6)(terser@5.46.0)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@4.0.18(@types/node@25.2.1)(jiti@2.6.1)(jsdom@25.0.1)(less@4.5.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + '@analogjs/vitest-angular@2.3.1(@analogjs/vite-plugin-angular@2.3.0(01de8ebf21c131e3ceb8a4beb5642786))(@angular-devkit/architect@0.2102.0(chokidar@5.0.0))(@angular-devkit/schematics@21.2.0(chokidar@5.0.0))(vitest@4.0.18(@types/node@25.2.1)(jiti@2.6.1)(jsdom@25.0.1)(less@4.5.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(zone.js@0.15.1)': + dependencies: + '@analogjs/vite-plugin-angular': 2.3.0(01de8ebf21c131e3ceb8a4beb5642786) + '@angular-devkit/architect': 0.2102.0(chokidar@5.0.0) + '@angular-devkit/schematics': 21.2.0(chokidar@5.0.0) + vitest: 4.0.18(@types/node@25.2.1)(jiti@2.6.1)(jsdom@25.0.1)(less@4.5.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + optionalDependencies: + zone.js: 0.15.1 + '@angular-devkit/architect@0.2102.0(chokidar@5.0.0)': dependencies: '@angular-devkit/core': 21.2.0(chokidar@5.0.0) @@ -8288,9 +8312,9 @@ snapshots: tree-kill: 1.2.2 tslib: 2.8.1 typescript: 5.9.3 - webpack: 5.105.2(esbuild@0.27.3) + webpack: 5.105.2 webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)) - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.105.2(esbuild@0.27.3)) optionalDependencies: @@ -16338,6 +16362,15 @@ snapshots: optionalDependencies: esbuild: 0.27.3 + terser-webpack-plugin@5.3.16(webpack@5.105.2(esbuild@0.27.3)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.46.0 + webpack: 5.105.2 + terser@5.46.0: dependencies: '@jridgewell/source-map': 0.3.11 @@ -16939,6 +16972,19 @@ snapshots: transitivePeerDependencies: - tslib + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.105.2): + dependencies: + colorette: 2.0.20 + memfs: 4.56.10(tslib@2.8.1) + mime-types: 3.0.2 + on-finished: 2.4.1 + range-parser: 1.2.1 + schema-utils: 4.3.3 + optionalDependencies: + webpack: 5.105.2 + transitivePeerDependencies: + - tslib + webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)): dependencies: '@types/bonjour': 3.5.13 @@ -16978,6 +17024,45 @@ snapshots: - tslib - utf-8-validate + webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.25 + '@types/express-serve-static-core': 4.19.8 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.10 + '@types/sockjs': 0.3.36 + '@types/ws': 8.18.1 + ansi-html-community: 0.0.8 + bonjour-service: 1.3.0 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.8.1 + connect-history-api-fallback: 2.0.0 + express: 4.22.1 + graceful-fs: 4.2.11 + http-proxy-middleware: 2.0.9(@types/express@4.17.25) + ipaddr.js: 2.3.0 + launch-editor: 2.13.1 + open: 10.2.0 + p-retry: 6.2.1 + schema-utils: 4.3.3 + selfsigned: 5.5.0 + serve-index: 1.9.2 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2) + ws: 8.19.0 + optionalDependencies: + webpack: 5.105.2 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - tslib + - utf-8-validate + webpack-merge@6.0.1: dependencies: clone-deep: 4.0.1 @@ -16994,6 +17079,38 @@ snapshots: webpack-virtual-modules@0.6.2: optional: true + webpack@5.105.2: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.20.0 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.16(webpack@5.105.2(esbuild@0.27.3)) + watchpack: 2.5.1 + webpack-sources: 3.3.4 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.105.2(esbuild@0.27.3): dependencies: '@types/eslint-scope': 3.7.7 From 9d3fa69dcedc578f52bbfe3bc2f2a9bc4f994f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjam=C3=ADn=20Vicente?= Date: Wed, 11 Mar 2026 20:52:24 -0300 Subject: [PATCH 4/5] chore(angular): prepare pr --- .changeset/strong-lines-change.md | 5 ++ .../reference/functions/injectStore.md | 69 ++++--------------- packages/angular-store/package.json | 1 - packages/angular-store/src/index.ts | 27 +++++--- packages/angular-store/tests/index.test.ts | 19 +++-- packages/angular-store/tests/test-setup.ts | 6 +- packages/angular-store/vite.config.ts | 2 +- pnpm-lock.yaml | 14 +--- 8 files changed, 54 insertions(+), 89 deletions(-) create mode 100644 .changeset/strong-lines-change.md diff --git a/.changeset/strong-lines-change.md b/.changeset/strong-lines-change.md new file mode 100644 index 0000000..6633089 --- /dev/null +++ b/.changeset/strong-lines-change.md @@ -0,0 +1,5 @@ +--- +'@tanstack/angular-store': patch +--- + +support input signals in angular store diff --git a/docs/framework/angular/reference/functions/injectStore.md b/docs/framework/angular/reference/functions/injectStore.md index 0219a90..a6d6bd3 100644 --- a/docs/framework/angular/reference/functions/injectStore.md +++ b/docs/framework/angular/reference/functions/injectStore.md @@ -5,80 +5,39 @@ title: injectStore # Function: injectStore() -## Call Signature - -```ts -function injectStore( - store, - selector?, -options?): Signal; -``` - -Defined in: [index.ts:16](https://github.com/TanStack/store/blob/main/packages/angular-store/src/index.ts#L16) - -### Type Parameters - -#### TState - -`TState` - -#### TSelected - -`TSelected` = `NoInfer`\<`TState`\> - -### Parameters - -#### store - -`Atom`\<`TState`\> - -#### selector? - -(`state`) => `TSelected` - -#### options? - -`CreateSignalOptions`\<`TSelected`\> & `object` - -### Returns - -`Signal`\<`TSelected`\> - -## Call Signature - ```ts function injectStore( - store, - selector?, -options?): Signal; + storeOrStoreSignal, + selector, +options): Signal; ``` -Defined in: [index.ts:21](https://github.com/TanStack/store/blob/main/packages/angular-store/src/index.ts#L21) +Defined in: [index.ts:14](https://github.com/TanStack/store/blob/main/packages/angular-store/src/index.ts#L14) -### Type Parameters +## Type Parameters -#### TState +### TState `TState` -#### TSelected +### TSelected `TSelected` = `NoInfer`\<`TState`\> -### Parameters +## Parameters -#### store +### storeOrStoreSignal -`Atom`\<`TState`\> | `ReadonlyAtom`\<`TState`\> +`Atom`\<`TState`\> | `ReadonlyAtom`\<`TState`\> | () => `Atom`\<`TState`\> \| `ReadonlyAtom`\<`TState`\> -#### selector? +### selector (`state`) => `TSelected` -#### options? +### options -`CreateSignalOptions`\<`TSelected`\> & `object` +`CreateSignalOptions`\<`TSelected`\> & `object` = `...` -### Returns +## Returns `Signal`\<`TSelected`\> diff --git a/packages/angular-store/package.json b/packages/angular-store/package.json index 7c6d55b..c98988b 100644 --- a/packages/angular-store/package.json +++ b/packages/angular-store/package.json @@ -58,7 +58,6 @@ "@angular/compiler-cli": "^21.1.2", "@angular/core": "^21.1.2", "@angular/platform-browser": "^21.1.2", - "@angular/platform-browser-dynamic": "^21.1.2", "ng-packagr": "^21.1.0", "zone.js": "^0.15.1" }, diff --git a/packages/angular-store/src/index.ts b/packages/angular-store/src/index.ts index c36f43f..802d475 100644 --- a/packages/angular-store/src/index.ts +++ b/packages/angular-store/src/index.ts @@ -12,8 +12,12 @@ import type { CreateSignalOptions, Signal } from '@angular/core' export * from '@tanstack/store' export function injectStore>( - storeOrStoreSignal: Atom | ReadonlyAtom | (() => Atom | ReadonlyAtom), - selector: (state: NoInfer) => TSelected = (d) => d as unknown as TSelected, + storeOrStoreSignal: + | Atom + | ReadonlyAtom + | (() => Atom | ReadonlyAtom), + selector: (state: NoInfer) => TSelected = (d) => + d as unknown as TSelected, options: CreateSignalOptions & { injector?: Injector } = { equal: shallow, }, @@ -25,20 +29,21 @@ export function injectStore>( } return runInInjectionContext(options.injector, () => { - const storeSignal = typeof storeOrStoreSignal === "function" - ? storeOrStoreSignal - : () => storeOrStoreSignal; + const storeSignal = + typeof storeOrStoreSignal === 'function' + ? storeOrStoreSignal + : () => storeOrStoreSignal - const slice = linkedSignal(() => selector(storeSignal().get()), options); + const slice = linkedSignal(() => selector(storeSignal().get()), options) effect((onCleanup) => { const { unsubscribe } = storeSignal().subscribe((s) => { - slice.set(selector(s)); - }); - onCleanup(() => unsubscribe()); - }); + slice.set(selector(s)) + }) + onCleanup(() => unsubscribe()) + }) - return slice.asReadonly(); + return slice.asReadonly() }) } diff --git a/packages/angular-store/tests/index.test.ts b/packages/angular-store/tests/index.test.ts index d72a02f..a5c7ade 100644 --- a/packages/angular-store/tests/index.test.ts +++ b/packages/angular-store/tests/index.test.ts @@ -4,15 +4,15 @@ import { computed, effect, input, - OnInit, + inputBinding, signal, untracked, - inputBinding, } from '@angular/core' import { TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' import { createStore } from '@tanstack/store' import { injectStore } from '../src/index' +import type { OnInit } from '@angular/core' function createStableSignal(fn: () => T): () => T { return computed(() => untracked(fn)) @@ -113,7 +113,10 @@ describe('injectStore', () => { const storeVal = TestBed.runInInjectionContext(() => { const store = createStableSignal(() => createStore({ value: count() })) - const storeVal = injectStore(() => store(), (state) => state.value) + const storeVal = injectStore( + () => store(), + (state) => state.value, + ) effect(() => { store().setState(() => ({ value: count() })) @@ -140,7 +143,10 @@ describe('injectStore', () => { store = createStableSignal(() => createStore({ doubled: this.value() * 2 }), ) - storeVal = injectStore(() => this.store(), (state) => state.doubled) + storeVal = injectStore( + () => this.store(), + (state) => state.doubled, + ) constructor() { effect(() => { @@ -215,7 +221,10 @@ describe('injectStore', () => { store = createStableSignal(() => createStore({ doubled: this.value() * 2 }), ) - storeVal = injectStore(() => this.store(), (state) => state.doubled) + storeVal = injectStore( + () => this.store(), + (state) => state.doubled, + ) constructor() { effect(() => { diff --git a/packages/angular-store/tests/test-setup.ts b/packages/angular-store/tests/test-setup.ts index ce038ed..1b56cce 100644 --- a/packages/angular-store/tests/test-setup.ts +++ b/packages/angular-store/tests/test-setup.ts @@ -1,4 +1,4 @@ -import '@angular/compiler'; -import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed'; +import '@angular/compiler' +import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed' -setupTestBed(); +setupTestBed() diff --git a/packages/angular-store/vite.config.ts b/packages/angular-store/vite.config.ts index 03b94af..0cb8375 100644 --- a/packages/angular-store/vite.config.ts +++ b/packages/angular-store/vite.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from 'vitest/config' import packageJson from './package.json' -import angular from '@analogjs/vite-plugin-angular'; +import angular from '@analogjs/vite-plugin-angular' export default defineConfig({ plugins: [angular()], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0839ceb..f13f1f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,9 +305,6 @@ importers: '@angular/platform-browser': specifier: ^21.1.2 version: 21.2.0(@angular/animations@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)) - '@angular/platform-browser-dynamic': - specifier: ^21.1.2 - version: 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.0(@angular/animations@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))) ng-packagr: specifier: ^21.1.0 version: 21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) @@ -16362,15 +16359,6 @@ snapshots: optionalDependencies: esbuild: 0.27.3 - terser-webpack-plugin@5.3.16(webpack@5.105.2(esbuild@0.27.3)): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.46.0 - webpack: 5.105.2 - terser@5.46.0: dependencies: '@jridgewell/source-map': 0.3.11 @@ -17103,7 +17091,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(webpack@5.105.2(esbuild@0.27.3)) + terser-webpack-plugin: 5.3.16(esbuild@0.27.3)(webpack@5.105.2(esbuild@0.27.3)) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: From 38ccc0b7b17162a488e1ce9ce033cca8e1608b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjam=C3=ADn=20Vicente?= Date: Wed, 11 Mar 2026 22:10:32 -0300 Subject: [PATCH 5/5] chore(angular): use testing library like the other adapters --- packages/angular-store/package.json | 4 + packages/angular-store/tests/index.test.ts | 115 ++++++--------------- packages/angular-store/tests/test-setup.ts | 1 + pnpm-lock.yaml | 35 ++++++- 4 files changed, 73 insertions(+), 82 deletions(-) diff --git a/packages/angular-store/package.json b/packages/angular-store/package.json index c98988b..d04cd29 100644 --- a/packages/angular-store/package.json +++ b/packages/angular-store/package.json @@ -58,6 +58,10 @@ "@angular/compiler-cli": "^21.1.2", "@angular/core": "^21.1.2", "@angular/platform-browser": "^21.1.2", + "@testing-library/angular": "^19.1.1", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/user-event": "^14.6.1", "ng-packagr": "^21.1.0", "zone.js": "^0.15.1" }, diff --git a/packages/angular-store/tests/index.test.ts b/packages/angular-store/tests/index.test.ts index a5c7ade..b902f89 100644 --- a/packages/angular-store/tests/index.test.ts +++ b/packages/angular-store/tests/index.test.ts @@ -9,11 +9,14 @@ import { untracked, } from '@angular/core' import { TestBed } from '@angular/core/testing' -import { By } from '@angular/platform-browser' +import { render } from '@testing-library/angular' +import userEvent from '@testing-library/user-event' import { createStore } from '@tanstack/store' import { injectStore } from '../src/index' import type { OnInit } from '@angular/core' +const user = userEvent.setup() + function createStableSignal(fn: () => T): () => T { return computed(() => untracked(fn)) } @@ -21,7 +24,7 @@ function createStableSignal(fn: () => T): () => T { const selectorReadsInputStore = createStore({ cats: 2, dogs: 4 }) describe('injectStore', () => { - test(`allows us to select state using a selector`, () => { + test(`allows us to select state using a selector`, async () => { const store = createStore({ select: 0, ignored: 1 }) @Component({ @@ -32,14 +35,11 @@ describe('injectStore', () => { storeVal = injectStore(store, (state) => state.select) } - const fixture = TestBed.createComponent(MyCmp) - fixture.detectChanges() - - const element = fixture.nativeElement - expect(element.textContent).toContain('Store: 0') + const { getByText } = await render(MyCmp) + expect(getByText('Store: 0')).toBeInTheDocument() }) - test('only triggers a re-render when selector state is updated', () => { + test('only triggers a re-render when selector state is updated', async () => { const store = createStore({ select: 0, ignored: 1 }) let count = 0 @@ -62,7 +62,7 @@ describe('injectStore', () => { constructor() { effect(() => { - console.log(this.storeVal()) + this.storeVal() count++ }) } @@ -82,29 +82,16 @@ describe('injectStore', () => { } } - const fixture = TestBed.createComponent(MyCmp) - fixture.detectChanges() - - const element = fixture.nativeElement - const debugElement = fixture.debugElement - - expect(element.textContent).toContain('Store: 0') + const { getByText, getByRole } = await render(MyCmp) + expect(getByText('Store: 0')).toBeInTheDocument() expect(count).toEqual(1) - debugElement - .query(By.css('button#updateSelect')) - .triggerEventHandler('click', null) - - fixture.detectChanges() - expect(element.textContent).toContain('Store: 10') + await user.click(getByRole('button', { name: /update select/i })) + expect(getByText('Store: 10')).toBeInTheDocument() expect(count).toEqual(2) - debugElement - .query(By.css('button#updateIgnored')) - .triggerEventHandler('click', null) - - fixture.detectChanges() - expect(element.textContent).toContain('Store: 10') + await user.click(getByRole('button', { name: /update ignored/i })) + expect(getByText('Store: 10')).toBeInTheDocument() expect(count).toEqual(2) }) @@ -133,9 +120,9 @@ describe('injectStore', () => { expect(storeVal()).toBe(5) }) - test('supports a store created from input signals', () => { + test('supports a store created from input signals', async () => { @Component({ - template: `

{{ storeVal() }}

`, + template: `

{{ storeVal() }}

`, standalone: true, }) class StoreFromInputChildCmp { @@ -156,29 +143,19 @@ describe('injectStore', () => { } const value = signal(3) - const fixture = TestBed.createComponent(StoreFromInputChildCmp, { + const { getByText, findByText } = await render(StoreFromInputChildCmp, { bindings: [inputBinding('value', value)], }) - fixture.detectChanges() - - const debugElement = fixture.debugElement - - expect( - debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, - ).toContain('6') + expect(getByText('6')).toBeInTheDocument() value.set(4) - fixture.detectChanges() - - expect( - debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, - ).toContain('8') + expect(await findByText('8')).toBeInTheDocument() }) - test('supports selectors that read input signals', () => { + test('supports selectors that read input signals', async () => { @Component({ selector: 'app-selector-reads-input', - template: `

{{ count() }}

`, + template: `

{{ count() }}

`, standalone: true, }) class SelectorReadsInputChildCmp { @@ -190,23 +167,13 @@ describe('injectStore', () => { } const animal = signal<'cats' | 'dogs'>('cats') - const fixture = TestBed.createComponent(SelectorReadsInputChildCmp, { + const { getByText, findByText } = await render(SelectorReadsInputChildCmp, { bindings: [inputBinding('animal', animal)], }) - fixture.detectChanges() - - const debugElement = fixture.debugElement - - expect( - debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, - ).toContain('2') + expect(getByText('2')).toBeInTheDocument() animal.set('dogs') - fixture.detectChanges() - - expect( - debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, - ).toContain('4') + expect(await findByText('4')).toBeInTheDocument() }) test('makes the selected store value available on ngOnInit', () => { @@ -248,14 +215,14 @@ describe('injectStore', () => { }) describe('dataType', () => { - test('date change trigger re-render', () => { + test('date change trigger re-render', async () => { const store = createStore({ date: new Date('2025-03-29T21:06:30.401Z') }) @Component({ template: `
-

{{ storeVal() }}

- +

{{ storeVal() }}

+
`, standalone: true, @@ -263,12 +230,6 @@ describe('dataType', () => { class MyCmp { storeVal = injectStore(store, (state) => state.date) - constructor() { - effect(() => { - console.log(this.storeVal()) - }) - } - updateDate() { store.setState((v) => ({ ...v, @@ -277,22 +238,14 @@ describe('dataType', () => { } } - const fixture = TestBed.createComponent(MyCmp) - fixture.detectChanges() - - const debugElement = fixture.debugElement - + const { getByText, getByRole, findByText } = await render(MyCmp) expect( - debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, - ).toContain(new Date('2025-03-29T21:06:30.401Z')) + getByText(new Date('2025-03-29T21:06:30.401Z').toString()), + ).toBeInTheDocument() - debugElement - .query(By.css('button#updateDate')) - .triggerEventHandler('click', null) - - fixture.detectChanges() + await user.click(getByRole('button', { name: /update date/i })) expect( - debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, - ).toContain(new Date('2025-03-29T21:06:40.401Z')) + await findByText(new Date('2025-03-29T21:06:40.401Z').toString()), + ).toBeInTheDocument() }) }) diff --git a/packages/angular-store/tests/test-setup.ts b/packages/angular-store/tests/test-setup.ts index 1b56cce..e17b53c 100644 --- a/packages/angular-store/tests/test-setup.ts +++ b/packages/angular-store/tests/test-setup.ts @@ -1,3 +1,4 @@ +import '@testing-library/jest-dom/vitest' import '@angular/compiler' import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f13f1f3..1c299fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,6 +305,12 @@ importers: '@angular/platform-browser': specifier: ^21.1.2 version: 21.2.0(@angular/animations@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)) + '@testing-library/angular': + specifier: ^19.1.1 + version: 19.1.1(dc0abb6b548f595266efa65842229e8b) + '@testing-library/dom': + specifier: ^10.4.1 + version: 10.4.1 ng-packagr: specifier: ^21.1.0 version: 21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(tslib@2.8.1)(typescript@5.9.3) @@ -3105,6 +3111,15 @@ packages: resolution: {integrity: sha512-FOl8EF6SAcljanKSm5aBeJaflFcxQAytTbxtNW8HC6D4x+UBW68IC4tBcrlrsI0wXHBmC/Gz4Ovvv8qCtiXSgQ==} engines: {node: '>=18'} + '@testing-library/angular@19.1.1': + resolution: {integrity: sha512-ruRBLMTxoxaIqSTBov2wbDHyFUKE5MeiNlk1xzbKjqHvZcmETNHVV2dFOXNOGFOpjBdxQZXLYjR2urOKiKO6yQ==} + peerDependencies: + '@angular/common': '>= 21.0.0' + '@angular/core': '>= 21.0.0' + '@angular/platform-browser': '>= 21.0.0' + '@angular/router': '>= 21.0.0' + '@testing-library/dom': ^10.0.0 + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -11250,6 +11265,15 @@ snapshots: - typescript - vite + '@testing-library/angular@19.1.1(dc0abb6b548f595266efa65842229e8b)': + dependencies: + '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1) + '@angular/platform-browser': 21.2.0(@angular/animations@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)) + '@angular/router': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.0(@angular/animations@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) + '@testing-library/dom': 10.4.1 + tslib: 2.8.1 + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 @@ -16359,6 +16383,15 @@ snapshots: optionalDependencies: esbuild: 0.27.3 + terser-webpack-plugin@5.3.16(webpack@5.105.2(esbuild@0.27.3)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.46.0 + webpack: 5.105.2 + terser@5.46.0: dependencies: '@jridgewell/source-map': 0.3.11 @@ -17091,7 +17124,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(esbuild@0.27.3)(webpack@5.105.2(esbuild@0.27.3)) + terser-webpack-plugin: 5.3.16(webpack@5.105.2(esbuild@0.27.3)) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: