@daltonr/pathwrite-angular 0.5.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 +188 -1
- package/dist/index.css +9 -0
- package/dist/index.d.ts +68 -1
- package/dist/index.js +56 -1
- package/dist/index.js.map +1 -1
- package/dist/shell.d.ts +26 -2
- package/dist/shell.js +78 -16
- package/dist/shell.js.map +1 -1
- package/package.json +4 -3
- package/src/index.ts +97 -1
- package/src/shell.ts +59 -8
package/README.md
CHANGED
|
@@ -87,6 +87,128 @@ facade.snapshot()?.data.name; // typed as string (or whatever MyData defines)
|
|
|
87
87
|
|
|
88
88
|
---
|
|
89
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
|
+
|
|
90
212
|
## Angular Forms integration — `syncFormGroup`
|
|
91
213
|
|
|
92
214
|
`syncFormGroup` eliminates the boilerplate of manually wiring an Angular
|
|
@@ -255,6 +377,38 @@ export class MyComponent {
|
|
|
255
377
|
|
|
256
378
|
Each `<ng-template pwStep="<stepId>">` is rendered when the active step matches `stepId`. The shell handles all navigation internally.
|
|
257
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
|
+
|
|
258
412
|
### Context sharing
|
|
259
413
|
|
|
260
414
|
`PathShellComponent` provides a `PathFacade` instance in its own `providers` array
|
|
@@ -306,7 +460,8 @@ export class MyComponent {
|
|
|
306
460
|
| `completeLabel` | `string` | `"Complete"` | Complete button label (last step). |
|
|
307
461
|
| `cancelLabel` | `string` | `"Cancel"` | Cancel button label. |
|
|
308
462
|
| `hideCancel` | `boolean` | `false` | Hide the Cancel button. |
|
|
309
|
-
| `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. |
|
|
310
465
|
|
|
311
466
|
### Outputs
|
|
312
467
|
|
|
@@ -364,6 +519,38 @@ export class MyComponent { ... }
|
|
|
364
519
|
|
|
365
520
|
Both directives can be combined. Only the sections you override are replaced — a custom header still shows the default footer, and vice versa.
|
|
366
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
|
+
|
|
367
554
|
---
|
|
368
555
|
|
|
369
556
|
## Sub-Paths
|
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
|
@@ -65,6 +65,73 @@ export declare class PathFacade<TData extends PathData = PathData> implements On
|
|
|
65
65
|
static ɵfac: i0.ɵɵFactoryDeclaration<PathFacade<any>, never>;
|
|
66
66
|
static ɵprov: i0.ɵɵInjectableDeclaration<PathFacade<any>>;
|
|
67
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>;
|
|
68
135
|
/**
|
|
69
136
|
* Minimal interface describing what syncFormGroup needs from an Angular
|
|
70
137
|
* FormGroup. Typed as a duck interface so that @angular/forms is not a
|
|
@@ -106,5 +173,5 @@ export interface FormGroupLike {
|
|
|
106
173
|
* ```
|
|
107
174
|
*/
|
|
108
175
|
export declare function syncFormGroup<TData extends PathData = PathData>(facade: PathFacade<TData>, formGroup: FormGroupLike, destroyRef?: DestroyRef): () => void;
|
|
109
|
-
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";
|
|
110
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";
|
|
@@ -116,6 +116,61 @@ export class PathFacade {
|
|
|
116
116
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathFacade, decorators: [{
|
|
117
117
|
type: Injectable
|
|
118
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
|
+
}
|
|
119
174
|
/**
|
|
120
175
|
* Syncs every key of an Angular FormGroup to the path engine via setData.
|
|
121
176
|
*
|
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,EAAyB,MAAM,EAAU,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/shell.d.ts
CHANGED
|
@@ -113,8 +113,15 @@ export declare class PathShellComponent implements OnInit, OnDestroy {
|
|
|
113
113
|
cancelLabel: string;
|
|
114
114
|
/** Hide the Cancel button entirely. */
|
|
115
115
|
hideCancel: boolean;
|
|
116
|
-
/** Hide the step progress indicator in the header. */
|
|
116
|
+
/** Hide the step progress indicator in the header. Also hidden automatically when the path has only one step. */
|
|
117
117
|
hideProgress: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Footer layout mode:
|
|
120
|
+
* - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
|
|
121
|
+
* - "wizard": Back button on left, Cancel and Submit together on right.
|
|
122
|
+
* - "form": Cancel on left, Submit alone on right. Back button never shown.
|
|
123
|
+
*/
|
|
124
|
+
footerLayout: "wizard" | "form" | "auto";
|
|
118
125
|
completed: EventEmitter<PathData>;
|
|
119
126
|
cancelled: EventEmitter<PathData>;
|
|
120
127
|
pathEvent: EventEmitter<PathEvent>;
|
|
@@ -132,6 +139,23 @@ export declare class PathShellComponent implements OnInit, OnDestroy {
|
|
|
132
139
|
ngOnInit(): void;
|
|
133
140
|
ngOnDestroy(): void;
|
|
134
141
|
doStart(): void;
|
|
142
|
+
/**
|
|
143
|
+
* Restart the active path from step 1 with the original `initialData`,
|
|
144
|
+
* without unmounting the shell. Call this via a `#shell` template reference:
|
|
145
|
+
*
|
|
146
|
+
* ```html
|
|
147
|
+
* <pw-shell #shell [path]="myPath" ...></pw-shell>
|
|
148
|
+
* <button (click)="shell.restart()">Try Again</button>
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
restart(): Promise<void>;
|
|
152
|
+
/** Returns Object.entries(s.fieldMessages) for use in *ngFor. */
|
|
153
|
+
protected fieldEntries(s: PathSnapshot): [string, string][];
|
|
154
|
+
/** Resolves "auto" footerLayout based on snapshot. Single-step top-level → "form", otherwise → "wizard". */
|
|
155
|
+
protected getResolvedFooterLayout(s: PathSnapshot): "wizard" | "form";
|
|
156
|
+
/** Converts a camelCase or lowercase field key to a display label.
|
|
157
|
+
* e.g. "firstName" → "First Name", "email" → "Email" */
|
|
158
|
+
protected formatFieldKey(key: string): string;
|
|
135
159
|
static ɵfac: i0.ɵɵFactoryDeclaration<PathShellComponent, never>;
|
|
136
|
-
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; }; "completeLabel": { "alias": "completeLabel"; "required": false; }; "cancelLabel": { "alias": "cancelLabel"; "required": false; }; "hideCancel": { "alias": "hideCancel"; "required": false; }; "hideProgress": { "alias": "hideProgress"; "required": false; }; }, { "completed": "completed"; "cancelled": "cancelled"; "pathEvent": "pathEvent"; }, ["customHeader", "customFooter", "stepDirectives"], never, true, never>;
|
|
160
|
+
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; }; "completeLabel": { "alias": "completeLabel"; "required": false; }; "cancelLabel": { "alias": "cancelLabel"; "required": false; }; "hideCancel": { "alias": "hideCancel"; "required": false; }; "hideProgress": { "alias": "hideProgress"; "required": false; }; "footerLayout": { "alias": "footerLayout"; "required": false; }; }, { "completed": "completed"; "cancelled": "cancelled"; "pathEvent": "pathEvent"; }, ["customHeader", "customFooter", "stepDirectives"], never, true, never>;
|
|
137
161
|
}
|
package/dist/shell.js
CHANGED
|
@@ -119,8 +119,15 @@ export class PathShellComponent {
|
|
|
119
119
|
this.cancelLabel = "Cancel";
|
|
120
120
|
/** Hide the Cancel button entirely. */
|
|
121
121
|
this.hideCancel = false;
|
|
122
|
-
/** Hide the step progress indicator in the header. */
|
|
122
|
+
/** Hide the step progress indicator in the header. Also hidden automatically when the path has only one step. */
|
|
123
123
|
this.hideProgress = false;
|
|
124
|
+
/**
|
|
125
|
+
* Footer layout mode:
|
|
126
|
+
* - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
|
|
127
|
+
* - "wizard": Back button on left, Cancel and Submit together on right.
|
|
128
|
+
* - "form": Cancel on left, Submit alone on right. Back button never shown.
|
|
129
|
+
*/
|
|
130
|
+
this.footerLayout = "auto";
|
|
124
131
|
this.completed = new EventEmitter();
|
|
125
132
|
this.cancelled = new EventEmitter();
|
|
126
133
|
this.pathEvent = new EventEmitter();
|
|
@@ -161,8 +168,35 @@ export class PathShellComponent {
|
|
|
161
168
|
this.started = true;
|
|
162
169
|
this.facade.start(this.path, this.initialData);
|
|
163
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Restart the active path from step 1 with the original `initialData`,
|
|
173
|
+
* without unmounting the shell. Call this via a `#shell` template reference:
|
|
174
|
+
*
|
|
175
|
+
* ```html
|
|
176
|
+
* <pw-shell #shell [path]="myPath" ...></pw-shell>
|
|
177
|
+
* <button (click)="shell.restart()">Try Again</button>
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
restart() {
|
|
181
|
+
return this.facade.restart(this.path, this.initialData);
|
|
182
|
+
}
|
|
183
|
+
/** Returns Object.entries(s.fieldMessages) for use in *ngFor. */
|
|
184
|
+
fieldEntries(s) {
|
|
185
|
+
return Object.entries(s.fieldMessages);
|
|
186
|
+
}
|
|
187
|
+
/** Resolves "auto" footerLayout based on snapshot. Single-step top-level → "form", otherwise → "wizard". */
|
|
188
|
+
getResolvedFooterLayout(s) {
|
|
189
|
+
return this.footerLayout === "auto"
|
|
190
|
+
? (s.stepCount === 1 && s.nestingLevel === 0 ? "form" : "wizard")
|
|
191
|
+
: this.footerLayout;
|
|
192
|
+
}
|
|
193
|
+
/** Converts a camelCase or lowercase field key to a display label.
|
|
194
|
+
* e.g. "firstName" → "First Name", "email" → "Email" */
|
|
195
|
+
formatFieldKey(key) {
|
|
196
|
+
return key.replace(/([A-Z])/g, " $1").replace(/^./, c => c.toUpperCase()).trim();
|
|
197
|
+
}
|
|
164
198
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
165
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: PathShellComponent, isStandalone: true, selector: "pw-shell", inputs: { path: "path", initialData: "initialData", autoStart: "autoStart", backLabel: "backLabel", nextLabel: "nextLabel", completeLabel: "completeLabel", cancelLabel: "cancelLabel", hideCancel: "hideCancel", hideProgress: "hideProgress" }, outputs: { completed: "completed", cancelled: "cancelled", pathEvent: "pathEvent" }, providers: [PathFacade], queries: [{ propertyName: "customHeader", first: true, predicate: PathShellHeaderDirective, descendants: true }, { propertyName: "customFooter", first: true, predicate: PathShellFooterDirective, descendants: true }, { propertyName: "stepDirectives", predicate: PathStepDirective }], ngImport: i0, template: `
|
|
199
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: PathShellComponent, isStandalone: true, selector: "pw-shell", inputs: { path: "path", initialData: "initialData", autoStart: "autoStart", backLabel: "backLabel", nextLabel: "nextLabel", completeLabel: "completeLabel", cancelLabel: "cancelLabel", hideCancel: "hideCancel", hideProgress: "hideProgress", footerLayout: "footerLayout" }, outputs: { completed: "completed", cancelled: "cancelled", pathEvent: "pathEvent" }, providers: [PathFacade], queries: [{ propertyName: "customHeader", first: true, predicate: PathShellHeaderDirective, descendants: true }, { propertyName: "customFooter", first: true, predicate: PathShellFooterDirective, descendants: true }, { propertyName: "stepDirectives", predicate: PathStepDirective }], ngImport: i0, template: `
|
|
166
200
|
<!-- Empty state -->
|
|
167
201
|
<div class="pw-shell" *ngIf="!(facade.state$ | async)">
|
|
168
202
|
<div class="pw-shell__empty" *ngIf="!started">
|
|
@@ -178,7 +212,7 @@ export class PathShellComponent {
|
|
|
178
212
|
<ng-container *ngTemplateOutlet="customHeader.templateRef; context: { $implicit: s }"></ng-container>
|
|
179
213
|
</ng-container>
|
|
180
214
|
<ng-template #defaultHeader>
|
|
181
|
-
<div class="pw-shell__header" *ngIf="!hideProgress">
|
|
215
|
+
<div class="pw-shell__header" *ngIf="!hideProgress && (s.stepCount > 1 || s.nestingLevel > 0)">
|
|
182
216
|
<div class="pw-shell__steps">
|
|
183
217
|
<div
|
|
184
218
|
*ngFor="let step of s.steps; let i = index"
|
|
@@ -204,9 +238,11 @@ export class PathShellComponent {
|
|
|
204
238
|
</ng-container>
|
|
205
239
|
</div>
|
|
206
240
|
|
|
207
|
-
<!-- Validation messages -->
|
|
208
|
-
<ul class="pw-shell__validation" *ngIf="s.
|
|
209
|
-
<li *ngFor="let
|
|
241
|
+
<!-- Validation messages — labeled by field name -->
|
|
242
|
+
<ul class="pw-shell__validation" *ngIf="s.hasAttemptedNext && fieldEntries(s).length > 0">
|
|
243
|
+
<li *ngFor="let entry of fieldEntries(s)" class="pw-shell__validation-item">
|
|
244
|
+
<span *ngIf="entry[0] !== '_'" class="pw-shell__validation-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
|
|
245
|
+
</li>
|
|
210
246
|
</ul>
|
|
211
247
|
|
|
212
248
|
<!-- Footer — custom or default navigation buttons -->
|
|
@@ -216,8 +252,17 @@ export class PathShellComponent {
|
|
|
216
252
|
<ng-template #defaultFooter>
|
|
217
253
|
<div class="pw-shell__footer">
|
|
218
254
|
<div class="pw-shell__footer-left">
|
|
255
|
+
<!-- Form mode: Cancel on the left -->
|
|
256
|
+
<button
|
|
257
|
+
*ngIf="getResolvedFooterLayout(s) === 'form' && !hideCancel"
|
|
258
|
+
type="button"
|
|
259
|
+
class="pw-shell__btn pw-shell__btn--cancel"
|
|
260
|
+
[disabled]="s.isNavigating"
|
|
261
|
+
(click)="facade.cancel()"
|
|
262
|
+
>{{ cancelLabel }}</button>
|
|
263
|
+
<!-- Wizard mode: Back on the left -->
|
|
219
264
|
<button
|
|
220
|
-
*ngIf="!s.isFirstStep"
|
|
265
|
+
*ngIf="getResolvedFooterLayout(s) === 'wizard' && !s.isFirstStep"
|
|
221
266
|
type="button"
|
|
222
267
|
class="pw-shell__btn pw-shell__btn--back"
|
|
223
268
|
[disabled]="s.isNavigating || !s.canMovePrevious"
|
|
@@ -225,17 +270,19 @@ export class PathShellComponent {
|
|
|
225
270
|
>{{ backLabel }}</button>
|
|
226
271
|
</div>
|
|
227
272
|
<div class="pw-shell__footer-right">
|
|
273
|
+
<!-- Wizard mode: Cancel on the right -->
|
|
228
274
|
<button
|
|
229
|
-
*ngIf="!hideCancel"
|
|
275
|
+
*ngIf="getResolvedFooterLayout(s) === 'wizard' && !hideCancel"
|
|
230
276
|
type="button"
|
|
231
277
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
232
278
|
[disabled]="s.isNavigating"
|
|
233
279
|
(click)="facade.cancel()"
|
|
234
280
|
>{{ cancelLabel }}</button>
|
|
281
|
+
<!-- Both modes: Submit on the right -->
|
|
235
282
|
<button
|
|
236
283
|
type="button"
|
|
237
284
|
class="pw-shell__btn pw-shell__btn--next"
|
|
238
|
-
[disabled]="s.isNavigating
|
|
285
|
+
[disabled]="s.isNavigating"
|
|
239
286
|
(click)="facade.next()"
|
|
240
287
|
>{{ s.isLastStep ? completeLabel : nextLabel }}</button>
|
|
241
288
|
</div>
|
|
@@ -268,7 +315,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
268
315
|
<ng-container *ngTemplateOutlet="customHeader.templateRef; context: { $implicit: s }"></ng-container>
|
|
269
316
|
</ng-container>
|
|
270
317
|
<ng-template #defaultHeader>
|
|
271
|
-
<div class="pw-shell__header" *ngIf="!hideProgress">
|
|
318
|
+
<div class="pw-shell__header" *ngIf="!hideProgress && (s.stepCount > 1 || s.nestingLevel > 0)">
|
|
272
319
|
<div class="pw-shell__steps">
|
|
273
320
|
<div
|
|
274
321
|
*ngFor="let step of s.steps; let i = index"
|
|
@@ -294,9 +341,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
294
341
|
</ng-container>
|
|
295
342
|
</div>
|
|
296
343
|
|
|
297
|
-
<!-- Validation messages -->
|
|
298
|
-
<ul class="pw-shell__validation" *ngIf="s.
|
|
299
|
-
<li *ngFor="let
|
|
344
|
+
<!-- Validation messages — labeled by field name -->
|
|
345
|
+
<ul class="pw-shell__validation" *ngIf="s.hasAttemptedNext && fieldEntries(s).length > 0">
|
|
346
|
+
<li *ngFor="let entry of fieldEntries(s)" class="pw-shell__validation-item">
|
|
347
|
+
<span *ngIf="entry[0] !== '_'" class="pw-shell__validation-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
|
|
348
|
+
</li>
|
|
300
349
|
</ul>
|
|
301
350
|
|
|
302
351
|
<!-- Footer — custom or default navigation buttons -->
|
|
@@ -306,8 +355,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
306
355
|
<ng-template #defaultFooter>
|
|
307
356
|
<div class="pw-shell__footer">
|
|
308
357
|
<div class="pw-shell__footer-left">
|
|
358
|
+
<!-- Form mode: Cancel on the left -->
|
|
309
359
|
<button
|
|
310
|
-
*ngIf="!
|
|
360
|
+
*ngIf="getResolvedFooterLayout(s) === 'form' && !hideCancel"
|
|
361
|
+
type="button"
|
|
362
|
+
class="pw-shell__btn pw-shell__btn--cancel"
|
|
363
|
+
[disabled]="s.isNavigating"
|
|
364
|
+
(click)="facade.cancel()"
|
|
365
|
+
>{{ cancelLabel }}</button>
|
|
366
|
+
<!-- Wizard mode: Back on the left -->
|
|
367
|
+
<button
|
|
368
|
+
*ngIf="getResolvedFooterLayout(s) === 'wizard' && !s.isFirstStep"
|
|
311
369
|
type="button"
|
|
312
370
|
class="pw-shell__btn pw-shell__btn--back"
|
|
313
371
|
[disabled]="s.isNavigating || !s.canMovePrevious"
|
|
@@ -315,17 +373,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
315
373
|
>{{ backLabel }}</button>
|
|
316
374
|
</div>
|
|
317
375
|
<div class="pw-shell__footer-right">
|
|
376
|
+
<!-- Wizard mode: Cancel on the right -->
|
|
318
377
|
<button
|
|
319
|
-
*ngIf="!hideCancel"
|
|
378
|
+
*ngIf="getResolvedFooterLayout(s) === 'wizard' && !hideCancel"
|
|
320
379
|
type="button"
|
|
321
380
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
322
381
|
[disabled]="s.isNavigating"
|
|
323
382
|
(click)="facade.cancel()"
|
|
324
383
|
>{{ cancelLabel }}</button>
|
|
384
|
+
<!-- Both modes: Submit on the right -->
|
|
325
385
|
<button
|
|
326
386
|
type="button"
|
|
327
387
|
class="pw-shell__btn pw-shell__btn--next"
|
|
328
|
-
[disabled]="s.isNavigating
|
|
388
|
+
[disabled]="s.isNavigating"
|
|
329
389
|
(click)="facade.next()"
|
|
330
390
|
>{{ s.isLastStep ? completeLabel : nextLabel }}</button>
|
|
331
391
|
</div>
|
|
@@ -353,6 +413,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
353
413
|
type: Input
|
|
354
414
|
}], hideProgress: [{
|
|
355
415
|
type: Input
|
|
416
|
+
}], footerLayout: [{
|
|
417
|
+
type: Input
|
|
356
418
|
}], completed: [{
|
|
357
419
|
type: Output
|
|
358
420
|
}], cancelled: [{
|
package/dist/shell.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,SAAS,EAET,KAAK,EACL,MAAM,EACN,YAAY,EACZ,YAAY,EACZ,eAAe,EAIf,MAAM,EACN,QAAQ,EACR,uBAAuB,EACxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAO3C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;;;AAsBrC,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E;;;;;;;;;;GAUG;AAEH,MAAM,OAAO,iBAAiB;IAE5B,YAAmC,WAAiC;QAAjC,gBAAW,GAAX,WAAW,CAAsB;IAAG,CAAC;+GAF7D,iBAAiB;mGAAjB,iBAAiB;;4FAAjB,iBAAiB;kBAD7B,SAAS;mBAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE;gFAEP,MAAM;sBAAjD,KAAK;uBAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE;;AAI5C,8EAA8E;AAC9E,2BAA2B;AAC3B,8EAA8E;AAE9E;;;;;;;;;;;;GAYG;AAEH,MAAM,OAAO,wBAAwB;IACnC,YACkB,WAAqD;QAArD,gBAAW,GAAX,WAAW,CAA0C;IACpE,CAAC;+GAHO,wBAAwB;mGAAxB,wBAAwB;;4FAAxB,wBAAwB;kBADpC,SAAS;mBAAC,EAAE,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,IAAI,EAAE;;AAO5D,8EAA8E;AAC9E,2BAA2B;AAC3B,8EAA8E;AAE9E;;;;;;;;;;;;;;GAcG;AAEH,MAAM,OAAO,wBAAwB;IACnC,YACkB,WAAgF;QAAhF,gBAAW,GAAX,WAAW,CAAqE;IAC/F,CAAC;+GAHO,wBAAwB;mGAAxB,wBAAwB;;4FAAxB,wBAAwB;kBADpC,SAAS;mBAAC,EAAE,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,IAAI,EAAE;;AAO5D,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daltonr/pathwrite-angular",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Angular adapter for @daltonr/pathwrite-core — RxJS observables, signal-friendly, with optional <pw-shell> default UI.",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"types": "./dist/shell.d.ts",
|
|
30
30
|
"import": "./dist/shell.js"
|
|
31
31
|
},
|
|
32
|
-
"./styles.css": "./dist/index.css"
|
|
32
|
+
"./styles.css": "./dist/index.css",
|
|
33
|
+
"./dist/index.css": "./dist/index.css"
|
|
33
34
|
},
|
|
34
35
|
"main": "dist/index.js",
|
|
35
36
|
"types": "dist/index.d.ts",
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
"@angular/compiler-cli": "^17.0.0"
|
|
61
62
|
},
|
|
62
63
|
"dependencies": {
|
|
63
|
-
"@daltonr/pathwrite-core": "^0.
|
|
64
|
+
"@daltonr/pathwrite-core": "^0.6.0"
|
|
64
65
|
},
|
|
65
66
|
"publishConfig": {
|
|
66
67
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Injectable, OnDestroy, DestroyRef, signal, Signal } from "@angular/core";
|
|
1
|
+
import { Injectable, OnDestroy, DestroyRef, signal, Signal, inject } from "@angular/core";
|
|
2
2
|
import { BehaviorSubject, Observable, Subject } from "rxjs";
|
|
3
3
|
import {
|
|
4
4
|
PathData,
|
|
@@ -134,6 +134,101 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// injectPath() - Signal-based path access
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Return type of `injectPath()`. Provides signal-based reactive access to the
|
|
143
|
+
* path state and strongly-typed navigation actions. Mirrors React's `usePathContext()`
|
|
144
|
+
* return type for consistency across adapters.
|
|
145
|
+
*/
|
|
146
|
+
export interface InjectPathReturn<TData extends PathData = PathData> {
|
|
147
|
+
/** Current path snapshot as a signal. Returns `null` when no path is active. */
|
|
148
|
+
snapshot: Signal<PathSnapshot<TData> | null>;
|
|
149
|
+
/** Start (or restart) a path. */
|
|
150
|
+
start: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
|
|
151
|
+
/** Push a sub-path onto the stack. */
|
|
152
|
+
startSubPath: (path: PathDefinition<any>, initialData?: PathData, meta?: Record<string, unknown>) => Promise<void>;
|
|
153
|
+
/** Advance one step. Completes the path on the last step. */
|
|
154
|
+
next: () => Promise<void>;
|
|
155
|
+
/** Go back one step. No-op when already on the first step. */
|
|
156
|
+
previous: () => Promise<void>;
|
|
157
|
+
/** Cancel the active path (or sub-path). */
|
|
158
|
+
cancel: () => Promise<void>;
|
|
159
|
+
/** Update a single data field. */
|
|
160
|
+
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
161
|
+
/** Jump to a step by ID without checking guards. */
|
|
162
|
+
goToStep: (stepId: string) => Promise<void>;
|
|
163
|
+
/** Jump to a step by ID, checking guards first. */
|
|
164
|
+
goToStepChecked: (stepId: string) => Promise<void>;
|
|
165
|
+
/**
|
|
166
|
+
* Tears down any active path and immediately starts the given path fresh.
|
|
167
|
+
* Use for "Start over" / retry flows.
|
|
168
|
+
*/
|
|
169
|
+
restart: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Inject a PathFacade and return a signal-based API for use in Angular components.
|
|
174
|
+
* Requires `PathFacade` to be provided in the component's injector tree (either via
|
|
175
|
+
* `providers: [PathFacade]` in the component or a parent component).
|
|
176
|
+
*
|
|
177
|
+
* **This is the recommended way to consume Pathwrite in Angular components** — it
|
|
178
|
+
* provides the same ergonomic, framework-native API that React's `usePathContext()`
|
|
179
|
+
* and Vue's `usePath()` offer. No template references or manual facade injection needed.
|
|
180
|
+
*
|
|
181
|
+
* The optional generic `TData` narrows `snapshot().data` and `setData()` to your
|
|
182
|
+
* data shape. It is a **type-level assertion**, not a runtime guarantee.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* @Component({
|
|
187
|
+
* selector: 'app-contact-step',
|
|
188
|
+
* standalone: true,
|
|
189
|
+
* providers: [PathFacade], // ← Provide at this component or a parent
|
|
190
|
+
* template: `
|
|
191
|
+
* @if (path.snapshot(); as s) {
|
|
192
|
+
* <div>Step: {{ s.activeStep?.title }}</div>
|
|
193
|
+
* <button (click)="path.next()">Next</button>
|
|
194
|
+
* }
|
|
195
|
+
* `
|
|
196
|
+
* })
|
|
197
|
+
* export class ContactStepComponent {
|
|
198
|
+
* protected readonly path = injectPath<ContactData>();
|
|
199
|
+
*
|
|
200
|
+
* updateName(name: string) {
|
|
201
|
+
* this.path.setData('name', name);
|
|
202
|
+
* }
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*
|
|
206
|
+
* @throws Error if PathFacade is not provided in the injector tree
|
|
207
|
+
*/
|
|
208
|
+
export function injectPath<TData extends PathData = PathData>(): InjectPathReturn<TData> {
|
|
209
|
+
const facade = inject(PathFacade, { optional: true }) as PathFacade<TData> | null;
|
|
210
|
+
|
|
211
|
+
if (!facade) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
"injectPath() requires PathFacade to be provided. " +
|
|
214
|
+
"Add 'providers: [PathFacade]' to your component or a parent component."
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
snapshot: facade.stateSignal,
|
|
220
|
+
start: (path, initialData = {}) => facade.start(path, initialData),
|
|
221
|
+
startSubPath: (path, initialData = {}, meta) => facade.startSubPath(path, initialData, meta),
|
|
222
|
+
next: () => facade.next(),
|
|
223
|
+
previous: () => facade.previous(),
|
|
224
|
+
cancel: () => facade.cancel(),
|
|
225
|
+
setData: (key, value) => facade.setData(key, value),
|
|
226
|
+
goToStep: (stepId) => facade.goToStep(stepId),
|
|
227
|
+
goToStepChecked: (stepId) => facade.goToStepChecked(stepId),
|
|
228
|
+
restart: (path, initialData = {}) => facade.restart(path, initialData),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
137
232
|
// ---------------------------------------------------------------------------
|
|
138
233
|
// Forms integration
|
|
139
234
|
// ---------------------------------------------------------------------------
|
|
@@ -206,6 +301,7 @@ export function syncFormGroup<TData extends PathData = PathData>(
|
|
|
206
301
|
// Re-export core types for convenience (users don't need to import from @daltonr/pathwrite-core)
|
|
207
302
|
export type {
|
|
208
303
|
PathData,
|
|
304
|
+
FieldErrors,
|
|
209
305
|
PathDefinition,
|
|
210
306
|
PathEvent,
|
|
211
307
|
PathSnapshot,
|
package/src/shell.ts
CHANGED
|
@@ -153,7 +153,7 @@ export class PathShellFooterDirective {
|
|
|
153
153
|
<ng-container *ngTemplateOutlet="customHeader.templateRef; context: { $implicit: s }"></ng-container>
|
|
154
154
|
</ng-container>
|
|
155
155
|
<ng-template #defaultHeader>
|
|
156
|
-
<div class="pw-shell__header" *ngIf="!hideProgress">
|
|
156
|
+
<div class="pw-shell__header" *ngIf="!hideProgress && (s.stepCount > 1 || s.nestingLevel > 0)">
|
|
157
157
|
<div class="pw-shell__steps">
|
|
158
158
|
<div
|
|
159
159
|
*ngFor="let step of s.steps; let i = index"
|
|
@@ -179,9 +179,11 @@ export class PathShellFooterDirective {
|
|
|
179
179
|
</ng-container>
|
|
180
180
|
</div>
|
|
181
181
|
|
|
182
|
-
<!-- Validation messages -->
|
|
183
|
-
<ul class="pw-shell__validation" *ngIf="s.
|
|
184
|
-
<li *ngFor="let
|
|
182
|
+
<!-- Validation messages — labeled by field name -->
|
|
183
|
+
<ul class="pw-shell__validation" *ngIf="s.hasAttemptedNext && fieldEntries(s).length > 0">
|
|
184
|
+
<li *ngFor="let entry of fieldEntries(s)" class="pw-shell__validation-item">
|
|
185
|
+
<span *ngIf="entry[0] !== '_'" class="pw-shell__validation-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
|
|
186
|
+
</li>
|
|
185
187
|
</ul>
|
|
186
188
|
|
|
187
189
|
<!-- Footer — custom or default navigation buttons -->
|
|
@@ -191,8 +193,17 @@ export class PathShellFooterDirective {
|
|
|
191
193
|
<ng-template #defaultFooter>
|
|
192
194
|
<div class="pw-shell__footer">
|
|
193
195
|
<div class="pw-shell__footer-left">
|
|
196
|
+
<!-- Form mode: Cancel on the left -->
|
|
194
197
|
<button
|
|
195
|
-
*ngIf="!
|
|
198
|
+
*ngIf="getResolvedFooterLayout(s) === 'form' && !hideCancel"
|
|
199
|
+
type="button"
|
|
200
|
+
class="pw-shell__btn pw-shell__btn--cancel"
|
|
201
|
+
[disabled]="s.isNavigating"
|
|
202
|
+
(click)="facade.cancel()"
|
|
203
|
+
>{{ cancelLabel }}</button>
|
|
204
|
+
<!-- Wizard mode: Back on the left -->
|
|
205
|
+
<button
|
|
206
|
+
*ngIf="getResolvedFooterLayout(s) === 'wizard' && !s.isFirstStep"
|
|
196
207
|
type="button"
|
|
197
208
|
class="pw-shell__btn pw-shell__btn--back"
|
|
198
209
|
[disabled]="s.isNavigating || !s.canMovePrevious"
|
|
@@ -200,17 +211,19 @@ export class PathShellFooterDirective {
|
|
|
200
211
|
>{{ backLabel }}</button>
|
|
201
212
|
</div>
|
|
202
213
|
<div class="pw-shell__footer-right">
|
|
214
|
+
<!-- Wizard mode: Cancel on the right -->
|
|
203
215
|
<button
|
|
204
|
-
*ngIf="!hideCancel"
|
|
216
|
+
*ngIf="getResolvedFooterLayout(s) === 'wizard' && !hideCancel"
|
|
205
217
|
type="button"
|
|
206
218
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
207
219
|
[disabled]="s.isNavigating"
|
|
208
220
|
(click)="facade.cancel()"
|
|
209
221
|
>{{ cancelLabel }}</button>
|
|
222
|
+
<!-- Both modes: Submit on the right -->
|
|
210
223
|
<button
|
|
211
224
|
type="button"
|
|
212
225
|
class="pw-shell__btn pw-shell__btn--next"
|
|
213
|
-
[disabled]="s.isNavigating
|
|
226
|
+
[disabled]="s.isNavigating"
|
|
214
227
|
(click)="facade.next()"
|
|
215
228
|
>{{ s.isLastStep ? completeLabel : nextLabel }}</button>
|
|
216
229
|
</div>
|
|
@@ -236,8 +249,15 @@ export class PathShellComponent implements OnInit, OnDestroy {
|
|
|
236
249
|
@Input() cancelLabel = "Cancel";
|
|
237
250
|
/** Hide the Cancel button entirely. */
|
|
238
251
|
@Input() hideCancel = false;
|
|
239
|
-
/** Hide the step progress indicator in the header. */
|
|
252
|
+
/** Hide the step progress indicator in the header. Also hidden automatically when the path has only one step. */
|
|
240
253
|
@Input() hideProgress = false;
|
|
254
|
+
/**
|
|
255
|
+
* Footer layout mode:
|
|
256
|
+
* - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
|
|
257
|
+
* - "wizard": Back button on left, Cancel and Submit together on right.
|
|
258
|
+
* - "form": Cancel on left, Submit alone on right. Back button never shown.
|
|
259
|
+
*/
|
|
260
|
+
@Input() footerLayout: "wizard" | "form" | "auto" = "auto";
|
|
241
261
|
|
|
242
262
|
@Output() completed = new EventEmitter<PathData>();
|
|
243
263
|
@Output() cancelled = new EventEmitter<PathData>();
|
|
@@ -287,4 +307,35 @@ export class PathShellComponent implements OnInit, OnDestroy {
|
|
|
287
307
|
this.started = true;
|
|
288
308
|
this.facade.start(this.path, this.initialData);
|
|
289
309
|
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Restart the active path from step 1 with the original `initialData`,
|
|
313
|
+
* without unmounting the shell. Call this via a `#shell` template reference:
|
|
314
|
+
*
|
|
315
|
+
* ```html
|
|
316
|
+
* <pw-shell #shell [path]="myPath" ...></pw-shell>
|
|
317
|
+
* <button (click)="shell.restart()">Try Again</button>
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
public restart(): Promise<void> {
|
|
321
|
+
return this.facade.restart(this.path, this.initialData);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Returns Object.entries(s.fieldMessages) for use in *ngFor. */
|
|
325
|
+
protected fieldEntries(s: PathSnapshot): [string, string][] {
|
|
326
|
+
return Object.entries(s.fieldMessages) as [string, string][];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Resolves "auto" footerLayout based on snapshot. Single-step top-level → "form", otherwise → "wizard". */
|
|
330
|
+
protected getResolvedFooterLayout(s: PathSnapshot): "wizard" | "form" {
|
|
331
|
+
return this.footerLayout === "auto"
|
|
332
|
+
? (s.stepCount === 1 && s.nestingLevel === 0 ? "form" : "wizard")
|
|
333
|
+
: this.footerLayout;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Converts a camelCase or lowercase field key to a display label.
|
|
337
|
+
* e.g. "firstName" → "First Name", "email" → "Email" */
|
|
338
|
+
protected formatFieldKey(key: string): string {
|
|
339
|
+
return key.replace(/([A-Z])/g, " $1").replace(/^./, c => c.toUpperCase()).trim();
|
|
340
|
+
}
|
|
290
341
|
}
|