@daltonr/pathwrite-angular 0.10.1 → 0.12.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 +92 -6
- package/dist/index.css +25 -0
- package/dist/index.d.ts +28 -7
- package/dist/index.js +19 -8
- package/dist/index.js.map +1 -1
- package/dist/shell.d.ts +60 -10
- package/dist/shell.js +245 -132
- package/dist/shell.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +32 -13
- package/src/shell.ts +163 -76
package/src/index.ts
CHANGED
|
@@ -30,6 +30,13 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
|
|
|
30
30
|
private _unsubscribeFromEngine: () => void = () => {};
|
|
31
31
|
private readonly _stateSignal = signal<PathSnapshot<TData> | null>(null);
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Arbitrary services object passed via `[services]` on `<pw-shell>`.
|
|
35
|
+
* Read it in step components via `usePathContext<TData, TServices>().services`.
|
|
36
|
+
* Set directly by `PathShellComponent` — do not set this manually.
|
|
37
|
+
*/
|
|
38
|
+
public services: unknown = null;
|
|
39
|
+
|
|
33
40
|
public readonly state$: Observable<PathSnapshot<TData> | null> = this._state$.asObservable();
|
|
34
41
|
public readonly events$: Observable<PathEvent> = this._events$.asObservable();
|
|
35
42
|
/** Signal version of state$. Updates on every path state change. Requires Angular 16+. */
|
|
@@ -72,8 +79,9 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
|
|
|
72
79
|
this._state$.next(event.snapshot as PathSnapshot<TData>);
|
|
73
80
|
this._stateSignal.set(event.snapshot as PathSnapshot<TData>);
|
|
74
81
|
} else if (event.type === "completed" || event.type === "cancelled") {
|
|
75
|
-
|
|
76
|
-
this.
|
|
82
|
+
const snap = engine.snapshot() as PathSnapshot<TData> | null;
|
|
83
|
+
this._state$.next(snap);
|
|
84
|
+
this._stateSignal.set(snap);
|
|
77
85
|
}
|
|
78
86
|
});
|
|
79
87
|
}
|
|
@@ -134,15 +142,19 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
|
|
|
134
142
|
return this._engine.resetStep();
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
public goToStep(stepId: string): Promise<void> {
|
|
138
|
-
return this._engine.goToStep(stepId);
|
|
145
|
+
public goToStep(stepId: string, options?: { validateOnLeave?: boolean }): Promise<void> {
|
|
146
|
+
return this._engine.goToStep(stepId, options);
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
/** Jump to a step by ID, checking the current step's canMoveNext (forward) or
|
|
142
150
|
* canMovePrevious (backward) guard first. Navigation is blocked if the guard
|
|
143
151
|
* returns false. Throws if the step ID does not exist. */
|
|
144
|
-
public goToStepChecked(stepId: string): Promise<void> {
|
|
145
|
-
return this._engine.goToStepChecked(stepId);
|
|
152
|
+
public goToStepChecked(stepId: string, options?: { validateOnLeave?: boolean }): Promise<void> {
|
|
153
|
+
return this._engine.goToStepChecked(stepId, options);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public validate(): void {
|
|
157
|
+
this._engine.validate();
|
|
146
158
|
}
|
|
147
159
|
|
|
148
160
|
public snapshot(): PathSnapshot<TData> | null {
|
|
@@ -159,7 +171,7 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
|
|
|
159
171
|
* path state and strongly-typed navigation actions. Mirrors React's `usePathContext()`
|
|
160
172
|
* return type for consistency across adapters.
|
|
161
173
|
*/
|
|
162
|
-
export interface UsePathContextReturn<TData extends PathData = PathData> {
|
|
174
|
+
export interface UsePathContextReturn<TData extends PathData = PathData, TServices = unknown> {
|
|
163
175
|
/** Current path snapshot as a signal. Returns `null` when no path is active. */
|
|
164
176
|
snapshot: Signal<PathSnapshot<TData> | null>;
|
|
165
177
|
/** Start (or restart) a path. */
|
|
@@ -176,10 +188,10 @@ export interface UsePathContextReturn<TData extends PathData = PathData> {
|
|
|
176
188
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
177
189
|
/** Reset the current step's data to what it was when the step was entered. */
|
|
178
190
|
resetStep: () => Promise<void>;
|
|
179
|
-
/** Jump to a step by ID without checking guards. */
|
|
180
|
-
goToStep: (stepId: string) => Promise<void>;
|
|
191
|
+
/** Jump to a step by ID without checking guards. Pass `{ validateOnLeave: true }` to mark the departing step as attempted before navigating. */
|
|
192
|
+
goToStep: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
181
193
|
/** Jump to a step by ID, checking guards first. */
|
|
182
|
-
goToStepChecked: (stepId: string) => Promise<void>;
|
|
194
|
+
goToStepChecked: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
183
195
|
/**
|
|
184
196
|
* Tears down any active path and immediately starts the given path fresh.
|
|
185
197
|
* Use for "Start over" / retry flows.
|
|
@@ -189,6 +201,12 @@ export interface UsePathContextReturn<TData extends PathData = PathData> {
|
|
|
189
201
|
retry: () => Promise<void>;
|
|
190
202
|
/** Pause with intent to return, preserving all state. Emits `suspended`. */
|
|
191
203
|
suspend: () => Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* The services object passed to the nearest `<pw-shell>` via `[services]`.
|
|
206
|
+
* Typed as `TServices` — pass your services interface as the second generic:
|
|
207
|
+
* `usePathContext<MyData, MyServices>().services`.
|
|
208
|
+
*/
|
|
209
|
+
services: TServices;
|
|
192
210
|
}
|
|
193
211
|
|
|
194
212
|
/**
|
|
@@ -227,7 +245,7 @@ export interface UsePathContextReturn<TData extends PathData = PathData> {
|
|
|
227
245
|
*
|
|
228
246
|
* @throws Error if PathFacade is not provided in the injector tree
|
|
229
247
|
*/
|
|
230
|
-
export function usePathContext<TData extends PathData = PathData>(): UsePathContextReturn<TData> {
|
|
248
|
+
export function usePathContext<TData extends PathData = PathData, TServices = unknown>(): UsePathContextReturn<TData, TServices> {
|
|
231
249
|
const facade = inject(PathFacade, { optional: true }) as PathFacade<TData> | null;
|
|
232
250
|
|
|
233
251
|
if (!facade) {
|
|
@@ -246,11 +264,12 @@ export function usePathContext<TData extends PathData = PathData>(): UsePathCont
|
|
|
246
264
|
cancel: () => facade.cancel(),
|
|
247
265
|
setData: (key, value) => facade.setData(key, value),
|
|
248
266
|
resetStep: () => facade.resetStep(),
|
|
249
|
-
goToStep: (stepId) => facade.goToStep(stepId),
|
|
250
|
-
goToStepChecked: (stepId) => facade.goToStepChecked(stepId),
|
|
267
|
+
goToStep: (stepId, options) => facade.goToStep(stepId, options),
|
|
268
|
+
goToStepChecked: (stepId, options) => facade.goToStepChecked(stepId, options),
|
|
251
269
|
restart: () => facade.restart(),
|
|
252
270
|
retry: () => facade.retry(),
|
|
253
271
|
suspend: () => facade.suspend(),
|
|
272
|
+
services: facade.services as TServices,
|
|
254
273
|
};
|
|
255
274
|
}
|
|
256
275
|
|
package/src/shell.ts
CHANGED
|
@@ -45,8 +45,8 @@ export interface PathShellActions {
|
|
|
45
45
|
next: () => Promise<void>;
|
|
46
46
|
previous: () => Promise<void>;
|
|
47
47
|
cancel: () => Promise<void>;
|
|
48
|
-
goToStep: (stepId: string) => Promise<void>;
|
|
49
|
-
goToStepChecked: (stepId: string) => Promise<void>;
|
|
48
|
+
goToStep: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
49
|
+
goToStepChecked: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
50
50
|
setData: (key: string, value: unknown) => Promise<void>;
|
|
51
51
|
/** Restart the shell's current path with its current `initialData`. */
|
|
52
52
|
restart: () => Promise<void>;
|
|
@@ -127,6 +127,31 @@ export class PathShellFooterDirective {
|
|
|
127
127
|
) {}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// PathShellCompletionDirective
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Replaces the default completion panel inside `<pw-shell>` when
|
|
136
|
+
* `snapshot.status === "completed"` (`completionBehaviour: "stayOnFinal"`).
|
|
137
|
+
* The template receives the current `PathSnapshot` as the implicit context.
|
|
138
|
+
*
|
|
139
|
+
* ```html
|
|
140
|
+
* <pw-shell [path]="myPath">
|
|
141
|
+
* <ng-template pwShellCompletion let-s>
|
|
142
|
+
* <my-completion-screen [data]="s.data" />
|
|
143
|
+
* </ng-template>
|
|
144
|
+
* <ng-template pwStep="details"><app-details-form /></ng-template>
|
|
145
|
+
* </pw-shell>
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
@Directive({ selector: "[pwShellCompletion]", standalone: true })
|
|
149
|
+
export class PathShellCompletionDirective {
|
|
150
|
+
public constructor(
|
|
151
|
+
public readonly templateRef: TemplateRef<{ $implicit: PathSnapshot }>
|
|
152
|
+
) {}
|
|
153
|
+
}
|
|
154
|
+
|
|
130
155
|
// ---------------------------------------------------------------------------
|
|
131
156
|
// PathShellComponent
|
|
132
157
|
// ---------------------------------------------------------------------------
|
|
@@ -160,7 +185,7 @@ export class PathShellFooterDirective {
|
|
|
160
185
|
<!-- Active path -->
|
|
161
186
|
<div class="pw-shell" [ngClass]="progressLayout !== 'merged' ? 'pw-shell--progress-' + progressLayout : ''" *ngIf="facade.state$ | async as s">
|
|
162
187
|
<!-- Root progress — persistent top-level bar visible during sub-paths -->
|
|
163
|
-
<div class="pw-shell__root-progress" *ngIf="!
|
|
188
|
+
<div class="pw-shell__root-progress" *ngIf="!effectiveHideProgress && s.rootProgress && progressLayout !== 'activeOnly'">
|
|
164
189
|
<div class="pw-shell__steps">
|
|
165
190
|
<div
|
|
166
191
|
*ngFor="let step of s.rootProgress!.steps; let i = index"
|
|
@@ -181,7 +206,7 @@ export class PathShellFooterDirective {
|
|
|
181
206
|
<ng-container *ngTemplateOutlet="customHeader.templateRef; context: { $implicit: s }"></ng-container>
|
|
182
207
|
</ng-container>
|
|
183
208
|
<ng-template #defaultHeader>
|
|
184
|
-
<div class="pw-shell__header" *ngIf="!
|
|
209
|
+
<div class="pw-shell__header" *ngIf="!effectiveHideProgress && (s.stepCount > 1 || s.nestingLevel > 0) && progressLayout !== 'rootOnly'">
|
|
185
210
|
<div class="pw-shell__steps">
|
|
186
211
|
<div
|
|
187
212
|
*ngFor="let step of s.steps; let i = index"
|
|
@@ -198,73 +223,91 @@ export class PathShellFooterDirective {
|
|
|
198
223
|
</div>
|
|
199
224
|
</ng-template>
|
|
200
225
|
|
|
201
|
-
<!--
|
|
202
|
-
<
|
|
203
|
-
<ng-container *
|
|
204
|
-
|
|
205
|
-
<ng-container *ngIf="stepDir.stepId === (s.formId ?? s.stepId)">
|
|
206
|
-
<ng-container *ngTemplateOutlet="stepDir.templateRef; injector: shellInjector"></ng-container>
|
|
207
|
-
</ng-container>
|
|
226
|
+
<!-- Completion panel — shown when path finishes with stayOnFinal -->
|
|
227
|
+
<ng-container *ngIf="s.status === 'completed'; else activeContent">
|
|
228
|
+
<ng-container *ngIf="customCompletion; else defaultCompletion">
|
|
229
|
+
<ng-container *ngTemplateOutlet="customCompletion.templateRef; context: { $implicit: s }"></ng-container>
|
|
208
230
|
</ng-container>
|
|
209
|
-
|
|
231
|
+
<ng-template #defaultCompletion>
|
|
232
|
+
<div class="pw-shell__completion">
|
|
233
|
+
<p class="pw-shell__completion-message">All done.</p>
|
|
234
|
+
<button type="button" class="pw-shell__completion-restart" (click)="facade.restart()">Start over</button>
|
|
235
|
+
</div>
|
|
236
|
+
</ng-template>
|
|
237
|
+
</ng-container>
|
|
210
238
|
|
|
211
|
-
<!--
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<span *ngIf="entry[0] !== '_'" class="pw-shell__warnings-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
|
|
222
|
-
</li>
|
|
223
|
-
</ul>
|
|
224
|
-
|
|
225
|
-
<!-- Blocking error — guard returned { allowed: false, reason } -->
|
|
226
|
-
<p class="pw-shell__blocking-error"
|
|
227
|
-
*ngIf="validationDisplay !== 'inline' && s.hasAttemptedNext && s.blockingError">
|
|
228
|
-
{{ s.blockingError }}
|
|
229
|
-
</p>
|
|
230
|
-
|
|
231
|
-
<!-- Error panel — replaces footer when an async operation has failed -->
|
|
232
|
-
<div class="pw-shell__error" *ngIf="s.status === 'error' && s.error; else footerOrCustom">
|
|
233
|
-
<div class="pw-shell__error-title">{{ s.error!.retryCount >= 2 ? 'Still having trouble.' : 'Something went wrong.' }}</div>
|
|
234
|
-
<div class="pw-shell__error-message">{{ errorPhaseMessage(s.error!.phase) }}{{ s.error!.message ? ' ' + s.error!.message : '' }}</div>
|
|
235
|
-
<div class="pw-shell__error-actions">
|
|
236
|
-
<button
|
|
237
|
-
*ngIf="s.error!.retryCount < 2"
|
|
238
|
-
type="button"
|
|
239
|
-
class="pw-shell__btn pw-shell__btn--retry"
|
|
240
|
-
(click)="facade.retry()"
|
|
241
|
-
>Try again</button>
|
|
242
|
-
<button
|
|
243
|
-
*ngIf="s.hasPersistence"
|
|
244
|
-
type="button"
|
|
245
|
-
[class]="'pw-shell__btn ' + (s.error!.retryCount >= 2 ? 'pw-shell__btn--retry' : 'pw-shell__btn--suspend')"
|
|
246
|
-
(click)="facade.suspend()"
|
|
247
|
-
>Save and come back later</button>
|
|
248
|
-
<button
|
|
249
|
-
*ngIf="s.error!.retryCount >= 2 && !s.hasPersistence"
|
|
250
|
-
type="button"
|
|
251
|
-
class="pw-shell__btn pw-shell__btn--retry"
|
|
252
|
-
(click)="facade.retry()"
|
|
253
|
-
>Try again</button>
|
|
239
|
+
<!-- Active step content -->
|
|
240
|
+
<ng-template #activeContent>
|
|
241
|
+
<!-- Body — step content -->
|
|
242
|
+
<div class="pw-shell__body">
|
|
243
|
+
<ng-container *ngFor="let stepDir of stepDirectives">
|
|
244
|
+
<!-- Match by formId first (inner step of a StepChoice), then stepId -->
|
|
245
|
+
<ng-container *ngIf="stepDir.stepId === (s.formId ?? s.stepId)">
|
|
246
|
+
<ng-container *ngTemplateOutlet="stepDir.templateRef; injector: shellInjector"></ng-container>
|
|
247
|
+
</ng-container>
|
|
248
|
+
</ng-container>
|
|
254
249
|
</div>
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
250
|
+
|
|
251
|
+
<!-- Validation messages — suppressed when validationDisplay="inline" -->
|
|
252
|
+
<ul class="pw-shell__validation" *ngIf="validationDisplay !== 'inline' && (s.hasAttemptedNext || s.hasValidated) && fieldEntries(s).length > 0">
|
|
253
|
+
<li *ngFor="let entry of fieldEntries(s)" class="pw-shell__validation-item">
|
|
254
|
+
<span *ngIf="entry[0] !== '_'" class="pw-shell__validation-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
|
|
255
|
+
</li>
|
|
256
|
+
</ul>
|
|
257
|
+
|
|
258
|
+
<!-- Warning messages — non-blocking, shown immediately (no hasAttemptedNext gate) -->
|
|
259
|
+
<ul class="pw-shell__warnings" *ngIf="validationDisplay !== 'inline' && warningEntries(s).length > 0">
|
|
260
|
+
<li *ngFor="let entry of warningEntries(s)" class="pw-shell__warnings-item">
|
|
261
|
+
<span *ngIf="entry[0] !== '_'" class="pw-shell__warnings-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
|
|
262
|
+
</li>
|
|
263
|
+
</ul>
|
|
264
|
+
|
|
265
|
+
<!-- Blocking error — guard returned { allowed: false, reason } -->
|
|
266
|
+
<p class="pw-shell__blocking-error"
|
|
267
|
+
*ngIf="validationDisplay !== 'inline' && (s.hasAttemptedNext || s.hasValidated) && s.blockingError">
|
|
268
|
+
{{ s.blockingError }}
|
|
269
|
+
</p>
|
|
270
|
+
|
|
271
|
+
<!-- Error panel — replaces footer when an async operation has failed -->
|
|
272
|
+
<div class="pw-shell__error" *ngIf="s.status === 'error' && s.error; else footerOrCustom">
|
|
273
|
+
<div class="pw-shell__error-title">{{ s.error!.retryCount >= 2 ? 'Still having trouble.' : 'Something went wrong.' }}</div>
|
|
274
|
+
<div class="pw-shell__error-message">{{ errorPhaseMessage(s.error!.phase) }}{{ s.error!.message ? ' ' + s.error!.message : '' }}</div>
|
|
275
|
+
<div class="pw-shell__error-actions">
|
|
276
|
+
<button
|
|
277
|
+
*ngIf="s.error!.retryCount < 2"
|
|
278
|
+
type="button"
|
|
279
|
+
class="pw-shell__btn pw-shell__btn--retry"
|
|
280
|
+
(click)="facade.retry()"
|
|
281
|
+
>Try again</button>
|
|
282
|
+
<button
|
|
283
|
+
*ngIf="s.hasPersistence"
|
|
284
|
+
type="button"
|
|
285
|
+
[class]="'pw-shell__btn ' + (s.error!.retryCount >= 2 ? 'pw-shell__btn--retry' : 'pw-shell__btn--suspend')"
|
|
286
|
+
(click)="facade.suspend()"
|
|
287
|
+
>Save and come back later</button>
|
|
288
|
+
<button
|
|
289
|
+
*ngIf="s.error!.retryCount >= 2 && !s.hasPersistence"
|
|
290
|
+
type="button"
|
|
291
|
+
class="pw-shell__btn pw-shell__btn--retry"
|
|
292
|
+
(click)="facade.retry()"
|
|
293
|
+
>Try again</button>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
<!-- Footer — custom or default navigation buttons -->
|
|
297
|
+
<ng-template #footerOrCustom>
|
|
298
|
+
<ng-container *ngIf="!effectiveHideFooter">
|
|
299
|
+
<ng-container *ngIf="customFooter; else defaultFooter">
|
|
300
|
+
<ng-container *ngTemplateOutlet="customFooter.templateRef; context: { $implicit: s, actions: shellActions }"></ng-container>
|
|
301
|
+
</ng-container>
|
|
302
|
+
</ng-container>
|
|
303
|
+
</ng-template>
|
|
261
304
|
</ng-template>
|
|
262
305
|
<ng-template #defaultFooter>
|
|
263
306
|
<div class="pw-shell__footer">
|
|
264
307
|
<div class="pw-shell__footer-left">
|
|
265
308
|
<!-- Form mode: Cancel on the left -->
|
|
266
309
|
<button
|
|
267
|
-
*ngIf="
|
|
310
|
+
*ngIf="getResolvedLayout(s) === 'form' && !hideCancel"
|
|
268
311
|
type="button"
|
|
269
312
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
270
313
|
[disabled]="s.status !== 'idle'"
|
|
@@ -272,7 +315,7 @@ export class PathShellFooterDirective {
|
|
|
272
315
|
>{{ cancelLabel }}</button>
|
|
273
316
|
<!-- Wizard mode: Back on the left -->
|
|
274
317
|
<button
|
|
275
|
-
*ngIf="
|
|
318
|
+
*ngIf="getResolvedLayout(s) === 'wizard' && !s.isFirstStep"
|
|
276
319
|
type="button"
|
|
277
320
|
class="pw-shell__btn pw-shell__btn--back"
|
|
278
321
|
[disabled]="s.status !== 'idle' || !s.canMovePrevious"
|
|
@@ -282,7 +325,7 @@ export class PathShellFooterDirective {
|
|
|
282
325
|
<div class="pw-shell__footer-right">
|
|
283
326
|
<!-- Wizard mode: Cancel on the right -->
|
|
284
327
|
<button
|
|
285
|
-
*ngIf="
|
|
328
|
+
*ngIf="getResolvedLayout(s) === 'wizard' && !hideCancel"
|
|
286
329
|
type="button"
|
|
287
330
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
288
331
|
[disabled]="s.status !== 'idle'"
|
|
@@ -320,8 +363,14 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
320
363
|
* ```
|
|
321
364
|
*/
|
|
322
365
|
@Input() engine?: PathEngine;
|
|
323
|
-
/** Initial data merged into the path engine on start. */
|
|
366
|
+
/** Initial data merged into the path engine on start. Used on first visit; overridden by stored snapshot when `restoreKey` is set. */
|
|
324
367
|
@Input() initialData: PathData = {};
|
|
368
|
+
/**
|
|
369
|
+
* When set, this shell automatically saves its state into the nearest outer `pw-shell`'s
|
|
370
|
+
* data under this key on every change, and restores from that stored state on remount.
|
|
371
|
+
* No-op when used on a top-level shell with no outer `pw-shell` ancestor.
|
|
372
|
+
*/
|
|
373
|
+
@Input() restoreKey?: string;
|
|
325
374
|
/** Start the path automatically on ngOnInit. Set to false to call doStart() manually. */
|
|
326
375
|
@Input() autoStart = true;
|
|
327
376
|
/** Label for the Back navigation button. */
|
|
@@ -338,13 +387,24 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
338
387
|
@Input() hideCancel = false;
|
|
339
388
|
/** Hide the step progress indicator in the header. Also hidden automatically when the path has only one step. */
|
|
340
389
|
@Input() hideProgress = false;
|
|
390
|
+
/** Hide the footer (navigation buttons). The error panel is still shown on async failure regardless of this input. */
|
|
391
|
+
@Input() hideFooter = false;
|
|
392
|
+
/** When true, calls `validate()` on the facade so all steps show inline errors simultaneously. Useful when this shell is nested inside a step of an outer shell: bind to the outer snapshot's `hasAttemptedNext`. */
|
|
393
|
+
@Input() validateWhen = false;
|
|
394
|
+
/**
|
|
395
|
+
* Arbitrary services object made available to all step components via
|
|
396
|
+
* `usePathContext<TData, TServices>().services`. Pass API clients, feature
|
|
397
|
+
* flags, or any shared dependency without prop-drilling through each step.
|
|
398
|
+
*/
|
|
399
|
+
@Input() services: unknown = null;
|
|
341
400
|
/**
|
|
342
|
-
*
|
|
401
|
+
* Shell layout mode:
|
|
343
402
|
* - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
|
|
344
|
-
* - "wizard": Back button on left, Cancel and Submit together on right.
|
|
345
|
-
* - "form": Cancel on left, Submit alone on right. Back button never shown.
|
|
403
|
+
* - "wizard": Progress header + Back button on left, Cancel and Submit together on right.
|
|
404
|
+
* - "form": Progress header + Cancel on left, Submit alone on right. Back button never shown.
|
|
405
|
+
* - "tabs": No progress header, no footer. Use for tabbed interfaces with a custom tab bar inside the step body.
|
|
346
406
|
*/
|
|
347
|
-
@Input()
|
|
407
|
+
@Input() layout: "wizard" | "form" | "auto" | "tabs" = "auto";
|
|
348
408
|
/**
|
|
349
409
|
* Controls whether the shell renders its auto-generated field-error summary box.
|
|
350
410
|
* - `"summary"` (default): Shell renders the labeled error list below the step body.
|
|
@@ -368,11 +428,14 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
368
428
|
@ContentChildren(PathStepDirective) stepDirectives!: QueryList<PathStepDirective>;
|
|
369
429
|
@ContentChild(PathShellHeaderDirective) customHeader?: PathShellHeaderDirective;
|
|
370
430
|
@ContentChild(PathShellFooterDirective) customFooter?: PathShellFooterDirective;
|
|
431
|
+
@ContentChild(PathShellCompletionDirective) customCompletion?: PathShellCompletionDirective;
|
|
371
432
|
|
|
372
433
|
public readonly facade = inject(PathFacade);
|
|
373
434
|
/** The shell's own component-level injector. Passed to ngTemplateOutlet so that
|
|
374
435
|
* step components can resolve PathFacade (provided by this shell) via inject(). */
|
|
375
436
|
protected readonly shellInjector = inject(Injector);
|
|
437
|
+
/** Outer shell's PathFacade — present when this shell is nested inside another pw-shell. */
|
|
438
|
+
private readonly outerFacade = inject(PathFacade, { skipSelf: true, optional: true });
|
|
376
439
|
public started = false;
|
|
377
440
|
|
|
378
441
|
/** Navigation actions passed to custom `pwShellFooter` templates. */
|
|
@@ -380,8 +443,8 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
380
443
|
next: () => this.facade.next(),
|
|
381
444
|
previous: () => this.facade.previous(),
|
|
382
445
|
cancel: () => this.facade.cancel(),
|
|
383
|
-
goToStep: (id) => this.facade.goToStep(id),
|
|
384
|
-
goToStepChecked: (id) => this.facade.goToStepChecked(id),
|
|
446
|
+
goToStep: (id, options) => this.facade.goToStep(id, options),
|
|
447
|
+
goToStepChecked: (id, options) => this.facade.goToStepChecked(id, options),
|
|
385
448
|
setData: (key, value) => this.facade.setData(key, value as never),
|
|
386
449
|
restart: () => this.facade.restart(),
|
|
387
450
|
retry: () => this.facade.retry(),
|
|
@@ -394,13 +457,23 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
394
457
|
if (changes['engine'] && this.engine) {
|
|
395
458
|
this.facade.adoptEngine(this.engine);
|
|
396
459
|
}
|
|
460
|
+
if (changes['validateWhen'] && this.validateWhen) {
|
|
461
|
+
this.facade.validate();
|
|
462
|
+
}
|
|
463
|
+
if (changes['services']) {
|
|
464
|
+
this.facade.services = this.services;
|
|
465
|
+
}
|
|
397
466
|
}
|
|
398
467
|
|
|
399
468
|
public ngOnInit(): void {
|
|
469
|
+
this.facade.services = this.services;
|
|
400
470
|
this.facade.events$.pipe(takeUntil(this.destroy$)).subscribe((event) => {
|
|
401
471
|
this.event.emit(event);
|
|
402
472
|
if (event.type === "completed") this.complete.emit(event.data);
|
|
403
473
|
if (event.type === "cancelled") this.cancel.emit(event.data);
|
|
474
|
+
if (this.restoreKey && this.outerFacade && event.type === "stateChanged") {
|
|
475
|
+
this.outerFacade.setData(this.restoreKey as any, (event as any).snapshot as any);
|
|
476
|
+
}
|
|
404
477
|
});
|
|
405
478
|
|
|
406
479
|
if (this.autoStart && !this.engine) {
|
|
@@ -416,7 +489,18 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
416
489
|
public doStart(): void {
|
|
417
490
|
if (!this.path) throw new Error('[pw-shell] [path] is required when no [engine] is provided');
|
|
418
491
|
this.started = true;
|
|
419
|
-
|
|
492
|
+
let startData: PathData = this.initialData;
|
|
493
|
+
let restoreStepId: string | undefined;
|
|
494
|
+
if (this.restoreKey && this.outerFacade) {
|
|
495
|
+
const stored = this.outerFacade.snapshot()?.data[this.restoreKey] as PathSnapshot | undefined;
|
|
496
|
+
if (stored != null && typeof stored === "object" && "stepId" in stored) {
|
|
497
|
+
startData = stored.data as PathData;
|
|
498
|
+
if (stored.stepIndex > 0) restoreStepId = stored.stepId as string;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
this.facade.start(this.path, startData).then(() => {
|
|
502
|
+
if (restoreStepId) this.facade.goToStep(restoreStepId!);
|
|
503
|
+
});
|
|
420
504
|
}
|
|
421
505
|
|
|
422
506
|
/**
|
|
@@ -442,11 +526,14 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
442
526
|
return Object.entries(s.fieldWarnings) as [string, string][];
|
|
443
527
|
}
|
|
444
528
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
529
|
+
get effectiveHideProgress(): boolean { return this.hideProgress || this.layout === "tabs"; }
|
|
530
|
+
get effectiveHideFooter(): boolean { return this.hideFooter || this.layout === "tabs"; }
|
|
531
|
+
|
|
532
|
+
/** Resolves "auto"/"tabs" layout to "wizard" or "form" for footer button arrangement. */
|
|
533
|
+
protected getResolvedLayout(s: PathSnapshot): "wizard" | "form" {
|
|
534
|
+
return this.layout === "auto" || this.layout === "tabs"
|
|
448
535
|
? (s.stepCount === 1 && s.nestingLevel === 0 ? "form" : "wizard")
|
|
449
|
-
: this.
|
|
536
|
+
: this.layout;
|
|
450
537
|
}
|
|
451
538
|
|
|
452
539
|
protected errorPhaseMessage = errorPhaseMessage;
|