@ngstato/angular 0.4.0 → 0.4.2

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.
package/dist/README.md CHANGED
@@ -1,394 +1,161 @@
1
- # ngStato
2
-
3
- > Stato est une librairie de gestion d'état Angular moderne pour remplacer NgRx complètement, avec une API plus simple, Signals-first, sans RxJS obligatoire.
4
-
5
- ![version](https://img.shields.io/badge/version-0.2.0-blue)
6
- ![tests](https://img.shields.io/badge/tests-144%20%E2%9C%85-green)
7
- ![bundle](https://img.shields.io/badge/bundle-~3KB-yellow)
8
- ![angular](https://img.shields.io/badge/Angular-17%2B-red)
9
- ![license](https://img.shields.io/badge/license-MIT-lightgrey)
10
-
11
- ---
12
-
13
- ## La même action. Deux approches.
14
-
15
- | ❌ NgRx v20 (officiel, MIT) | ✅ ngStato |
16
- |---|---|
17
- | `loadStudents: rxMethod<void>(` | `async loadStudents(state) {` |
18
- | ` pipe(` | ` state.isLoading = true` |
19
- | ` tap(() => patchState(store, { isLoading: true })),` | ` state.students = await service.getAll()` |
20
- | ` switchMap(() =>` | ` state.isLoading = false` |
21
- | ` from(service.getAll()).pipe(` | `}` |
22
- | ` tapResponse({` | |
23
- | ` next: (s) => patchState(store, { students: s, isLoading: false }),` | **1 concept : async/await** |
24
- | ` error: (e) => patchState(store, { error: e.message, isLoading: false })` | **5 lignes** |
25
- | ` })` | |
26
- | ` )` | |
27
- | ` )` | |
28
- | ` )` | |
29
- | `)` | |
30
- | **9 concepts RxJS/NgRx — 14 lignes** | |
31
-
32
- ---
33
-
34
- ## Pourquoi switcher vers ngStato ?
35
-
36
- **1 concept au lieu de 9 pour écrire une action async**
37
- NgRx nécessite rxMethod, pipe, tap, switchMap, from, tapResponse, patchState... ngStato nécessite uniquement async/await — natif JavaScript.
38
-
39
- **2x moins de code pour le même résultat**
40
- Sur un store CRUD complet (5 actions), NgRx v20 nécessite ~90 lignes. ngStato nécessite ~45 lignes.
41
-
42
- **DevTools sans extension browser**
43
- NgRx DevTools nécessite l'extension Chrome Redux DevTools. ngStato intègre ses DevTools directement dans l'app — fonctionnels sur tous les browsers et mobile.
44
-
45
- **Protection production automatique**
46
- ngStato utilise `isDevMode()` d'Angular — les DevTools sont physiquement impossibles à activer en prod.
47
-
48
- **38x plus léger — ~3 KB vs ~50 KB gzip**
49
-
50
- ---
51
-
52
- ## Installation
53
-
54
- ```bash
55
- npm install @ngstato/core @ngstato/angular
56
- ```
57
-
58
- ```ts
59
- // app.config.ts
60
- import { provideStato } from '@ngstato/angular'
61
- import { isDevMode } from '@angular/core'
62
-
63
- export const appConfig: ApplicationConfig = {
64
- providers: [
65
- provideRouter(routes),
66
- provideStato({
67
- http: { baseUrl: 'https://api.monapp.com', timeout: 8000 },
68
- devtools: isDevMode()
69
- })
70
- ]
71
- }
72
- ```
73
-
74
- ---
75
-
76
- ## Créer un store
77
-
78
- ```ts
79
- // user.store.ts
80
- import { createStore, http, optimistic, retryable, connectDevTools } from '@ngstato/core'
81
- import { StatoStore, injectStore } from '@ngstato/angular'
82
-
83
- function createUserStore() {
84
- const store = createStore({
85
-
86
- // State
87
- users: [] as User[],
88
- isLoading: false,
89
- error: null as string | null,
90
-
91
- // Computed — recalculés automatiquement
92
- computed: {
93
- total: (state) => state.users.length,
94
- admins: (state) => state.users.filter(u => u.role === 'admin')
95
- },
96
-
97
- // Actions
98
- actions: {
99
-
100
- // Chargement avec retry automatique
101
- loadUsers: retryable(
102
- async (state) => {
103
- state.isLoading = true
104
- state.users = await http.get('/users')
105
- state.isLoading = false
106
- },
107
- { attempts: 3, backoff: 'exponential', delay: 1000 }
108
- ),
109
-
110
- // Suppression avec rollback automatique
111
- deleteUser: optimistic(
112
- (state, id: string) => {
113
- state.users = state.users.filter(u => u.id !== id)
114
- },
115
- async (_, id: string) => {
116
- await http.delete(`/users/${id}`)
117
- }
118
- ),
119
-
120
- // Action simple
121
- async addUser(state, user: UserCreate) {
122
- const created = await http.post('/users', user)
123
- state.users = [...state.users, created]
124
- }
125
- },
126
-
127
- // Hooks lifecycle
128
- hooks: {
129
- onError: (err, name) => console.error(`[UserStore] ${name}:`, err.message)
130
- }
131
- })
132
-
133
- connectDevTools(store, 'UserStore') // une seule ligne
134
- return store
135
- }
136
-
137
- // Injection auto + API classe sans boilerplate
138
- export const UserStore = StatoStore(() => {
139
- return createUserStore()
140
- })
141
-
142
- // Dans un composant
143
- @Component({ ... })
144
- export class UserListComponent {
145
- store = injectStore(UserStore) // ou inject(UserStore)
146
- }
147
- ```
148
-
149
- ---
150
-
151
- ## Helpers
152
-
153
- | Helper | Description | Équivalent NgRx |
154
- |--------|-------------|-----------------|
155
- | `abortable()` | Annule la requête précédente automatiquement | switchMap |
156
- | `debounced()` | Debounce sans RxJS — défaut 300ms | debounceTime |
157
- | `throttled()` | Throttle sans RxJS | throttleTime |
158
- | `retryable()` | Retry avec backoff fixe ou exponentiel | retryWhen |
159
- | `fromStream()` | Écoute Observable/WebSocket/Firebase/Supabase | rxMethod + Effect |
160
- | `optimistic()` | Update immédiat + rollback automatique si échec | Manuel en NgRx |
161
- | `withPersist()` | Persistance state (localStorage/sessionStorage) + migration | Meta-reducers custom |
162
-
163
- ```ts
164
- import { abortable, debounced, throttled, retryable, fromStream, optimistic } from '@ngstato/core'
165
-
166
- actions: {
167
- // Annulation auto — comme switchMap
168
- search: abortable(async (state, q: string, { signal }) => {
169
- state.results = await fetch(`/api/search?q=${q}`, { signal }).then(r => r.json())
170
- }),
171
-
172
- // Debounce 300ms
173
- filter: debounced((state, q: string) => { state.query = q }, 300),
174
-
175
- // Retry x3 avec backoff exponentiel
176
- load: retryable(async (state) => {
177
- state.data = await http.get('/data')
178
- }, { attempts: 3, backoff: 'exponential' }),
179
-
180
- // Realtime WebSocket
181
- listen: fromStream(
182
- () => webSocket('wss://api.monapp.com/ws'),
183
- (state, msg) => { state.messages = [...state.messages, msg] }
184
- ),
185
-
186
- // Optimistic + rollback auto
187
- delete: optimistic(
188
- (state, id) => { state.items = state.items.filter(i => i.id !== id) },
189
- async (_, id) => { await http.delete(`/items/${id}`) }
190
- )
191
- }
192
- ```
193
-
194
- ---
195
-
196
- ## Nouveautés v0.2
197
-
198
- - `selectors` memoïzés avec recalcul ciblé
199
- - `effects` réactifs explicites avec cleanup
200
- - `withPersist()` pour hydrate/persist avec versioning
201
-
202
- ---
203
-
204
- ## Client HTTP
205
-
206
- ```ts
207
- import { http } from '@ngstato/core'
208
-
209
- // Configurer via provideStato() — une seule fois
210
- provideStato({
211
- http: {
212
- baseUrl: 'https://api.monapp.com',
213
- timeout: 8000,
214
- headers: { 'X-App-Version': '1.0' },
215
- auth: () => localStorage.getItem('token')
216
- }
217
- })
218
-
219
- // Utiliser partout dans les actions
220
- await http.get('/users')
221
- await http.get('/users', { params: { page: 1, limit: 10 } })
222
- await http.post('/users', { name: 'Alice' })
223
- await http.put('/users/1', { name: 'Bob' })
224
- await http.patch('/users/1', { active: false })
225
- await http.delete('/users/1')
226
- ```
227
-
228
- ---
229
-
230
- ## DevTools
231
-
232
- Panel intégré dans l'app — sans extension browser, sans installation supplémentaire.
233
-
234
- - Panel déplaçable à la souris
235
- - Redimensionnable — coin bas-droite
236
- - Minimisable — bouton ▼/▲
237
- - Historique des actions avec durées et timestamps
238
- - Diff Avant/Après pour chaque action
239
- - Onglet State — state actuel complet
240
- - **Désactivé automatiquement en production via `isDevMode()`**
241
-
242
- ```ts
243
- // app.config.ts
244
- provideStato({ devtools: isDevMode() })
245
-
246
- // app.component.ts
247
- import { StatoDevToolsComponent } from '@ngstato/angular'
248
-
249
- @Component({
250
- imports: [RouterOutlet, StatoDevToolsComponent],
251
- template: `<router-outlet /><stato-devtools />`
252
- })
253
- export class AppComponent {}
254
-
255
- // mon-store.ts
256
- connectDevTools(store, 'MonStore') // une seule ligne
257
- ```
258
-
259
- | | NgRx DevTools | ngStato DevTools |
260
- |---|---|---|
261
- | Installation | Extension Chrome | Zéro installation |
262
- | Browser support | Chrome/Firefox | Tous browsers |
263
- | Mobile | ❌ | ✅ |
264
- | Désactivé en prod | Manuel | `isDevMode()` auto |
265
- | State visible en prod | Oui si oubli | Jamais |
266
-
267
- ---
268
-
269
- ## Guide de migration NgRx → ngStato
270
-
271
- La migration est progressive — store par store.
272
-
273
- ```ts
274
- // withState → state initial
275
- // NgRx
276
- withState({ users: [] as User[], isLoading: false })
277
- // ngStato
278
- users: [] as User[], isLoading: false,
279
-
280
- // withMethods + rxMethod → actions
281
- // NgRx
282
- withMethods((store) => ({
283
- load: rxMethod<void>(pipe(
284
- tap(() => patchState(store, { isLoading: true })),
285
- switchMap(() => from(service.get()).pipe(
286
- tapResponse({
287
- next: (d) => patchState(store, { data: d, isLoading: false }),
288
- error: (e) => patchState(store, { error: e.message })
289
- })
290
- ))
291
- ))
292
- }))
293
- // ngStato
294
- actions: {
295
- async load(state) {
296
- state.isLoading = true
297
- state.data = await service.get()
298
- state.isLoading = false
299
- }
300
- }
301
-
302
- // withComputed → computed
303
- // NgRx
304
- withComputed((store) => ({
305
- total: computed(() => store.users().length)
306
- }))
307
- // ngStato
308
- computed: {
309
- total: (state) => state.users.length
310
- }
311
- ```
312
-
313
- ---
314
-
315
- ## Comparaison NgRx SignalStore v20 vs ngStato
316
-
317
- | Feature | NgRx SignalStore v20 | ngStato v0.1 |
318
- |---------|---------------------|--------------|
319
- | withState | ✅ | ✅ |
320
- | withMethods / actions | ✅ rxMethod requis | ✅ async/await |
321
- | withComputed | ✅ | ✅ |
322
- | patchState | ✅ obligatoire | ✅ state.x = y |
323
- | provideStore | ✅ | ✅ provideStato() |
324
- | inject() | ✅ | ✅ injectStore() |
325
- | onInit / onDestroy | ✅ | ✅ |
326
- | DevTools | ✅ extension Chrome | ✅ panel intégré |
327
- | DevTools mobile | ❌ | ✅ |
328
- | Protection prod | ⚠️ logOnly manuel | ✅ isDevMode() auto |
329
- | RxJS requis | ✅ obligatoire | ❌ optionnel |
330
- | Bundle size | ~50 KB gzip | ~3 KB gzip |
331
- | withProps | ✅ | 🔜 v0.2 |
332
- | withEntities | ✅ | 🔜 v1.0 |
333
- | signalStoreFeature() | ✅ | 🔜 v0.4 |
334
- | Schematics CLI | ✅ | 🔜 v1.0 |
335
- | ESLint plugin | ✅ | 🔜 v1.0 |
336
-
337
- ---
338
-
339
- ## Demo live
340
-
341
- <a href="https://stackblitz.com/github/becher/ngstato-demo" target="_blank" rel="noopener">
342
- <img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz">
343
- </a>
344
-
345
- ---
346
-
347
- ## Roadmap
348
-
349
- ### v0.1 ✅ TERMINÉ
350
- - `createStore()` — state, actions, computed, hooks
351
- - `StatoHttp` — GET POST PUT PATCH DELETE avec auth, timeout, params
352
- - `abortable()`, `debounced()`, `throttled()`, `retryable()`, `fromStream()`, `optimistic()`
353
- - `@ngstato/angular` — Signals natifs, `provideStato()`, `injectStore()`
354
- - DevTools — panel déplaçable, redimensionnable, minimisable
355
- - `connectDevTools()` — connexion automatique store → DevTools
356
- - Protection prod automatique via `isDevMode()`
357
- - **144 tests — 100% passing**
358
-
359
- ### v0.2 — Selectors / Effects / Persist ✅
360
- - `selectors` memoïzés
361
- - `effects` réactifs avec cleanup
362
- - `withPersist()` — localStorage / sessionStorage + migration
363
-
364
- ### v0.3 — Helpers avancés
365
- - `exclusive()` — = exhaustMap NgRx
366
- - `queued()` — = concatMap NgRx
367
- - `store.on()` — réactions inter-stores
368
- - Testing utilities
369
- - DevTools time-travel
370
-
371
- ### v1.0 — Production ready
372
- - `withEntities()`, Schematics CLI, ESLint plugin
373
- - Documentation VitePress complète
374
- - Benchmarks comparatifs
375
-
376
- ---
377
-
378
- ## Contribuer
379
-
380
- ```bash
381
- git clone https://github.com/becher/ngstato
382
- cd ngstato
383
- pnpm install # Node >= 18, pnpm >= 8
384
- pnpm build
385
- pnpm test # 144 tests
386
- ```
387
-
388
- Convention commits : `feat` / `fix` / `docs` / `test` / `refactor` / `chore`
389
-
390
- ---
391
-
392
- ## License
393
-
394
- MIT — Copyright (c) 2025 ngStato
1
+ <div align="center">
2
+
3
+ # @ngstato/angular
4
+
5
+ ### NgRx requires 9 concepts for an async action. ngStato requires 1.
6
+
7
+ **Angular Signals. Dependency injection. Built-in DevTools. ~1 KB.**
8
+
9
+ [![npm](https://img.shields.io/badge/npm-v0.3.1-blue)](https://www.npmjs.com/package/@ngstato/angular)
10
+ [![Angular](https://img.shields.io/badge/Angular-17%2B-dd0031)](https://angular.dev)
11
+ [![gzip](https://img.shields.io/badge/gzip-~1KB-brightgreen)](#)
12
+ [![license](https://img.shields.io/badge/license-MIT-lightgrey)](#)
13
+
14
+ [Documentation](https://becher.github.io/ngStato/) · [Angular Guide](https://becher.github.io/ngStato/guide/angular) · [API Reference](https://becher.github.io/ngStato/api/core)
15
+
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install @ngstato/core @ngstato/angular
24
+ ```
25
+
26
+ ## Full working example — 3 files
27
+
28
+ **1. Config** one-time setup:
29
+
30
+ ```ts
31
+ // app.config.ts
32
+ provideStato({
33
+ http: { baseUrl: 'https://api.example.com' },
34
+ devtools: isDevMode()
35
+ })
36
+ ```
37
+
38
+ **2. Store** — state + actions + selectors in one object:
39
+
40
+ ```ts
41
+ // users.store.ts
42
+ import { createStore, http, connectDevTools } from '@ngstato/core'
43
+ import { StatoStore } from '@ngstato/angular'
44
+
45
+ export const UsersStore = StatoStore(() => {
46
+ const store = createStore({
47
+ users: [] as User[],
48
+ loading: false,
49
+ error: null as string | null,
50
+
51
+ selectors: {
52
+ total: (s) => s.users.length,
53
+ admins: (s) => s.users.filter(u => u.role === 'admin')
54
+ },
55
+
56
+ actions: {
57
+ async loadUsers(state) {
58
+ state.loading = true
59
+ state.users = await http.get('/users')
60
+ state.loading = false
61
+ },
62
+
63
+ async deleteUser(state, id: string) {
64
+ await http.delete(`/users/${id}`)
65
+ state.users = state.users.filter(u => u.id !== id)
66
+ }
67
+ },
68
+
69
+ hooks: { onInit: (s) => s.loadUsers() }
70
+ })
71
+
72
+ connectDevTools(store, 'Users')
73
+ return store
74
+ })
75
+ ```
76
+
77
+ **3. Component** — everything is a Signal:
78
+
79
+ ```ts
80
+ // users.component.ts
81
+ @Component({
82
+ template: `
83
+ @if (store.loading()) { <spinner /> }
84
+
85
+ <h2>Users ({{ store.total() }})</h2>
86
+
87
+ @for (user of store.users(); track user.id) {
88
+ <div>
89
+ {{ user.name }}
90
+ <button (click)="store.deleteUser(user.id)">×</button>
91
+ </div>
92
+ }
93
+ `
94
+ })
95
+ export class UsersComponent {
96
+ store = injectStore(UsersStore)
97
+ }
98
+ ```
99
+
100
+ **That's the entire pattern.** No reducers, no effects class, no action creators, no selectors file.
101
+
102
+ ---
103
+
104
+ ## What you get
105
+
106
+ | What you define | What Angular gets |
107
+ |:--|:--|
108
+ | `users: []` (state) | `store.users()` → `WritableSignal` |
109
+ | `total: (s) => s.users.length` (selector) | `store.total()` → `computed Signal` (memoized) |
110
+ | `async loadUsers(state) { ... }` (action) | `store.loadUsers()` → `Promise<void>` |
111
+
112
+ All Signals **update automatically**. Zero manual subscriptions.
113
+
114
+ ---
115
+
116
+ ## DevTools — zero install, all browsers
117
+
118
+ ```html
119
+ <stato-devtools />
120
+ ```
121
+
122
+ - Draggable, resizable, minimizable panel
123
+ - Action history with timestamps and duration
124
+ - State diffs — before/after for every action
125
+ - Current state explorer
126
+ - **Works on mobile**
127
+ - **Impossible to enable in production** (`isDevMode()`)
128
+
129
+ | | NgRx | ngStato |
130
+ |:--|:--|:--|
131
+ | Setup | Chrome extension | `<stato-devtools />` |
132
+ | Mobile | ❌ | ✅ |
133
+ | Prod safety | Manual | **Automatic** |
134
+
135
+ ---
136
+
137
+ ## Why teams switch
138
+
139
+ | | NgRx v21 | @ngstato |
140
+ |:--|:--|:--|
141
+ | **Bundle** | ~50 KB gzip | **~4 KB** (core + angular) |
142
+ | **Async action** | `rxMethod` + `pipe` + `tap` + `switchMap` + `from` + `tapResponse` + `patchState` | **`async/await`** |
143
+ | **CRUD store** | ~90 lines, 3+ files | **~45 lines, 1 file** |
144
+ | **DevTools** | Chrome only | **All browsers + mobile** |
145
+ | **Entity adapter** | ✅ | ✅ |
146
+ | **Feature composition** | ✅ | ✅ `mergeFeatures()` |
147
+ | **Concurrency** | RxJS operators | ✅ `exclusive` `queued` `abortable` |
148
+ | **Testing** | `provideMockStore` | ✅ `createMockStore()` |
149
+ | **Persistence** | Custom meta-reducers | ✅ `withPersist()` |
150
+
151
+ ---
152
+
153
+ ## 📖 Documentation
154
+
155
+ **[becher.github.io/ngStato](https://becher.github.io/ngStato/)**
156
+
157
+ [Start in 5 min](https://becher.github.io/ngStato/guide/start-in-5-min) · [Angular guide](https://becher.github.io/ngStato/guide/angular) · [Testing](https://becher.github.io/ngStato/guide/testing) · [CRUD recipe](https://becher.github.io/ngStato/recipes/crud) · [NgRx migration](https://becher.github.io/ngStato/migration/ngrx-to-ngstato) · [API](https://becher.github.io/ngStato/api/core)
158
+
159
+ ## License
160
+
161
+ MIT
@@ -8,6 +8,11 @@ export declare class StatoDevToolsComponent implements OnInit, OnDestroy {
8
8
  activeTab: import("@angular/core").WritableSignal<"actions" | "state">;
9
9
  logs: import("@angular/core").WritableSignal<ActionLog[]>;
10
10
  selectedLog: import("@angular/core").WritableSignal<ActionLog>;
11
+ activeLogId: import("@angular/core").WritableSignal<number>;
12
+ isTimeTraveling: import("@angular/core").WritableSignal<boolean>;
13
+ globalState: import("@angular/core").Signal<Map<string, unknown>>;
14
+ canUndo: import("@angular/core").Signal<boolean>;
15
+ canRedo: import("@angular/core").Signal<boolean>;
11
16
  posX: import("@angular/core").WritableSignal<number>;
12
17
  posY: import("@angular/core").WritableSignal<number>;
13
18
  panelWidth: import("@angular/core").WritableSignal<number>;
@@ -29,10 +34,19 @@ export declare class StatoDevToolsComponent implements OnInit, OnDestroy {
29
34
  clear(): void;
30
35
  selectLog(log: ActionLog): void;
31
36
  formatTime(iso: string): string;
37
+ onTravelTo(log: ActionLog): void;
38
+ onUndo(): void;
39
+ onRedo(): void;
40
+ onResume(): void;
41
+ onReplay(log: ActionLog, event: Event): void;
42
+ isFutureLog(log: ActionLog): boolean;
43
+ onExport(): void;
44
+ onImport(): void;
45
+ onFileSelected(event: Event): void;
32
46
  onDragStart(e: MouseEvent): void;
33
47
  onResizeStart(e: MouseEvent): void;
34
48
  onMouseMove(e: MouseEvent): void;
35
49
  onMouseUp(): void;
36
50
  static ɵfac: i0.ɵɵFactoryDeclaration<StatoDevToolsComponent, never>;
37
- static ɵcmp: i0.ɵɵComponentDeclaration<StatoDevToolsComponent, "ngstato-devtools", never, {}, {}, never, never, true, never>;
51
+ static ɵcmp: i0.ɵɵComponentDeclaration<StatoDevToolsComponent, "ngstato-devtools, stato-devtools", never, {}, {}, never, never, true, never>;
38
52
  }