@daltonr/pathwrite-angular 0.4.0 → 0.6.0

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/README.md CHANGED
@@ -15,6 +15,7 @@ For convenience, this package re-exports core types so you don't need to import
15
15
  ```typescript
16
16
  import {
17
17
  PathFacade, // Angular-specific
18
+ PathEngine, // Re-exported from core (value + type)
18
19
  PathData, // Re-exported from core
19
20
  PathDefinition, // Re-exported from core
20
21
  PathEvent, // Re-exported from core
@@ -65,6 +66,7 @@ export class MyComponent {
65
66
 
66
67
  | Method | Description |
67
68
  |--------|-------------|
69
+ | `adoptEngine(engine)` | Adopt an externally-managed `PathEngine` (e.g. from `restoreOrStart()`). The facade immediately reflects the engine's current state and forwards all subsequent events. The caller is responsible for the engine's lifecycle. |
68
70
  | `start(definition, data?)` | Start or re-start a path. |
69
71
  | `restart(definition, data?)` | Tear down any active path (without firing hooks) and start the given path fresh. Safe to call at any time. Use for "Start over" / retry flows. |
70
72
  | `startSubPath(definition, data?, meta?)` | Push a sub-path. Requires an active path. `meta` is returned unchanged to `onSubPathComplete` / `onSubPathCancel`. |
@@ -85,6 +87,128 @@ facade.snapshot()?.data.name; // typed as string (or whatever MyData defines)
85
87
 
86
88
  ---
87
89
 
90
+ ## `injectPath()` — Recommended API for Components
91
+
92
+ **New in v0.6.0** — `injectPath()` provides an ergonomic, signal-based API for accessing the path engine inside Angular components. This is the **recommended approach** for step components and forms — it mirrors React's `usePathContext()` and Vue's `usePath()` for consistency across frameworks.
93
+
94
+ ### Quick start
95
+
96
+ ```typescript
97
+ import { Component } from "@angular/core";
98
+ import { PathFacade, injectPath } from "@daltonr/pathwrite-angular";
99
+
100
+ @Component({
101
+ selector: "app-contact-step",
102
+ standalone: true,
103
+ providers: [PathFacade], // ← Required: provide at this component or a parent
104
+ template: `
105
+ @if (path.snapshot(); as s) {
106
+ <h2>{{ s.activeStep?.title }}</h2>
107
+ <input
108
+ type="text"
109
+ [value]="name"
110
+ (input)="updateName($any($event.target).value)"
111
+ />
112
+ <button (click)="path.next()">Next</button>
113
+ }
114
+ `
115
+ })
116
+ export class ContactStepComponent {
117
+ protected readonly path = injectPath<ContactData>();
118
+ protected name = "";
119
+
120
+ protected updateName(value: string): void {
121
+ this.name = value;
122
+ this.path.setData("name", value); // ← No facade or template ref needed
123
+ }
124
+ }
125
+ ```
126
+
127
+ ### API
128
+
129
+ `injectPath()` returns an object with:
130
+
131
+ | Member | Type | Description |
132
+ |--------|------|-------------|
133
+ | `snapshot` | `Signal<PathSnapshot \| null>` | Current path snapshot as a signal. `null` when no path is active. |
134
+ | `start(path, data?)` | `Promise<void>` | Start or restart a path. |
135
+ | `restart(path, data?)` | `Promise<void>` | Tear down and restart fresh. |
136
+ | `startSubPath(path, data?, meta?)` | `Promise<void>` | Push a sub-path onto the stack. |
137
+ | `next()` | `Promise<void>` | Advance one step. |
138
+ | `previous()` | `Promise<void>` | Go back one step. |
139
+ | `cancel()` | `Promise<void>` | Cancel the active path. |
140
+ | `setData(key, value)` | `Promise<void>` | Update a single data field. Type-safe when `TData` is specified. |
141
+ | `goToStep(stepId)` | `Promise<void>` | Jump to a step by ID (no guard checks). |
142
+ | `goToStepChecked(stepId)` | `Promise<void>` | Jump to a step by ID (guard-checked). |
143
+
144
+ ### Type safety
145
+
146
+ Pass your data type as a generic to get full type checking:
147
+
148
+ ```typescript
149
+ interface ContactData {
150
+ name: string;
151
+ email: string;
152
+ }
153
+
154
+ const path = injectPath<ContactData>();
155
+
156
+ path.setData("name", "Jane"); // ✅ OK
157
+ path.setData("foo", 123); // ❌ Type error: "foo" not in ContactData
158
+ path.snapshot()?.data.name; // ✅ Typed as string
159
+ ```
160
+
161
+ ### Requirements
162
+
163
+ - **Angular 16+** (signals required)
164
+ - `PathFacade` must be provided in the injector tree (either at the component or a parent component)
165
+
166
+ ### Compared to manual facade injection
167
+
168
+ **Before** (manual facade injection + template reference):
169
+ ```typescript
170
+ @Component({
171
+ providers: [PathFacade],
172
+ template: `
173
+ <pw-shell #shell ...>
174
+ <ng-template pwStep="contact">
175
+ <input (input)="shell.facade.setData('name', $any($event.target).value)" />
176
+ </ng-template>
177
+ </pw-shell>
178
+ `
179
+ })
180
+ export class MyComponent {
181
+ protected readonly facade = inject(PathFacade);
182
+ }
183
+ ```
184
+
185
+ **After** (with `injectPath()`):
186
+ ```typescript
187
+ @Component({
188
+ providers: [PathFacade],
189
+ template: `
190
+ <input (input)="updateName($any($event.target).value)" />
191
+ <button (click)="path.next()">Next</button>
192
+ `
193
+ })
194
+ export class MyStepComponent {
195
+ protected readonly path = injectPath<MyData>();
196
+
197
+ protected updateName(value: string): void {
198
+ this.path.setData("name", value); // ← Clean, no template ref
199
+ }
200
+ }
201
+ ```
202
+
203
+ The `injectPath()` approach:
204
+ - **No template references** — access the engine directly from the component class
205
+ - **Signal-native** — `path.snapshot()` returns the reactive signal directly
206
+ - **Type-safe** — generic parameter flows through to all methods
207
+ - **Framework-consistent** — matches React's `usePathContext()` and Vue's `usePath()`
208
+ - **Less Angular-specific knowledge** — just `inject()` and signals, patterns Angular developers already know
209
+
210
+ ---
211
+
88
212
  ## Angular Forms integration — `syncFormGroup`
89
213
 
90
214
  `syncFormGroup` eliminates the boilerplate of manually wiring an Angular
@@ -253,6 +377,38 @@ export class MyComponent {
253
377
 
254
378
  Each `<ng-template pwStep="<stepId>">` is rendered when the active step matches `stepId`. The shell handles all navigation internally.
255
379
 
380
+ > **⚠️ Important: `pwStep` Values Must Match Step IDs**
381
+ >
382
+ > The string value passed to the `pwStep` directive **must exactly match** the corresponding step's `id`:
383
+ >
384
+ > ```typescript
385
+ > const myPath: PathDefinition = {
386
+ > id: 'signup',
387
+ > steps: [
388
+ > { id: 'details' }, // ← Step ID
389
+ > { id: 'review' } // ← Step ID
390
+ > ]
391
+ > };
392
+ > ```
393
+ >
394
+ > ```html
395
+ > <pw-shell [path]="myPath">
396
+ > <ng-template pwStep="details"> <!-- ✅ Matches "details" step -->
397
+ > <app-details-form />
398
+ > </ng-template>
399
+ > <ng-template pwStep="review"> <!-- ✅ Matches "review" step -->
400
+ > <app-review-panel />
401
+ > </ng-template>
402
+ > <ng-template pwStep="foo"> <!-- ❌ No step with id "foo" -->
403
+ > <app-foo-panel />
404
+ > </ng-template>
405
+ > </pw-shell>
406
+ > ```
407
+ >
408
+ > If a `pwStep` value doesn't match any step ID, that template will never be rendered (silent — no error message).
409
+ >
410
+ > **💡 Tip:** Use your IDE's "Go to Definition" on the step ID in your path definition, then copy-paste the exact string when creating the `pwStep` directive. This ensures perfect matching and avoids typos.
411
+
256
412
  ### Context sharing
257
413
 
258
414
  `PathShellComponent` provides a `PathFacade` instance in its own `providers` array
@@ -299,12 +455,13 @@ export class MyComponent {
299
455
  | `path` | `PathDefinition` | *required* | The path definition to drive. |
300
456
  | `initialData` | `PathData` | `{}` | Initial data passed to `facade.start()`. |
301
457
  | `autoStart` | `boolean` | `true` | Start the path automatically on `ngOnInit`. |
302
- | `backLabel` | `string` | `"Back"` | Back button label. |
458
+ | `backLabel` | `string` | `"Previous"` | Previous button label. |
303
459
  | `nextLabel` | `string` | `"Next"` | Next button label. |
304
- | `finishLabel` | `string` | `"Finish"` | Finish button label (last step). |
460
+ | `completeLabel` | `string` | `"Complete"` | Complete button label (last step). |
305
461
  | `cancelLabel` | `string` | `"Cancel"` | Cancel button label. |
306
462
  | `hideCancel` | `boolean` | `false` | Hide the Cancel button. |
307
- | `hideProgress` | `boolean` | `false` | Hide the progress indicator. |
463
+ | `hideProgress` | `boolean` | `false` | Hide the progress indicator. Also hidden automatically for single-step top-level paths. |
464
+ | `footerLayout` | `"wizard" \| "form" \| "auto"` | `"auto"` | Footer button layout. `"auto"` uses `"form"` for single-step top-level paths, `"wizard"` otherwise. `"wizard"`: Back on left, Cancel+Submit on right. `"form"`: Cancel on left, Submit on right, no Back button. |
308
465
 
309
466
  ### Outputs
310
467
 
@@ -346,7 +503,7 @@ export class MyComponent { ... }
346
503
  <ng-template pwShellFooter let-s let-actions="actions">
347
504
  <button (click)="actions.previous()" [disabled]="s.isFirstStep || s.isNavigating">Back</button>
348
505
  <button (click)="actions.next()" [disabled]="!s.canMoveNext || s.isNavigating">
349
- {{ s.isLastStep ? 'Finish' : 'Next' }}
506
+ {{ s.isLastStep ? 'Complete' : 'Next' }}
350
507
  </button>
351
508
  </ng-template>
352
509
  <ng-template pwStep="details"><app-details-form /></ng-template>
@@ -362,6 +519,38 @@ export class MyComponent { ... }
362
519
 
363
520
  Both directives can be combined. Only the sections you override are replaced — a custom header still shows the default footer, and vice versa.
364
521
 
522
+ ### Resetting the path
523
+
524
+ There are two ways to reset `<pw-shell>` to step 1.
525
+
526
+ **Option 1 — Toggle mount** (simplest, always correct)
527
+
528
+ Toggle an `@if` flag to destroy and recreate the shell. Every child component resets from scratch:
529
+
530
+ ```html
531
+ @if (isActive) {
532
+ <pw-shell [path]="myPath" (completed)="isActive = false" (cancelled)="isActive = false">
533
+ <ng-template pwStep="details"><app-details-form /></ng-template>
534
+ </pw-shell>
535
+ } @else {
536
+ <button (click)="isActive = true">Try Again</button>
537
+ }
538
+ ```
539
+
540
+ **Option 2 — Call `restart()` on the shell ref** (in-place, no unmount)
541
+
542
+ Use the existing `#shell` template reference — `restart()` is a public method:
543
+
544
+ ```html
545
+ <pw-shell #shell [path]="myPath" (completed)="onDone($event)">
546
+ <ng-template pwStep="details"><app-details-form /></ng-template>
547
+ </pw-shell>
548
+
549
+ <button (click)="shell.restart()">Try Again</button>
550
+ ```
551
+
552
+ `restart()` resets the path engine to step 1 with the original `[initialData]` without unmounting the component. Use this when you need to keep the shell mounted — for example, to preserve scroll position in a parent container or to drive a CSS transition.
553
+
365
554
  ---
366
555
 
367
556
  ## Sub-Paths
@@ -634,6 +823,79 @@ Use it to distinguish initialization from re-entry:
634
823
 
635
824
  ---
636
825
 
826
+ ## Persistence
827
+
828
+ Use with [@daltonr/pathwrite-store-http](../store-http) for automatic state persistence. The engine is created externally via `restoreOrStart()`, then handed to the facade via `adoptEngine()`.
829
+
830
+ ### Simple persistence example
831
+
832
+ ```typescript
833
+ import { Component, inject, OnInit } from '@angular/core';
834
+ import { PathFacade, PathShellComponent, PathStepDirective } from '@daltonr/pathwrite-angular';
835
+ import { HttpStore, restoreOrStart, httpPersistence } from '@daltonr/pathwrite-store-http';
836
+ import { signupPath } from './signup-path';
837
+
838
+ @Component({
839
+ selector: 'app-signup-wizard',
840
+ standalone: true,
841
+ imports: [PathShellComponent, PathStepDirective],
842
+ providers: [PathFacade],
843
+ template: `
844
+ @if (ready) {
845
+ <pw-shell [path]="path" [autoStart]="false" (completed)="onComplete($event)">
846
+ <ng-template pwStep="details"><app-details-form /></ng-template>
847
+ <ng-template pwStep="review"><app-review-panel /></ng-template>
848
+ </pw-shell>
849
+ } @else {
850
+ <p>Loading…</p>
851
+ }
852
+ `
853
+ })
854
+ export class SignupWizardComponent implements OnInit {
855
+ private readonly facade = inject(PathFacade);
856
+
857
+ path = signupPath;
858
+ ready = false;
859
+
860
+ async ngOnInit() {
861
+ const store = new HttpStore({ baseUrl: '/api/wizard' });
862
+ const key = 'user:signup';
863
+
864
+ const { engine, restored } = await restoreOrStart({
865
+ store,
866
+ key,
867
+ path: this.path,
868
+ initialData: { name: '', email: '' },
869
+ observers: [
870
+ httpPersistence({ store, key, strategy: 'onNext' })
871
+ ]
872
+ });
873
+
874
+ // Hand the running engine to the facade — it immediately
875
+ // reflects the engine's current state and forwards all events.
876
+ this.facade.adoptEngine(engine);
877
+ this.ready = true;
878
+
879
+ if (restored) {
880
+ console.log('Progress restored — resuming from', engine.snapshot()?.stepId);
881
+ }
882
+ }
883
+
884
+ onComplete(data: any) {
885
+ console.log('Wizard completed:', data);
886
+ }
887
+ }
888
+ ```
889
+
890
+ ### Key points
891
+
892
+ - **`adoptEngine()`** connects the facade to an externally-created engine. No `@ViewChild` or shell reference needed — inject `PathFacade` directly.
893
+ - **`[autoStart]="false"`** prevents the shell from calling `start()` on its own — the engine is already running.
894
+ - **The facade is the same one the shell uses** (provided in the component's `providers`), so the shell's template bindings, progress indicator, and navigation buttons all work automatically.
895
+ - **`restoreOrStart()`** handles the load-or-create logic: if saved state exists on the server, it restores the engine mid-flow; otherwise it starts fresh.
896
+
897
+ ---
898
+
637
899
  ## Styling
638
900
 
639
901
  Import the optional stylesheet for sensible default styles. All visual values are CSS custom properties (`--pw-*`) so you can theme without overriding selectors.
package/dist/index.css CHANGED
@@ -204,6 +204,15 @@
204
204
  left: 4px;
205
205
  }
206
206
 
207
+ .pw-shell__validation-label {
208
+ font-weight: 600;
209
+ margin-right: 3px;
210
+ }
211
+
212
+ .pw-shell__validation-label::after {
213
+ content: ":";
214
+ }
215
+
207
216
  /* ------------------------------------------------------------------ */
208
217
  /* Footer — navigation buttons */
209
218
  /* ------------------------------------------------------------------ */
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { OnDestroy, DestroyRef, Signal } from "@angular/core";
2
2
  import { Observable } from "rxjs";
3
- import { PathData, PathDefinition, PathEvent, PathSnapshot } from "@daltonr/pathwrite-core";
3
+ import { PathData, PathDefinition, PathEngine, PathEvent, PathSnapshot } from "@daltonr/pathwrite-core";
4
4
  import * as i0 from "@angular/core";
5
5
  /**
6
6
  * Angular facade over PathEngine. Provide at component level for an isolated
@@ -17,16 +17,31 @@ import * as i0 from "@angular/core";
17
17
  * ```
18
18
  */
19
19
  export declare class PathFacade<TData extends PathData = PathData> implements OnDestroy {
20
- private readonly engine;
20
+ private _engine;
21
21
  private readonly _state$;
22
22
  private readonly _events$;
23
- private readonly unsubscribeFromEngine;
23
+ private _unsubscribeFromEngine;
24
24
  private readonly _stateSignal;
25
25
  readonly state$: Observable<PathSnapshot<TData> | null>;
26
26
  readonly events$: Observable<PathEvent>;
27
27
  /** Signal version of state$. Updates on every path state change. Requires Angular 16+. */
28
28
  readonly stateSignal: Signal<PathSnapshot<TData> | null>;
29
29
  constructor();
30
+ /**
31
+ * Adopt an externally-managed `PathEngine` — for example, the engine returned
32
+ * by `restoreOrStart()` from `@daltonr/pathwrite-store-http`.
33
+ *
34
+ * The facade immediately reflects the engine's current state and forwards all
35
+ * subsequent events. The **caller** is responsible for the engine's lifecycle
36
+ * (starting, cleanup); the facade only subscribes to it.
37
+ *
38
+ * ```typescript
39
+ * const { engine } = await restoreOrStart({ store, key, path, initialData, observers: [...] });
40
+ * facade.adoptEngine(engine);
41
+ * ```
42
+ */
43
+ adoptEngine(engine: PathEngine): void;
44
+ private connectEngine;
30
45
  ngOnDestroy(): void;
31
46
  start(path: PathDefinition<any>, initialData?: PathData): Promise<void>;
32
47
  /**
@@ -50,6 +65,73 @@ export declare class PathFacade<TData extends PathData = PathData> implements On
50
65
  static ɵfac: i0.ɵɵFactoryDeclaration<PathFacade<any>, never>;
51
66
  static ɵprov: i0.ɵɵInjectableDeclaration<PathFacade<any>>;
52
67
  }
68
+ /**
69
+ * Return type of `injectPath()`. Provides signal-based reactive access to the
70
+ * path state and strongly-typed navigation actions. Mirrors React's `usePathContext()`
71
+ * return type for consistency across adapters.
72
+ */
73
+ export interface InjectPathReturn<TData extends PathData = PathData> {
74
+ /** Current path snapshot as a signal. Returns `null` when no path is active. */
75
+ snapshot: Signal<PathSnapshot<TData> | null>;
76
+ /** Start (or restart) a path. */
77
+ start: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
78
+ /** Push a sub-path onto the stack. */
79
+ startSubPath: (path: PathDefinition<any>, initialData?: PathData, meta?: Record<string, unknown>) => Promise<void>;
80
+ /** Advance one step. Completes the path on the last step. */
81
+ next: () => Promise<void>;
82
+ /** Go back one step. No-op when already on the first step. */
83
+ previous: () => Promise<void>;
84
+ /** Cancel the active path (or sub-path). */
85
+ cancel: () => Promise<void>;
86
+ /** Update a single data field. */
87
+ setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
88
+ /** Jump to a step by ID without checking guards. */
89
+ goToStep: (stepId: string) => Promise<void>;
90
+ /** Jump to a step by ID, checking guards first. */
91
+ goToStepChecked: (stepId: string) => Promise<void>;
92
+ /**
93
+ * Tears down any active path and immediately starts the given path fresh.
94
+ * Use for "Start over" / retry flows.
95
+ */
96
+ restart: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
97
+ }
98
+ /**
99
+ * Inject a PathFacade and return a signal-based API for use in Angular components.
100
+ * Requires `PathFacade` to be provided in the component's injector tree (either via
101
+ * `providers: [PathFacade]` in the component or a parent component).
102
+ *
103
+ * **This is the recommended way to consume Pathwrite in Angular components** — it
104
+ * provides the same ergonomic, framework-native API that React's `usePathContext()`
105
+ * and Vue's `usePath()` offer. No template references or manual facade injection needed.
106
+ *
107
+ * The optional generic `TData` narrows `snapshot().data` and `setData()` to your
108
+ * data shape. It is a **type-level assertion**, not a runtime guarantee.
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * @Component({
113
+ * selector: 'app-contact-step',
114
+ * standalone: true,
115
+ * providers: [PathFacade], // ← Provide at this component or a parent
116
+ * template: `
117
+ * @if (path.snapshot(); as s) {
118
+ * <div>Step: {{ s.activeStep?.title }}</div>
119
+ * <button (click)="path.next()">Next</button>
120
+ * }
121
+ * `
122
+ * })
123
+ * export class ContactStepComponent {
124
+ * protected readonly path = injectPath<ContactData>();
125
+ *
126
+ * updateName(name: string) {
127
+ * this.path.setData('name', name);
128
+ * }
129
+ * }
130
+ * ```
131
+ *
132
+ * @throws Error if PathFacade is not provided in the injector tree
133
+ */
134
+ export declare function injectPath<TData extends PathData = PathData>(): InjectPathReturn<TData>;
53
135
  /**
54
136
  * Minimal interface describing what syncFormGroup needs from an Angular
55
137
  * FormGroup. Typed as a duck interface so that @angular/forms is not a
@@ -91,4 +173,5 @@ export interface FormGroupLike {
91
173
  * ```
92
174
  */
93
175
  export declare function syncFormGroup<TData extends PathData = PathData>(facade: PathFacade<TData>, formGroup: FormGroupLike, destroyRef?: DestroyRef): () => void;
94
- export type { PathData, PathDefinition, PathEvent, PathSnapshot, PathStep, PathStepContext, SerializedPathState } from "@daltonr/pathwrite-core";
176
+ export type { PathData, FieldErrors, PathDefinition, PathEvent, PathSnapshot, PathStep, PathStepContext, SerializedPathState } from "@daltonr/pathwrite-core";
177
+ export { PathEngine } from "@daltonr/pathwrite-core";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Injectable, signal } from "@angular/core";
1
+ import { Injectable, signal, inject } from "@angular/core";
2
2
  import { BehaviorSubject, Subject } from "rxjs";
3
3
  import { PathEngine } from "@daltonr/pathwrite-core";
4
4
  import * as i0 from "@angular/core";
@@ -18,15 +18,43 @@ import * as i0 from "@angular/core";
18
18
  */
19
19
  export class PathFacade {
20
20
  constructor() {
21
- this.engine = new PathEngine();
21
+ this._engine = new PathEngine();
22
22
  this._state$ = new BehaviorSubject(null);
23
23
  this._events$ = new Subject();
24
+ this._unsubscribeFromEngine = () => { };
24
25
  this._stateSignal = signal(null);
25
26
  this.state$ = this._state$.asObservable();
26
27
  this.events$ = this._events$.asObservable();
27
28
  /** Signal version of state$. Updates on every path state change. Requires Angular 16+. */
28
29
  this.stateSignal = this._stateSignal.asReadonly();
29
- this.unsubscribeFromEngine = this.engine.subscribe((event) => {
30
+ this.connectEngine(this._engine);
31
+ }
32
+ /**
33
+ * Adopt an externally-managed `PathEngine` — for example, the engine returned
34
+ * by `restoreOrStart()` from `@daltonr/pathwrite-store-http`.
35
+ *
36
+ * The facade immediately reflects the engine's current state and forwards all
37
+ * subsequent events. The **caller** is responsible for the engine's lifecycle
38
+ * (starting, cleanup); the facade only subscribes to it.
39
+ *
40
+ * ```typescript
41
+ * const { engine } = await restoreOrStart({ store, key, path, initialData, observers: [...] });
42
+ * facade.adoptEngine(engine);
43
+ * ```
44
+ */
45
+ adoptEngine(engine) {
46
+ // Disconnect from whatever engine we're currently listening to
47
+ this._unsubscribeFromEngine();
48
+ this._engine = engine;
49
+ this.connectEngine(engine);
50
+ }
51
+ connectEngine(engine) {
52
+ // Seed state immediately — critical when restoring a persisted path since
53
+ // the engine is already running before the facade connects to it.
54
+ const current = engine.snapshot();
55
+ this._state$.next(current);
56
+ this._stateSignal.set(current);
57
+ this._unsubscribeFromEngine = engine.subscribe((event) => {
30
58
  this._events$.next(event);
31
59
  if (event.type === "stateChanged" || event.type === "resumed") {
32
60
  this._state$.next(event.snapshot);
@@ -39,12 +67,12 @@ export class PathFacade {
39
67
  });
40
68
  }
41
69
  ngOnDestroy() {
42
- this.unsubscribeFromEngine();
70
+ this._unsubscribeFromEngine();
43
71
  this._events$.complete();
44
72
  this._state$.complete();
45
73
  }
46
74
  start(path, initialData = {}) {
47
- return this.engine.start(path, initialData);
75
+ return this._engine.start(path, initialData);
48
76
  }
49
77
  /**
50
78
  * Tears down any active path (without firing lifecycle hooks) and immediately
@@ -53,31 +81,31 @@ export class PathFacade {
53
81
  * component that provides this facade.
54
82
  */
55
83
  restart(path, initialData = {}) {
56
- return this.engine.restart(path, initialData);
84
+ return this._engine.restart(path, initialData);
57
85
  }
58
86
  startSubPath(path, initialData = {}, meta) {
59
- return this.engine.startSubPath(path, initialData, meta);
87
+ return this._engine.startSubPath(path, initialData, meta);
60
88
  }
61
89
  next() {
62
- return this.engine.next();
90
+ return this._engine.next();
63
91
  }
64
92
  previous() {
65
- return this.engine.previous();
93
+ return this._engine.previous();
66
94
  }
67
95
  cancel() {
68
- return this.engine.cancel();
96
+ return this._engine.cancel();
69
97
  }
70
98
  setData(key, value) {
71
- return this.engine.setData(key, value);
99
+ return this._engine.setData(key, value);
72
100
  }
73
101
  goToStep(stepId) {
74
- return this.engine.goToStep(stepId);
102
+ return this._engine.goToStep(stepId);
75
103
  }
76
104
  /** Jump to a step by ID, checking the current step's canMoveNext (forward) or
77
105
  * canMovePrevious (backward) guard first. Navigation is blocked if the guard
78
106
  * returns false. Throws if the step ID does not exist. */
79
107
  goToStepChecked(stepId) {
80
- return this.engine.goToStepChecked(stepId);
108
+ return this._engine.goToStepChecked(stepId);
81
109
  }
82
110
  snapshot() {
83
111
  return this._state$.getValue();
@@ -88,6 +116,61 @@ export class PathFacade {
88
116
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathFacade, decorators: [{
89
117
  type: Injectable
90
118
  }], ctorParameters: () => [] });
119
+ /**
120
+ * Inject a PathFacade and return a signal-based API for use in Angular components.
121
+ * Requires `PathFacade` to be provided in the component's injector tree (either via
122
+ * `providers: [PathFacade]` in the component or a parent component).
123
+ *
124
+ * **This is the recommended way to consume Pathwrite in Angular components** — it
125
+ * provides the same ergonomic, framework-native API that React's `usePathContext()`
126
+ * and Vue's `usePath()` offer. No template references or manual facade injection needed.
127
+ *
128
+ * The optional generic `TData` narrows `snapshot().data` and `setData()` to your
129
+ * data shape. It is a **type-level assertion**, not a runtime guarantee.
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * @Component({
134
+ * selector: 'app-contact-step',
135
+ * standalone: true,
136
+ * providers: [PathFacade], // ← Provide at this component or a parent
137
+ * template: `
138
+ * @if (path.snapshot(); as s) {
139
+ * <div>Step: {{ s.activeStep?.title }}</div>
140
+ * <button (click)="path.next()">Next</button>
141
+ * }
142
+ * `
143
+ * })
144
+ * export class ContactStepComponent {
145
+ * protected readonly path = injectPath<ContactData>();
146
+ *
147
+ * updateName(name: string) {
148
+ * this.path.setData('name', name);
149
+ * }
150
+ * }
151
+ * ```
152
+ *
153
+ * @throws Error if PathFacade is not provided in the injector tree
154
+ */
155
+ export function injectPath() {
156
+ const facade = inject(PathFacade, { optional: true });
157
+ if (!facade) {
158
+ throw new Error("injectPath() requires PathFacade to be provided. " +
159
+ "Add 'providers: [PathFacade]' to your component or a parent component.");
160
+ }
161
+ return {
162
+ snapshot: facade.stateSignal,
163
+ start: (path, initialData = {}) => facade.start(path, initialData),
164
+ startSubPath: (path, initialData = {}, meta) => facade.startSubPath(path, initialData, meta),
165
+ next: () => facade.next(),
166
+ previous: () => facade.previous(),
167
+ cancel: () => facade.cancel(),
168
+ setData: (key, value) => facade.setData(key, value),
169
+ goToStep: (stepId) => facade.goToStep(stepId),
170
+ goToStepChecked: (stepId) => facade.goToStepChecked(stepId),
171
+ restart: (path, initialData = {}) => facade.restart(path, initialData),
172
+ };
173
+ }
91
174
  /**
92
175
  * Syncs every key of an Angular FormGroup to the path engine via setData.
93
176
  *
@@ -131,4 +214,5 @@ export function syncFormGroup(facade, formGroup, destroyRef) {
131
214
  destroyRef?.onDestroy(cleanup);
132
215
  return cleanup;
133
216
  }
217
+ export { PathEngine } from "@daltonr/pathwrite-core";
134
218
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}