@daltonr/pathwrite-angular 0.1.4 → 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 +145 -12
- package/dist/index.css +39 -5
- package/dist/index.d.ts +70 -5
- package/dist/index.js +121 -98
- package/dist/index.js.map +1 -1
- package/dist/shell.d.ts +19 -2
- package/dist/shell.js +183 -191
- package/dist/shell.js.map +1 -1
- package/package.json +14 -3
- package/src/index.ts +164 -0
- package/src/shell.ts +197 -0
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
127
|
-
protected readonly snapshot =
|
|
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 "
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
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
|
|
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:
|
|
36
|
+
setData<K extends string & keyof TData>(key: K, value: TData[K]): Promise<void>;
|
|
19
37
|
goToStep(stepId: string): Promise<void>;
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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":"
|
|
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
|
}
|