Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add angular store #41

Merged
merged 2 commits into from
Feb 9, 2024
Merged
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
23 changes: 23 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,29 @@
]
}
]
},
{
"framework": "angular",
"menuItems": [
{
"label": "Getting Started",
"children": [
{
"label": "Quick Start",
"to": "framework/angular/quick-start"
}
]
},
{
"label": "API Reference",
"children": [
{
"label": "injectStore",
"to": "framework/angular/reference/injectStore"
}
]
}
]
}
]
}
73 changes: 73 additions & 0 deletions docs/framework/angular/quick-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: Quick Start
id: quick-start
---

The basic angular app example to get started with the Tanstack angular-store.

**app.component.ts**
```html
<h1>How many of your friends like cats or dogs?</h1>
<p>Press one of the buttons to add a counter of how many of your friends like cats or dogs</p>
<app-increment animal="dogs" />
<app-display animal="dogs" />
<app-increment animal="cats" />
<app-display animal="cats" />
```

**store.ts**
```js
import { Store } from '@tanstack/store';

// You can use @tanstack/store outside of App components too!
export const store = new Store({
dogs: 0,
cats: 0,
});

export function updateState(animal: 'dogs' | 'cats') {
store.setState((state) => {
return {
...state,
[animal]: state[animal] + 1,
};
});
}
```

**display.component.ts**
```typescript
import { injectStore } from '@tanstack/angular-store';
import { store } from './store';

@Component({
selector: 'app-display',
template: `
<!-- This will only re-render when animal changes. If an unrelated store property changes, it won't re-render -->
<div>{{ animal() }}: {{ count() }}</div>
`,
standalone: true
})
export class Display {
animal = input.required<string>();
count = injectStore(store, (state) => state[this.animal()]);
}
```

**increment.component.ts**
```typescript
import { injectStore } from '@tanstack/angular-store';
import { store, updateState } from './store';

@Component({
selector: 'app-increment',
template: `
<button (click)="updateState(animal())">My Friend Likes {{ animal() }}</button>
`,
standalone: true
})
export class Increment {
animal = input.required<string>();
updateState = injectStore(store, updateState);
}
```
6 changes: 6 additions & 0 deletions docs/framework/angular/reference/injectStore.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Inject Store
id: injectStore
---

Please see [/packages/angular-store/src/index.ts](https://github.com/tanstack/store/tree/main/packages/angular-store/src/index.ts)
8 changes: 8 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ npm install @tanstack/vue-store
```

TanStack Store is compatible with Vue 2 and 3.

## Angular

```sh
npm install @tanstack/angular-store
```

TanStack Store is compatible with Angular 16+
2 changes: 1 addition & 1 deletion docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ title: Overview
id: overview
---

TanStack Store is a framework agnostic data store that ships with framework specific adapters for major frameworks like React, Solid, Vue and Svelte.
TanStack Store is a framework agnostic data store that ships with framework specific adapters for major frameworks like React, Solid, Vue, Angular, and Svelte.

TanStack Store is primarily used for state management internally for most framework agnostic TanStack libraries. It can also be used as a standalone library for any framework or application.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@types/react-dom": "^18.0.5",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"@vitest/coverage-istanbul": "^1.1.0",
"@vitest/coverage-istanbul": "^1.2.2",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
Expand All @@ -66,8 +66,8 @@
"typescript49": "npm:[email protected]",
"typescript50": "npm:[email protected]",
"typescript51": "npm:[email protected]",
"vite": "^5.0.10",
"vitest": "^1.1.0",
"vite": "^5.1.0",
"vitest": "^1.2.2",
"vue": "^3.3.4"
}
}
11 changes: 11 additions & 0 deletions packages/angular-store/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @ts-check

/** @type {import('eslint').Linter.Config} */
const config = {
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json',
},
}

module.exports = config
69 changes: 69 additions & 0 deletions packages/angular-store/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"name": "@tanstack/angular-store",
"author": "Tanner Linsley",
"version": "0.3.1",
"license": "MIT",
"repository": "tanstack/store",
"homepage": "https://tanstack.com/store",
"description": "",
"scripts": {
"clean": "rimraf ./dist && rimraf ./coverage",
"test:eslint": "eslint --ext .ts,.tsx ./src",
"test:types": "tsc",
"test:lib": "vitest",
"test:lib:dev": "pnpm run test:lib --watch",
"test:build": "publint --strict",
"build": "vite build"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"keywords": [
"store",
"typescript",
"angular"
],
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"type": "module",
"types": "dist/esm/index.d.ts",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.js",
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
}
},
"./package.json": "./package.json"
},
"sideEffects": false,
"files": [
"dist",
"src"
],
"dependencies": {
"@tanstack/store": "workspace:*"
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "^0.2.32",
"@angular/core": "^17.1.2",
"@angular/common": "^17.1.2",
"@angular/compiler": "^17.1.2",
"@angular/compiler-cli": "^17.1.2",
"@angular/platform-browser": "^17.1.2",
"@angular/platform-browser-dynamic": "^17.1.2",
"zone.js": "^0.14.3"
},
"peerDependencies": {
"@angular/core": ">=16 < 18",
"@angular/common": ">=16 < 18"
}
}
76 changes: 76 additions & 0 deletions packages/angular-store/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
effect,
signal,
type CreateSignalOptions,
Injector,
assertInInjectionContext,
inject,
runInInjectionContext,
} from '@angular/core'
import type { AnyUpdater, Store } from '@tanstack/store'

type NoInfer<T> = [T][T extends any ? 0 : never]

export function injectStore<
TState,
TSelected = NoInfer<TState>,
TUpdater extends AnyUpdater = AnyUpdater,
>(
store: Store<TState, TUpdater>,
selector: (state: NoInfer<TState>) => TSelected = (d) => d as TSelected,
options: CreateSignalOptions<TSelected> & { injector?: Injector } = {
equal: shallow,
},
) {
!options.injector && assertInInjectionContext(injectStore)

if (!options.injector) {
options.injector = inject(Injector)
}

return runInInjectionContext(options.injector, () => {
const slice = signal(selector(store.state), options)

effect(
(onCleanup) => {
const unsub = store.subscribe(() => {
slice.set(selector(store.state))
})
onCleanup(unsub)
},
{ allowSignalWrites: true },
)

return slice.asReadonly()
})
}

function shallow<T>(objA: T, objB: T) {
if (Object.is(objA, objB)) {
return true
}

if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false
}

const keysA = Object.keys(objA)
if (keysA.length !== Object.keys(objB).length) {
return false
}

for (let i = 0; i < keysA.length; i++) {
if (
!Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
!Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
) {
return false
}
}
return true
}
Loading
Loading