@daltonr/pathwrite-angular 0.1.5 → 0.2.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
@@ -28,11 +28,12 @@ export class MyComponent {
28
28
 
29
29
  ## PathFacade API
30
30
 
31
- ### Observables
31
+ ### Observables and signals
32
32
 
33
33
  | Member | Type | Description |
34
34
  |--------|------|-------------|
35
35
  | `state$` | `Observable<PathSnapshot \| null>` | Current snapshot. `null` when no path is active. Backed by a `BehaviorSubject` — late subscribers receive the current value immediately. |
36
+ | `stateSignal` | `Signal<PathSnapshot \| null>` | Signal version of `state$`. Same value, updated synchronously. Use directly in signal-based components without `toSignal()`. |
36
37
  | `events$` | `Observable<PathEvent>` | All engine events: `stateChanged`, `completed`, `cancelled`, `resumed`. |
37
38
 
38
39
  ### Methods
@@ -42,12 +43,106 @@ export class MyComponent {
42
43
  | `start(definition, data?)` | Start or re-start a path. |
43
44
  | `startSubPath(definition, data?)` | Push a sub-path. Requires an active path. |
44
45
  | `next()` | Advance one step. Completes the path on the last step. |
45
- | `previous()` | Go back one step. Cancels the path from the first step. |
46
+ | `previous()` | Go back one step. No-op when already on the first step of a top-level path. |
46
47
  | `cancel()` | Cancel the active path (or sub-path). |
47
- | `setData(key, value)` | Update a single data value; emits `stateChanged`. |
48
- | `goToStep(stepId)` | Jump directly to a step by ID. |
48
+ | `setData(key, value)` | Update a single data value; emits `stateChanged`. When `TData` is specified, `key` and `value` are type-checked against your data shape. |
49
+ | `goToStep(stepId)` | Jump directly to a step by ID. Calls `onLeave`/`onEnter`; bypasses guards and `shouldSkip`. |
50
+ | `goToStepChecked(stepId)` | Jump to a step by ID, checking `canMoveNext` (forward) or `canMovePrevious` (backward) first. Blocked if the guard returns false. |
49
51
  | `snapshot()` | Synchronous read of the current `PathSnapshot \| null`. |
50
52
 
53
+ `PathFacade` accepts an optional generic `PathFacade<TData>`. Because Angular's DI cannot carry generics at runtime, narrow it with a cast at the injection site:
54
+
55
+ ```typescript
56
+ protected readonly facade = inject(PathFacade) as PathFacade<MyData>;
57
+ facade.snapshot()?.data.name; // typed as string (or whatever MyData defines)
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Angular Forms integration — `syncFormGroup`
63
+
64
+ `syncFormGroup` eliminates the boilerplate of manually wiring an Angular
65
+ `FormGroup` to the path engine. Call it once (typically in `ngOnInit`) and every
66
+ form value change is automatically propagated to the engine via `setData`, keeping
67
+ `canMoveNext` guards reactive without any manual plumbing.
68
+
69
+ ```typescript
70
+ import { PathFacade, syncFormGroup } from "@daltonr/pathwrite-angular";
71
+
72
+ @Component({
73
+ providers: [PathFacade],
74
+ template: `
75
+ <form [formGroup]="form">
76
+ <input formControlName="name" />
77
+ <input formControlName="email" />
78
+ </form>
79
+ <button [disabled]="!(snapshot()?.canMoveNext)" (click)="facade.next()">Next</button>
80
+ `
81
+ })
82
+ export class DetailsStepComponent implements OnInit {
83
+ protected readonly facade = inject(PathFacade);
84
+ protected readonly snapshot = toSignal(this.facade.state$, { initialValue: null });
85
+
86
+ protected readonly form = new FormGroup({
87
+ name: new FormControl('', Validators.required),
88
+ email: new FormControl('', [Validators.required, Validators.email]),
89
+ });
90
+
91
+ async ngOnInit() {
92
+ await this.facade.start(myPath, { name: '', email: '' });
93
+ // Immediately syncs current form values and keeps them in sync on every change.
94
+ // Cleanup is automatic when the component is destroyed.
95
+ syncFormGroup(this.facade, this.form, inject(DestroyRef));
96
+ }
97
+ }
98
+ ```
99
+
100
+ The corresponding path definition can now use a clean `canMoveNext` guard with no
101
+ manual sync code in the template:
102
+
103
+ ```typescript
104
+ const myPath: PathDefinition = {
105
+ id: 'registration',
106
+ steps: [
107
+ {
108
+ id: 'details',
109
+ canMoveNext: (ctx) =>
110
+ typeof ctx.data.name === 'string' && ctx.data.name.trim().length > 0 &&
111
+ typeof ctx.data.email === 'string' && ctx.data.email.includes('@'),
112
+ },
113
+ { id: 'review' },
114
+ ],
115
+ };
116
+ ```
117
+
118
+ ### `syncFormGroup` signature
119
+
120
+ ```typescript
121
+ function syncFormGroup(
122
+ facade: PathFacade,
123
+ formGroup: FormGroupLike,
124
+ destroyRef?: DestroyRef
125
+ ): () => void
126
+ ```
127
+
128
+ | Parameter | Type | Description |
129
+ |-----------|------|-------------|
130
+ | `facade` | `PathFacade` | The facade to write values into. |
131
+ | `formGroup` | `FormGroupLike` | Any Angular `FormGroup` (or any object satisfying the duck interface). |
132
+ | `destroyRef` | `DestroyRef` *(optional)* | Pass `inject(DestroyRef)` to auto-unsubscribe on component destroy. |
133
+ | **returns** | `() => void` | Cleanup function — call manually if not using `DestroyRef`. |
134
+
135
+ #### Behaviour details
136
+
137
+ - **Immediate sync** — current `getRawValue()` is written on the first call, so
138
+ guards evaluate against the real form state from the first snapshot.
139
+ - **Disabled controls included** — uses `getRawValue()` (not `formGroup.value`)
140
+ so disabled controls are always synced.
141
+ - **Safe before `start()`** — if no path is active when a change fires, the call
142
+ is silently ignored (no error).
143
+ - **`FormGroupLike` duck interface** — `@angular/forms` is not a required import;
144
+ any object with `getRawValue()` and `valueChanges` works.
145
+
51
146
  ### Lifecycle
52
147
 
53
148
  `PathFacade` implements `OnDestroy`. When Angular destroys the providing component, `ngOnDestroy` is called automatically, which:
@@ -56,12 +151,28 @@ export class MyComponent {
56
151
 
57
152
  ## Using with signals (Angular 16+)
58
153
 
154
+ `PathFacade` ships a pre-wired `stateSignal` so no manual `toSignal()` call is
155
+ needed:
156
+
59
157
  ```typescript
60
- public readonly snapshot = toSignal(this.facade.state$, { initialValue: null });
158
+ @Component({ providers: [PathFacade] })
159
+ export class MyComponent {
160
+ protected readonly facade = inject(PathFacade);
161
+
162
+ // Use stateSignal directly — no toSignal() required
163
+ protected readonly snapshot = this.facade.stateSignal;
61
164
 
62
- // Derive computed values from the snapshot signal
63
- public readonly isActive = computed(() => this.snapshot() !== null);
64
- public readonly currentStep = computed(() => this.snapshot()?.stepId ?? null);
165
+ // Derive computed values
166
+ public readonly isActive = computed(() => this.snapshot() !== null);
167
+ public readonly currentStep = computed(() => this.snapshot()?.stepId ?? null);
168
+ public readonly canAdvance = computed(() => this.snapshot()?.canMoveNext ?? false);
169
+ }
170
+ ```
171
+
172
+ If you prefer the Observable-based approach, `toSignal()` still works as before:
173
+
174
+ ```typescript
175
+ public readonly snapshot = toSignal(this.facade.state$, { initialValue: null });
65
176
  ```
66
177
 
67
178
  ## Using with the async pipe
@@ -113,18 +224,40 @@ Each `<ng-template pwStep="<stepId>">` is rendered when the active step matches
113
224
 
114
225
  ### Context sharing
115
226
 
116
- `PathShellComponent` provides a `PathFacade` instance at the component level. Step content components can inject it directly without a separate provider:
227
+ `PathShellComponent` provides a `PathFacade` instance in its own `providers` array
228
+ and passes its component-level `Injector` to every step template via
229
+ `ngTemplateOutletInjector`. Step components can therefore resolve the shell's
230
+ `PathFacade` directly via `inject()` — no extra provider setup required:
117
231
 
118
232
  ```typescript
233
+ // Step component — inject(PathFacade) resolves the shell's instance automatically
119
234
  @Component({
120
235
  template: `
121
- <input [value]="snapshot()?.data?.name ?? ''"
236
+ <input [value]="snapshot()?.data?.['name'] ?? ''"
122
237
  (input)="facade.setData('name', $event.target.value)" />
123
238
  `
124
239
  })
125
240
  export class DetailsFormComponent {
126
- protected readonly facade = inject(PathFacade);
127
- protected readonly snapshot = toSignal(this.facade.state$, { initialValue: null });
241
+ protected readonly facade = inject(PathFacade);
242
+ protected readonly snapshot = this.facade.stateSignal;
243
+ }
244
+ ```
245
+
246
+ The parent component hosting `<pw-shell>` does **not** need its own
247
+ `PathFacade` provider. To access the facade from the parent, use `@ViewChild`:
248
+
249
+ ```typescript
250
+ @Component({
251
+ imports: [PathShellComponent, PathStepDirective],
252
+ template: `
253
+ <pw-shell #shell [path]="myPath" (completed)="onDone($event)">
254
+ <ng-template pwStep="details"><app-details-form /></ng-template>
255
+ </pw-shell>
256
+ `
257
+ })
258
+ export class MyComponent {
259
+ @ViewChild('shell', { read: PathShellComponent }) shell!: PathShellComponent;
260
+ protected onDone(data: PathData) { console.log('done', data); }
128
261
  }
129
262
  ```
130
263
 
package/dist/index.css CHANGED
@@ -6,11 +6,11 @@
6
6
  * Every visual value is a CSS custom property (--pw-*) so you can
7
7
  * theme the shell without overriding selectors.
8
8
  *
9
- * Usage:
10
- * import "@daltonr/pathwrite-react/styles.css"; // React
11
- * import "@daltonr/pathwrite-vue/styles.css"; // Vue
12
- * // Angular: add "node_modules/@daltonr/pathwrite-angular/dist/index.css"
13
- * // to the styles array in angular.json
9
+ * Usage (React / Vue):
10
+ * import "adapter-package/styles.css";
11
+ *
12
+ * Usage (Angular) add to the styles array in angular.json:
13
+ * "node_modules/@daltonr/pathwrite-angular/dist/index.css"
14
14
  */
15
15
 
16
16
  /* ------------------------------------------------------------------ */
@@ -170,6 +170,40 @@
170
170
  min-height: 120px;
171
171
  }
172
172
 
173
+ /* ------------------------------------------------------------------ */
174
+ /* Validation messages */
175
+ /* ------------------------------------------------------------------ */
176
+ :root {
177
+ --pw-color-error: #dc2626;
178
+ --pw-color-error-bg: #fef2f2;
179
+ --pw-color-error-border: #fecaca;
180
+ }
181
+
182
+ .pw-shell__validation {
183
+ list-style: none;
184
+ margin: 0;
185
+ padding: 12px 16px;
186
+ background: var(--pw-color-error-bg);
187
+ border: 1px solid var(--pw-color-error-border);
188
+ border-radius: var(--pw-shell-radius);
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: 4px;
192
+ }
193
+
194
+ .pw-shell__validation-item {
195
+ font-size: 13px;
196
+ color: var(--pw-color-error);
197
+ padding-left: 16px;
198
+ position: relative;
199
+ }
200
+
201
+ .pw-shell__validation-item::before {
202
+ content: "•";
203
+ position: absolute;
204
+ left: 4px;
205
+ }
206
+
173
207
  /* ------------------------------------------------------------------ */
174
208
  /* Footer — navigation buttons */
175
209
  /* ------------------------------------------------------------------ */
package/dist/index.d.ts CHANGED
@@ -1,13 +1,31 @@
1
- import { OnDestroy } from "@angular/core";
1
+ import { OnDestroy, DestroyRef, Signal } from "@angular/core";
2
2
  import { Observable } from "rxjs";
3
3
  import { PathData, PathDefinition, PathEvent, PathSnapshot } from "@daltonr/pathwrite-core";
4
- export declare class PathFacade implements OnDestroy {
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * Angular facade over PathEngine. Provide at component level for an isolated
7
+ * instance per component; Angular handles cleanup via ngOnDestroy.
8
+ *
9
+ * The optional generic `TData` narrows `state$`, `stateSignal`, `snapshot()`,
10
+ * and `setData()` to your data shape. It is a **type-level assertion** — no
11
+ * runtime validation is performed. Inject as `PathFacade` (untyped default)
12
+ * then cast:
13
+ *
14
+ * ```typescript
15
+ * const facade = inject(PathFacade) as PathFacade<MyData>;
16
+ * facade.snapshot()?.data.name; // typed as string (or whatever MyData defines)
17
+ * ```
18
+ */
19
+ export declare class PathFacade<TData extends PathData = PathData> implements OnDestroy {
5
20
  private readonly engine;
6
21
  private readonly _state$;
7
22
  private readonly _events$;
8
23
  private readonly unsubscribeFromEngine;
9
- readonly state$: Observable<PathSnapshot | null>;
24
+ private readonly _stateSignal;
25
+ readonly state$: Observable<PathSnapshot<TData> | null>;
10
26
  readonly events$: Observable<PathEvent>;
27
+ /** Signal version of state$. Updates on every path state change. Requires Angular 16+. */
28
+ readonly stateSignal: Signal<PathSnapshot<TData> | null>;
11
29
  constructor();
12
30
  ngOnDestroy(): void;
13
31
  start(path: PathDefinition, initialData?: PathData): Promise<void>;
@@ -15,7 +33,54 @@ export declare class PathFacade implements OnDestroy {
15
33
  next(): Promise<void>;
16
34
  previous(): Promise<void>;
17
35
  cancel(): Promise<void>;
18
- setData(key: string, value: unknown): Promise<void>;
36
+ setData<K extends string & keyof TData>(key: K, value: TData[K]): Promise<void>;
19
37
  goToStep(stepId: string): Promise<void>;
20
- snapshot(): PathSnapshot | null;
38
+ /** Jump to a step by ID, checking the current step's canMoveNext (forward) or
39
+ * canMovePrevious (backward) guard first. Navigation is blocked if the guard
40
+ * returns false. Throws if the step ID does not exist. */
41
+ goToStepChecked(stepId: string): Promise<void>;
42
+ snapshot(): PathSnapshot<TData> | null;
43
+ static ɵfac: i0.ɵɵFactoryDeclaration<PathFacade<any>, never>;
44
+ static ɵprov: i0.ɵɵInjectableDeclaration<PathFacade<any>>;
21
45
  }
46
+ /**
47
+ * Minimal interface describing what syncFormGroup needs from an Angular
48
+ * FormGroup. Typed as a duck interface so that @angular/forms is not a
49
+ * required import — any object with getRawValue() and valueChanges works.
50
+ *
51
+ * Angular's FormGroup satisfies this interface automatically.
52
+ */
53
+ export interface FormGroupLike {
54
+ /** Returns all control values including disabled ones (FormGroup.getRawValue()). */
55
+ getRawValue(): Record<string, unknown>;
56
+ /** Observable that emits whenever any control value changes. */
57
+ readonly valueChanges: Observable<unknown>;
58
+ }
59
+ /**
60
+ * Syncs every key of an Angular FormGroup to the path engine via setData.
61
+ *
62
+ * - Immediately writes getRawValue() to the facade so canMoveNext guards
63
+ * evaluate against the current form state on the very first snapshot.
64
+ * - Subscribes to valueChanges and re-applies getRawValue() on every emission
65
+ * so disabled controls are always included.
66
+ * - Guards against calling setData when no path is active, so it is safe to
67
+ * call syncFormGroup before or after facade.start().
68
+ * - Returns a cleanup function that unsubscribes from the observable.
69
+ * Pass a DestroyRef to wire cleanup automatically to the component lifecycle.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * export class MyStepComponent implements OnInit {
74
+ * private readonly facade = inject(PathFacade) as PathFacade<MyData>;
75
+ * protected readonly form = new FormGroup({
76
+ * name: new FormControl('', Validators.required),
77
+ * email: new FormControl(''),
78
+ * });
79
+ *
80
+ * ngOnInit() {
81
+ * syncFormGroup(this.facade, this.form, inject(DestroyRef));
82
+ * }
83
+ * }
84
+ * ```
85
+ */
86
+ export declare function syncFormGroup<TData extends PathData = PathData>(facade: PathFacade<TData>, formGroup: FormGroupLike, destroyRef?: DestroyRef): () => void;
package/dist/index.js CHANGED
@@ -1,102 +1,125 @@
1
- var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
- function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
- var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
- var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
- var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
- var _, done = false;
7
- for (var i = decorators.length - 1; i >= 0; i--) {
8
- var context = {};
9
- for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
- for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
- context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
- var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
- if (kind === "accessor") {
14
- if (result === void 0) continue;
15
- if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
- if (_ = accept(result.get)) descriptor.get = _;
17
- if (_ = accept(result.set)) descriptor.set = _;
18
- if (_ = accept(result.init)) initializers.unshift(_);
19
- }
20
- else if (_ = accept(result)) {
21
- if (kind === "field") initializers.unshift(_);
22
- else descriptor[key] = _;
23
- }
24
- }
25
- if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
- done = true;
27
- };
28
- var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
- var useValue = arguments.length > 2;
30
- for (var i = 0; i < initializers.length; i++) {
31
- value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
- }
33
- return useValue ? value : void 0;
34
- };
35
- import { Injectable } from "@angular/core";
1
+ import { Injectable, signal } from "@angular/core";
36
2
  import { BehaviorSubject, Subject } from "rxjs";
37
3
  import { PathEngine } from "@daltonr/pathwrite-core";
38
- let PathFacade = (() => {
39
- let _classDecorators = [Injectable()];
40
- let _classDescriptor;
41
- let _classExtraInitializers = [];
42
- let _classThis;
43
- var PathFacade = class {
44
- static { _classThis = this; }
45
- static {
46
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
47
- __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
48
- PathFacade = _classThis = _classDescriptor.value;
49
- if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
50
- __runInitializers(_classThis, _classExtraInitializers);
51
- }
52
- engine = new PathEngine();
53
- _state$ = new BehaviorSubject(null);
54
- _events$ = new Subject();
55
- unsubscribeFromEngine;
56
- state$ = this._state$.asObservable();
57
- events$ = this._events$.asObservable();
58
- constructor() {
59
- this.unsubscribeFromEngine = this.engine.subscribe((event) => {
60
- this._events$.next(event);
61
- if (event.type === "stateChanged" || event.type === "resumed") {
62
- this._state$.next(event.snapshot);
63
- }
64
- else if (event.type === "completed" || event.type === "cancelled") {
65
- this._state$.next(null);
66
- }
67
- });
68
- }
69
- ngOnDestroy() {
70
- this.unsubscribeFromEngine();
71
- this._events$.complete();
72
- this._state$.complete();
73
- }
74
- start(path, initialData = {}) {
75
- return this.engine.start(path, initialData);
76
- }
77
- startSubPath(path, initialData = {}) {
78
- return this.engine.startSubPath(path, initialData);
79
- }
80
- next() {
81
- return this.engine.next();
82
- }
83
- previous() {
84
- return this.engine.previous();
85
- }
86
- cancel() {
87
- return this.engine.cancel();
88
- }
89
- setData(key, value) {
90
- return this.engine.setData(key, value);
91
- }
92
- goToStep(stepId) {
93
- return this.engine.goToStep(stepId);
94
- }
95
- snapshot() {
96
- return this._state$.getValue();
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * Angular facade over PathEngine. Provide at component level for an isolated
7
+ * instance per component; Angular handles cleanup via ngOnDestroy.
8
+ *
9
+ * The optional generic `TData` narrows `state$`, `stateSignal`, `snapshot()`,
10
+ * and `setData()` to your data shape. It is a **type-level assertion** — no
11
+ * runtime validation is performed. Inject as `PathFacade` (untyped default)
12
+ * then cast:
13
+ *
14
+ * ```typescript
15
+ * const facade = inject(PathFacade) as PathFacade<MyData>;
16
+ * facade.snapshot()?.data.name; // typed as string (or whatever MyData defines)
17
+ * ```
18
+ */
19
+ export class PathFacade {
20
+ constructor() {
21
+ this.engine = new PathEngine();
22
+ this._state$ = new BehaviorSubject(null);
23
+ this._events$ = new Subject();
24
+ this._stateSignal = signal(null);
25
+ this.state$ = this._state$.asObservable();
26
+ this.events$ = this._events$.asObservable();
27
+ /** Signal version of state$. Updates on every path state change. Requires Angular 16+. */
28
+ this.stateSignal = this._stateSignal.asReadonly();
29
+ this.unsubscribeFromEngine = this.engine.subscribe((event) => {
30
+ this._events$.next(event);
31
+ if (event.type === "stateChanged" || event.type === "resumed") {
32
+ this._state$.next(event.snapshot);
33
+ this._stateSignal.set(event.snapshot);
34
+ }
35
+ else if (event.type === "completed" || event.type === "cancelled") {
36
+ this._state$.next(null);
37
+ this._stateSignal.set(null);
38
+ }
39
+ });
40
+ }
41
+ ngOnDestroy() {
42
+ this.unsubscribeFromEngine();
43
+ this._events$.complete();
44
+ this._state$.complete();
45
+ }
46
+ start(path, initialData = {}) {
47
+ return this.engine.start(path, initialData);
48
+ }
49
+ startSubPath(path, initialData = {}) {
50
+ return this.engine.startSubPath(path, initialData);
51
+ }
52
+ next() {
53
+ return this.engine.next();
54
+ }
55
+ previous() {
56
+ return this.engine.previous();
57
+ }
58
+ cancel() {
59
+ return this.engine.cancel();
60
+ }
61
+ setData(key, value) {
62
+ return this.engine.setData(key, value);
63
+ }
64
+ goToStep(stepId) {
65
+ return this.engine.goToStep(stepId);
66
+ }
67
+ /** Jump to a step by ID, checking the current step's canMoveNext (forward) or
68
+ * canMovePrevious (backward) guard first. Navigation is blocked if the guard
69
+ * returns false. Throws if the step ID does not exist. */
70
+ goToStepChecked(stepId) {
71
+ return this.engine.goToStepChecked(stepId);
72
+ }
73
+ snapshot() {
74
+ return this._state$.getValue();
75
+ }
76
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
77
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathFacade }); }
78
+ }
79
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathFacade, decorators: [{
80
+ type: Injectable
81
+ }], ctorParameters: () => [] });
82
+ /**
83
+ * Syncs every key of an Angular FormGroup to the path engine via setData.
84
+ *
85
+ * - Immediately writes getRawValue() to the facade so canMoveNext guards
86
+ * evaluate against the current form state on the very first snapshot.
87
+ * - Subscribes to valueChanges and re-applies getRawValue() on every emission
88
+ * so disabled controls are always included.
89
+ * - Guards against calling setData when no path is active, so it is safe to
90
+ * call syncFormGroup before or after facade.start().
91
+ * - Returns a cleanup function that unsubscribes from the observable.
92
+ * Pass a DestroyRef to wire cleanup automatically to the component lifecycle.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * export class MyStepComponent implements OnInit {
97
+ * private readonly facade = inject(PathFacade) as PathFacade<MyData>;
98
+ * protected readonly form = new FormGroup({
99
+ * name: new FormControl('', Validators.required),
100
+ * email: new FormControl(''),
101
+ * });
102
+ *
103
+ * ngOnInit() {
104
+ * syncFormGroup(this.facade, this.form, inject(DestroyRef));
105
+ * }
106
+ * }
107
+ * ```
108
+ */
109
+ export function syncFormGroup(facade, formGroup, destroyRef) {
110
+ const baseFacade = facade;
111
+ function applyValues() {
112
+ if (baseFacade.snapshot() === null)
113
+ return; // no active path — nothing to sync
114
+ for (const [key, value] of Object.entries(formGroup.getRawValue())) {
115
+ void baseFacade.setData(key, value);
97
116
  }
98
- };
99
- return PathFacade = _classThis;
100
- })();
101
- export { PathFacade };
117
+ }
118
+ // Write current form values immediately so guards see the initial state.
119
+ applyValues();
120
+ const subscription = formGroup.valueChanges.subscribe(() => applyValues());
121
+ const cleanup = () => subscription.unsubscribe();
122
+ destroyRef?.onDestroy(cleanup);
123
+ return cleanup;
124
+ }
102
125
  //# 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,eAAe,EAAc,OAAO,EAAE,MAAM,MAAM,CAAC;AAC5D,OAAO,EAGL,UAAU,EAGX,MAAM,yBAAyB,CAAC;IAGpB,UAAU;4BADtB,UAAU,EAAE;;;;;;;;YACb,6KAyDC;;;YAzDY,uDAAU;;QACJ,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC1B,OAAO,GAAG,IAAI,eAAe,CAAsB,IAAI,CAAC,CAAC;QACzD,QAAQ,GAAG,IAAI,OAAO,EAAa,CAAC;QACpC,qBAAqB,CAAa;QAEnC,MAAM,GAAoC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;QACtE,OAAO,GAA0B,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,CAAC;QAE9E;YACE,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;gBAC3D,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC1B,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;oBAC9D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBACpC,CAAC;qBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;oBACpE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAEM,WAAW;YAChB,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC7B,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACzB,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QAC1B,CAAC;QAEM,KAAK,CAAC,IAAoB,EAAE,cAAwB,EAAE;YAC3D,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QAC9C,CAAC;QAEM,YAAY,CAAC,IAAoB,EAAE,cAAwB,EAAE;YAClE,OAAO,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QACrD,CAAC;QAEM,IAAI;YACT,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAC5B,CAAC;QAEM,QAAQ;YACb,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QAChC,CAAC;QAEM,MAAM;YACX,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QAC9B,CAAC;QAEM,OAAO,CAAC,GAAW,EAAE,KAAc;YACxC,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACzC,CAAC;QAEM,QAAQ,CAAC,MAAc;YAC5B,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACtC,CAAC;QAEM,QAAQ;YACb,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACjC,CAAC;;;;SAxDU,UAAU"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/shell.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { TemplateRef, EventEmitter, QueryList, OnInit, OnDestroy } from "@angular/core";
1
+ import { TemplateRef, EventEmitter, QueryList, OnInit, OnDestroy, Injector } from "@angular/core";
2
2
  import { PathData, PathDefinition, PathEvent } from "@daltonr/pathwrite-core";
3
3
  import { PathFacade } from "./index";
4
+ import * as i0 from "@angular/core";
4
5
  /**
5
6
  * Structural directive that associates a template with a step ID.
6
7
  * Used inside `<pw-shell>` to define per-step content.
@@ -16,6 +17,8 @@ export declare class PathStepDirective {
16
17
  readonly templateRef: TemplateRef<unknown>;
17
18
  stepId: string;
18
19
  constructor(templateRef: TemplateRef<unknown>);
20
+ static ɵfac: i0.ɵɵFactoryDeclaration<PathStepDirective, never>;
21
+ static ɵdir: i0.ɵɵDirectiveDeclaration<PathStepDirective, "[pwStep]", never, { "stepId": { "alias": "pwStep"; "required": true; }; }, {}, never, never, true, never>;
19
22
  }
20
23
  /**
21
24
  * Default UI shell component. Renders a progress indicator, step content,
@@ -29,23 +32,37 @@ export declare class PathStepDirective {
29
32
  * ```
30
33
  */
31
34
  export declare class PathShellComponent implements OnInit, OnDestroy {
35
+ /** The path definition to run. Required. */
32
36
  path: PathDefinition;
37
+ /** Initial data merged into the path engine on start. */
33
38
  initialData: PathData;
39
+ /** Start the path automatically on ngOnInit. Set to false to call doStart() manually. */
34
40
  autoStart: boolean;
41
+ /** Label for the Back navigation button. */
35
42
  backLabel: string;
43
+ /** Label for the Next navigation button. */
36
44
  nextLabel: string;
45
+ /** Label for the Next button when on the last step. */
37
46
  finishLabel: string;
47
+ /** Label for the Cancel button. */
38
48
  cancelLabel: string;
49
+ /** Hide the Cancel button entirely. */
39
50
  hideCancel: boolean;
51
+ /** Hide the step progress indicator in the header. */
40
52
  hideProgress: boolean;
41
53
  completed: EventEmitter<PathData>;
42
54
  cancelled: EventEmitter<PathData>;
43
55
  pathEvent: EventEmitter<PathEvent>;
44
56
  stepDirectives: QueryList<PathStepDirective>;
45
- readonly facade: PathFacade;
57
+ readonly facade: PathFacade<PathData>;
58
+ /** The shell's own component-level injector. Passed to ngTemplateOutlet so that
59
+ * step components can resolve PathFacade (provided by this shell) via inject(). */
60
+ protected readonly shellInjector: Injector;
46
61
  started: boolean;
47
62
  private readonly destroy$;
48
63
  ngOnInit(): void;
49
64
  ngOnDestroy(): void;
50
65
  doStart(): void;
66
+ static ɵfac: i0.ɵɵFactoryDeclaration<PathShellComponent, never>;
67
+ static ɵcmp: i0.ɵɵComponentDeclaration<PathShellComponent, "pw-shell", never, { "path": { "alias": "path"; "required": true; }; "initialData": { "alias": "initialData"; "required": false; }; "autoStart": { "alias": "autoStart"; "required": false; }; "backLabel": { "alias": "backLabel"; "required": false; }; "nextLabel": { "alias": "nextLabel"; "required": false; }; "finishLabel": { "alias": "finishLabel"; "required": false; }; "cancelLabel": { "alias": "cancelLabel"; "required": false; }; "hideCancel": { "alias": "hideCancel"; "required": false; }; "hideProgress": { "alias": "hideProgress"; "required": false; }; }, { "completed": "completed"; "cancelled": "cancelled"; "pathEvent": "pathEvent"; }, ["stepDirectives"], never, true, never>;
51
68
  }