@daltonr/pathwrite-angular 0.11.0 → 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 +27 -7
- package/dist/index.js +16 -8
- package/dist/index.js.map +1 -1
- package/dist/shell.d.ts +56 -10
- package/dist/shell.js +232 -134
- package/dist/shell.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +28 -13
- package/src/shell.ts +155 -77
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,15 @@ 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);
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
public validate(): void {
|
|
@@ -163,7 +171,7 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
|
|
|
163
171
|
* path state and strongly-typed navigation actions. Mirrors React's `usePathContext()`
|
|
164
172
|
* return type for consistency across adapters.
|
|
165
173
|
*/
|
|
166
|
-
export interface UsePathContextReturn<TData extends PathData = PathData> {
|
|
174
|
+
export interface UsePathContextReturn<TData extends PathData = PathData, TServices = unknown> {
|
|
167
175
|
/** Current path snapshot as a signal. Returns `null` when no path is active. */
|
|
168
176
|
snapshot: Signal<PathSnapshot<TData> | null>;
|
|
169
177
|
/** Start (or restart) a path. */
|
|
@@ -180,10 +188,10 @@ export interface UsePathContextReturn<TData extends PathData = PathData> {
|
|
|
180
188
|
setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
|
|
181
189
|
/** Reset the current step's data to what it was when the step was entered. */
|
|
182
190
|
resetStep: () => Promise<void>;
|
|
183
|
-
/** Jump to a step by ID without checking guards. */
|
|
184
|
-
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>;
|
|
185
193
|
/** Jump to a step by ID, checking guards first. */
|
|
186
|
-
goToStepChecked: (stepId: string) => Promise<void>;
|
|
194
|
+
goToStepChecked: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
|
|
187
195
|
/**
|
|
188
196
|
* Tears down any active path and immediately starts the given path fresh.
|
|
189
197
|
* Use for "Start over" / retry flows.
|
|
@@ -193,6 +201,12 @@ export interface UsePathContextReturn<TData extends PathData = PathData> {
|
|
|
193
201
|
retry: () => Promise<void>;
|
|
194
202
|
/** Pause with intent to return, preserving all state. Emits `suspended`. */
|
|
195
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;
|
|
196
210
|
}
|
|
197
211
|
|
|
198
212
|
/**
|
|
@@ -231,7 +245,7 @@ export interface UsePathContextReturn<TData extends PathData = PathData> {
|
|
|
231
245
|
*
|
|
232
246
|
* @throws Error if PathFacade is not provided in the injector tree
|
|
233
247
|
*/
|
|
234
|
-
export function usePathContext<TData extends PathData = PathData>(): UsePathContextReturn<TData> {
|
|
248
|
+
export function usePathContext<TData extends PathData = PathData, TServices = unknown>(): UsePathContextReturn<TData, TServices> {
|
|
235
249
|
const facade = inject(PathFacade, { optional: true }) as PathFacade<TData> | null;
|
|
236
250
|
|
|
237
251
|
if (!facade) {
|
|
@@ -250,11 +264,12 @@ export function usePathContext<TData extends PathData = PathData>(): UsePathCont
|
|
|
250
264
|
cancel: () => facade.cancel(),
|
|
251
265
|
setData: (key, value) => facade.setData(key, value),
|
|
252
266
|
resetStep: () => facade.resetStep(),
|
|
253
|
-
goToStep: (stepId) => facade.goToStep(stepId),
|
|
254
|
-
goToStepChecked: (stepId) => facade.goToStepChecked(stepId),
|
|
267
|
+
goToStep: (stepId, options) => facade.goToStep(stepId, options),
|
|
268
|
+
goToStepChecked: (stepId, options) => facade.goToStepChecked(stepId, options),
|
|
255
269
|
restart: () => facade.restart(),
|
|
256
270
|
retry: () => facade.retry(),
|
|
257
271
|
suspend: () => facade.suspend(),
|
|
272
|
+
services: facade.services as TServices,
|
|
258
273
|
};
|
|
259
274
|
}
|
|
260
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,75 +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.hasValidated) && 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>
|
|
261
302
|
</ng-container>
|
|
262
|
-
</ng-
|
|
303
|
+
</ng-template>
|
|
263
304
|
</ng-template>
|
|
264
305
|
<ng-template #defaultFooter>
|
|
265
306
|
<div class="pw-shell__footer">
|
|
266
307
|
<div class="pw-shell__footer-left">
|
|
267
308
|
<!-- Form mode: Cancel on the left -->
|
|
268
309
|
<button
|
|
269
|
-
*ngIf="
|
|
310
|
+
*ngIf="getResolvedLayout(s) === 'form' && !hideCancel"
|
|
270
311
|
type="button"
|
|
271
312
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
272
313
|
[disabled]="s.status !== 'idle'"
|
|
@@ -274,7 +315,7 @@ export class PathShellFooterDirective {
|
|
|
274
315
|
>{{ cancelLabel }}</button>
|
|
275
316
|
<!-- Wizard mode: Back on the left -->
|
|
276
317
|
<button
|
|
277
|
-
*ngIf="
|
|
318
|
+
*ngIf="getResolvedLayout(s) === 'wizard' && !s.isFirstStep"
|
|
278
319
|
type="button"
|
|
279
320
|
class="pw-shell__btn pw-shell__btn--back"
|
|
280
321
|
[disabled]="s.status !== 'idle' || !s.canMovePrevious"
|
|
@@ -284,7 +325,7 @@ export class PathShellFooterDirective {
|
|
|
284
325
|
<div class="pw-shell__footer-right">
|
|
285
326
|
<!-- Wizard mode: Cancel on the right -->
|
|
286
327
|
<button
|
|
287
|
-
*ngIf="
|
|
328
|
+
*ngIf="getResolvedLayout(s) === 'wizard' && !hideCancel"
|
|
288
329
|
type="button"
|
|
289
330
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
290
331
|
[disabled]="s.status !== 'idle'"
|
|
@@ -322,8 +363,14 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
322
363
|
* ```
|
|
323
364
|
*/
|
|
324
365
|
@Input() engine?: PathEngine;
|
|
325
|
-
/** 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. */
|
|
326
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;
|
|
327
374
|
/** Start the path automatically on ngOnInit. Set to false to call doStart() manually. */
|
|
328
375
|
@Input() autoStart = true;
|
|
329
376
|
/** Label for the Back navigation button. */
|
|
@@ -345,12 +392,19 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
345
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`. */
|
|
346
393
|
@Input() validateWhen = false;
|
|
347
394
|
/**
|
|
348
|
-
*
|
|
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;
|
|
400
|
+
/**
|
|
401
|
+
* Shell layout mode:
|
|
349
402
|
* - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
|
|
350
|
-
* - "wizard": Back button on left, Cancel and Submit together on right.
|
|
351
|
-
* - "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.
|
|
352
406
|
*/
|
|
353
|
-
@Input()
|
|
407
|
+
@Input() layout: "wizard" | "form" | "auto" | "tabs" = "auto";
|
|
354
408
|
/**
|
|
355
409
|
* Controls whether the shell renders its auto-generated field-error summary box.
|
|
356
410
|
* - `"summary"` (default): Shell renders the labeled error list below the step body.
|
|
@@ -374,11 +428,14 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
374
428
|
@ContentChildren(PathStepDirective) stepDirectives!: QueryList<PathStepDirective>;
|
|
375
429
|
@ContentChild(PathShellHeaderDirective) customHeader?: PathShellHeaderDirective;
|
|
376
430
|
@ContentChild(PathShellFooterDirective) customFooter?: PathShellFooterDirective;
|
|
431
|
+
@ContentChild(PathShellCompletionDirective) customCompletion?: PathShellCompletionDirective;
|
|
377
432
|
|
|
378
433
|
public readonly facade = inject(PathFacade);
|
|
379
434
|
/** The shell's own component-level injector. Passed to ngTemplateOutlet so that
|
|
380
435
|
* step components can resolve PathFacade (provided by this shell) via inject(). */
|
|
381
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 });
|
|
382
439
|
public started = false;
|
|
383
440
|
|
|
384
441
|
/** Navigation actions passed to custom `pwShellFooter` templates. */
|
|
@@ -386,8 +443,8 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
386
443
|
next: () => this.facade.next(),
|
|
387
444
|
previous: () => this.facade.previous(),
|
|
388
445
|
cancel: () => this.facade.cancel(),
|
|
389
|
-
goToStep: (id) => this.facade.goToStep(id),
|
|
390
|
-
goToStepChecked: (id) => this.facade.goToStepChecked(id),
|
|
446
|
+
goToStep: (id, options) => this.facade.goToStep(id, options),
|
|
447
|
+
goToStepChecked: (id, options) => this.facade.goToStepChecked(id, options),
|
|
391
448
|
setData: (key, value) => this.facade.setData(key, value as never),
|
|
392
449
|
restart: () => this.facade.restart(),
|
|
393
450
|
retry: () => this.facade.retry(),
|
|
@@ -403,13 +460,20 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
403
460
|
if (changes['validateWhen'] && this.validateWhen) {
|
|
404
461
|
this.facade.validate();
|
|
405
462
|
}
|
|
463
|
+
if (changes['services']) {
|
|
464
|
+
this.facade.services = this.services;
|
|
465
|
+
}
|
|
406
466
|
}
|
|
407
467
|
|
|
408
468
|
public ngOnInit(): void {
|
|
469
|
+
this.facade.services = this.services;
|
|
409
470
|
this.facade.events$.pipe(takeUntil(this.destroy$)).subscribe((event) => {
|
|
410
471
|
this.event.emit(event);
|
|
411
472
|
if (event.type === "completed") this.complete.emit(event.data);
|
|
412
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
|
+
}
|
|
413
477
|
});
|
|
414
478
|
|
|
415
479
|
if (this.autoStart && !this.engine) {
|
|
@@ -425,7 +489,18 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
425
489
|
public doStart(): void {
|
|
426
490
|
if (!this.path) throw new Error('[pw-shell] [path] is required when no [engine] is provided');
|
|
427
491
|
this.started = true;
|
|
428
|
-
|
|
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
|
+
});
|
|
429
504
|
}
|
|
430
505
|
|
|
431
506
|
/**
|
|
@@ -451,11 +526,14 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
451
526
|
return Object.entries(s.fieldWarnings) as [string, string][];
|
|
452
527
|
}
|
|
453
528
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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"
|
|
457
535
|
? (s.stepCount === 1 && s.nestingLevel === 0 ? "form" : "wizard")
|
|
458
|
-
: this.
|
|
536
|
+
: this.layout;
|
|
459
537
|
}
|
|
460
538
|
|
|
461
539
|
protected errorPhaseMessage = errorPhaseMessage;
|