@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/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
- this._state$.next(null);
76
- this._stateSignal.set(null);
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="!hideProgress && s.rootProgress && progressLayout !== 'activeOnly'">
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="!hideProgress && (s.stepCount > 1 || s.nestingLevel > 0) && progressLayout !== 'rootOnly'">
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
- <!-- Bodystep content -->
202
- <div class="pw-shell__body">
203
- <ng-container *ngFor="let stepDir of stepDirectives">
204
- <!-- Match by formId first (inner step of a StepChoice), then stepId -->
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
- </div>
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
- <!-- Validation messages suppressed when validationDisplay="inline" -->
212
- <ul class="pw-shell__validation" *ngIf="validationDisplay !== 'inline' && s.hasAttemptedNext && fieldEntries(s).length > 0">
213
- <li *ngFor="let entry of fieldEntries(s)" class="pw-shell__validation-item">
214
- <span *ngIf="entry[0] !== '_'" class="pw-shell__validation-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
215
- </li>
216
- </ul>
217
-
218
- <!-- Warning messages — non-blocking, shown immediately (no hasAttemptedNext gate) -->
219
- <ul class="pw-shell__warnings" *ngIf="validationDisplay !== 'inline' && warningEntries(s).length > 0">
220
- <li *ngFor="let entry of warningEntries(s)" class="pw-shell__warnings-item">
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
- </div>
256
- <!-- Footercustom or default navigation buttons -->
257
- <ng-template #footerOrCustom>
258
- <ng-container *ngIf="customFooter; else defaultFooter">
259
- <ng-container *ngTemplateOutlet="customFooter.templateRef; context: { $implicit: s, actions: shellActions }"></ng-container>
260
- </ng-container>
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="getResolvedFooterLayout(s) === 'form' && !hideCancel"
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="getResolvedFooterLayout(s) === 'wizard' && !s.isFirstStep"
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="getResolvedFooterLayout(s) === 'wizard' && !hideCancel"
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
- * Footer layout mode:
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() footerLayout: "wizard" | "form" | "auto" = "auto";
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
- this.facade.start(this.path, this.initialData);
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
- /** Resolves "auto" footerLayout based on snapshot. Single-step top-level "form", otherwise → "wizard". */
446
- protected getResolvedFooterLayout(s: PathSnapshot): "wizard" | "form" {
447
- return this.footerLayout === "auto"
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.footerLayout;
536
+ : this.layout;
450
537
  }
451
538
 
452
539
  protected errorPhaseMessage = errorPhaseMessage;