@daltonr/pathwrite-angular 0.2.1 → 0.4.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 CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  Angular `@Injectable` facade over `@daltonr/pathwrite-core`. Exposes path state and events as RxJS observables that work seamlessly with Angular signals, the `async` pipe, and `takeUntilDestroyed`.
4
4
 
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @daltonr/pathwrite-core @daltonr/pathwrite-angular
9
+ ```
10
+
11
+ ## Exported Types
12
+
13
+ For convenience, this package re-exports core types so you don't need to import from `@daltonr/pathwrite-core`:
14
+
15
+ ```typescript
16
+ import {
17
+ PathFacade, // Angular-specific
18
+ PathData, // Re-exported from core
19
+ PathDefinition, // Re-exported from core
20
+ PathEvent, // Re-exported from core
21
+ PathSnapshot, // Re-exported from core
22
+ PathStep, // Re-exported from core
23
+ PathStepContext, // Re-exported from core
24
+ SerializedPathState // Re-exported from core
25
+ } from "@daltonr/pathwrite-angular";
26
+ ```
27
+
28
+ ---
29
+
5
30
  ## Setup
6
31
 
7
32
  Provide `PathFacade` at the component level so each component gets its own isolated path instance, and Angular handles cleanup automatically via `ngOnDestroy`.
@@ -41,7 +66,8 @@ export class MyComponent {
41
66
  | Method | Description |
42
67
  |--------|-------------|
43
68
  | `start(definition, data?)` | Start or re-start a path. |
44
- | `startSubPath(definition, data?)` | Push a sub-path. Requires an active path. |
69
+ | `restart(definition, data?)` | Tear down any active path (without firing hooks) and start the given path fresh. Safe to call at any time. Use for "Start over" / retry flows. |
70
+ | `startSubPath(definition, data?, meta?)` | Push a sub-path. Requires an active path. `meta` is returned unchanged to `onSubPathComplete` / `onSubPathCancel`. |
45
71
  | `next()` | Advance one step. Completes the path on the last step. |
46
72
  | `previous()` | Go back one step. No-op when already on the first step of a top-level path. |
47
73
  | `cancel()` | Cancel the active path (or sub-path). |
@@ -199,7 +225,12 @@ The Angular adapter ships an optional shell component that renders a complete pr
199
225
  The shell lives in a separate entry point so that headless-only usage does not pull in the Angular compiler:
200
226
 
201
227
  ```typescript
202
- import { PathShellComponent, PathStepDirective } from "@daltonr/pathwrite-angular/shell";
228
+ import {
229
+ PathShellComponent,
230
+ PathStepDirective,
231
+ PathShellHeaderDirective,
232
+ PathShellFooterDirective,
233
+ } from "@daltonr/pathwrite-angular/shell";
203
234
  ```
204
235
 
205
236
  ### Usage
@@ -283,6 +314,324 @@ export class MyComponent {
283
314
  | `(cancelled)` | `PathData` | Emitted when the path is cancelled. |
284
315
  | `(pathEvent)` | `PathEvent` | Emitted for every engine event. |
285
316
 
317
+ ### Customising the header and footer
318
+
319
+ Use `pwShellHeader` and `pwShellFooter` directives to replace the built-in progress bar or navigation buttons with your own templates. Both are declared on `<ng-template>` elements inside the shell.
320
+
321
+ **`pwShellHeader`** — receives the current `PathSnapshot` as the implicit template variable:
322
+
323
+ ```typescript
324
+ @Component({
325
+ imports: [PathShellComponent, PathStepDirective, PathShellHeaderDirective],
326
+ template: `
327
+ <pw-shell [path]="myPath">
328
+ <ng-template pwShellHeader let-s>
329
+ <p>Step {{ s.stepIndex + 1 }} of {{ s.stepCount }} — {{ s.stepTitle }}</p>
330
+ </ng-template>
331
+ <ng-template pwStep="details"><app-details-form /></ng-template>
332
+ <ng-template pwStep="review"><app-review-panel /></ng-template>
333
+ </pw-shell>
334
+ `
335
+ })
336
+ export class MyComponent { ... }
337
+ ```
338
+
339
+ **`pwShellFooter`** — receives the snapshot as the implicit variable and an `actions` variable with all navigation callbacks:
340
+
341
+ ```typescript
342
+ @Component({
343
+ imports: [PathShellComponent, PathStepDirective, PathShellFooterDirective],
344
+ template: `
345
+ <pw-shell [path]="myPath">
346
+ <ng-template pwShellFooter let-s let-actions="actions">
347
+ <button (click)="actions.previous()" [disabled]="s.isFirstStep || s.isNavigating">Back</button>
348
+ <button (click)="actions.next()" [disabled]="!s.canMoveNext || s.isNavigating">
349
+ {{ s.isLastStep ? 'Finish' : 'Next' }}
350
+ </button>
351
+ </ng-template>
352
+ <ng-template pwStep="details"><app-details-form /></ng-template>
353
+ </pw-shell>
354
+ `
355
+ })
356
+ export class MyComponent { ... }
357
+ ```
358
+
359
+ `actions` (`PathShellActions`) contains: `next`, `previous`, `cancel`, `goToStep`, `goToStepChecked`, `setData`, `restart`. All return `Promise<void>`.
360
+
361
+ `restart()` restarts the shell's own `[path]` input with its own `[initialData]` input — useful for a "Start over" button in a custom footer.
362
+
363
+ Both directives can be combined. Only the sections you override are replaced — a custom header still shows the default footer, and vice versa.
364
+
365
+ ---
366
+
367
+ ## Sub-Paths
368
+
369
+ Sub-paths allow you to nest multi-step workflows. Common use cases include:
370
+ - Running a child workflow per collection item (e.g., approve each document)
371
+ - Conditional drill-down flows (e.g., "Add payment method" modal)
372
+ - Reusable wizard components
373
+
374
+ ### Basic Sub-Path Flow
375
+
376
+ When a sub-path is active:
377
+ - The shell switches to show the sub-path's steps
378
+ - The progress bar displays sub-path steps (not main path steps)
379
+ - Pressing Back on the first sub-path step **cancels** the sub-path and returns to the parent
380
+ - The `PathFacade` (and thus `state$`, `stateSignal`) reflects the **sub-path** snapshot, not the parent's
381
+
382
+ ### Complete Example: Approver Collection
383
+
384
+ ```typescript
385
+ import { PathData, PathDefinition, PathFacade } from "@daltonr/pathwrite-angular";
386
+
387
+ // Sub-path data shape
388
+ interface ApproverReviewData extends PathData {
389
+ decision: "approve" | "reject" | "";
390
+ comments: string;
391
+ }
392
+
393
+ // Main path data shape
394
+ interface ApprovalWorkflowData extends PathData {
395
+ documentTitle: string;
396
+ approvers: string[];
397
+ approvals: Array<{ approver: string; decision: string; comments: string }>;
398
+ }
399
+
400
+ // Define the sub-path (approver review wizard)
401
+ const approverReviewPath: PathDefinition<ApproverReviewData> = {
402
+ id: "approver-review",
403
+ steps: [
404
+ { id: "review", title: "Review Document" },
405
+ {
406
+ id: "decision",
407
+ title: "Make Decision",
408
+ canMoveNext: ({ data }) =>
409
+ data.decision === "approve" || data.decision === "reject",
410
+ validationMessages: ({ data }) =>
411
+ !data.decision ? ["Please select Approve or Reject"] : []
412
+ },
413
+ { id: "comments", title: "Add Comments" }
414
+ ]
415
+ };
416
+
417
+ // Define the main path
418
+ const approvalWorkflowPath: PathDefinition<ApprovalWorkflowData> = {
419
+ id: "approval-workflow",
420
+ steps: [
421
+ {
422
+ id: "setup",
423
+ title: "Setup Approval",
424
+ canMoveNext: ({ data }) =>
425
+ (data.documentTitle ?? "").trim().length > 0 &&
426
+ data.approvers.length > 0
427
+ },
428
+ {
429
+ id: "run-approvals",
430
+ title: "Collect Approvals",
431
+ // Block "Next" until all approvers have completed their reviews
432
+ canMoveNext: ({ data }) =>
433
+ data.approvals.length === data.approvers.length,
434
+ validationMessages: ({ data }) => {
435
+ const remaining = data.approvers.length - data.approvals.length;
436
+ return remaining > 0
437
+ ? [`${remaining} approver(s) pending review`]
438
+ : [];
439
+ },
440
+ // When an approver finishes their sub-path, record the result
441
+ onSubPathComplete(subPathId, subPathData, ctx, meta) {
442
+ const approverName = meta?.approverName as string;
443
+ const result = subPathData as ApproverReviewData;
444
+ return {
445
+ approvals: [
446
+ ...ctx.data.approvals,
447
+ {
448
+ approver: approverName,
449
+ decision: result.decision,
450
+ comments: result.comments
451
+ }
452
+ ]
453
+ };
454
+ },
455
+ // If an approver cancels (presses Back on first step), you can track it
456
+ onSubPathCancel(subPathId, subPathData, ctx, meta) {
457
+ console.log(`${meta?.approverName} cancelled their review`);
458
+ // Optionally return data changes, or just log
459
+ }
460
+ },
461
+ { id: "summary", title: "Summary" }
462
+ ]
463
+ };
464
+
465
+ // Component
466
+ @Component({
467
+ selector: 'app-approval-workflow',
468
+ standalone: true,
469
+ imports: [PathShellComponent, PathStepDirective],
470
+ providers: [PathFacade],
471
+ template: `
472
+ <pw-shell [path]="approvalWorkflowPath" [initialData]="initialData">
473
+ <!-- Main path steps -->
474
+ <ng-template pwStep="setup">
475
+ <input [(ngModel)]="facade.snapshot()!.data.documentTitle" placeholder="Document title" />
476
+ <!-- approver selection UI here -->
477
+ </ng-template>
478
+
479
+ <ng-template pwStep="run-approvals">
480
+ <h3>Approvers</h3>
481
+ <ul>
482
+ @for (approver of facade.snapshot()!.data.approvers; track $index) {
483
+ <li>
484
+ {{ approver }}
485
+ @if (!hasApproval(approver)) {
486
+ <button (click)="launchReviewForApprover(approver, $index)">
487
+ Start Review
488
+ </button>
489
+ } @else {
490
+ <span>✓ {{ getApproval(approver)?.decision }}</span>
491
+ }
492
+ </li>
493
+ }
494
+ </ul>
495
+ </ng-template>
496
+
497
+ <ng-template pwStep="summary">
498
+ <h3>All Approvals Collected</h3>
499
+ <ul>
500
+ @for (approval of facade.snapshot()!.data.approvals; track approval.approver) {
501
+ <li>{{ approval.approver }}: {{ approval.decision }}</li>
502
+ }
503
+ </ul>
504
+ </ng-template>
505
+
506
+ <!-- Sub-path steps (must be co-located in the same pw-shell) -->
507
+ <ng-template pwStep="review">
508
+ <p>Review the document: "{{ facade.snapshot()!.data.documentTitle }}"</p>
509
+ </ng-template>
510
+
511
+ <ng-template pwStep="decision">
512
+ <label><input type="radio" value="approve" [(ngModel)]="facade.snapshot()!.data.decision" /> Approve</label>
513
+ <label><input type="radio" value="reject" [(ngModel)]="facade.snapshot()!.data.decision" /> Reject</label>
514
+ </ng-template>
515
+
516
+ <ng-template pwStep="comments">
517
+ <textarea [(ngModel)]="facade.snapshot()!.data.comments" placeholder="Optional comments"></textarea>
518
+ </ng-template>
519
+ </pw-shell>
520
+ `
521
+ })
522
+ export class ApprovalWorkflowComponent {
523
+ protected readonly facade = inject(PathFacade) as PathFacade<ApprovalWorkflowData>;
524
+ protected readonly approvalWorkflowPath = approvalWorkflowPath;
525
+ protected readonly initialData = { documentTitle: '', approvers: [], approvals: [] };
526
+
527
+ protected launchReviewForApprover(approverName: string, index: number): void {
528
+ // Pass correlation data via `meta` — it's echoed back to onSubPathComplete
529
+ void this.facade.startSubPath(
530
+ approverReviewPath,
531
+ { decision: "", comments: "" },
532
+ { approverName, approverIndex: index }
533
+ );
534
+ }
535
+
536
+ protected hasApproval(approver: string): boolean {
537
+ return this.facade.snapshot()!.data.approvals.some(a => a.approver === approver);
538
+ }
539
+
540
+ protected getApproval(approver: string) {
541
+ return this.facade.snapshot()!.data.approvals.find(a => a.approver === approver);
542
+ }
543
+ }
544
+ ```
545
+
546
+ ### Key Notes
547
+
548
+ **1. Sub-path steps must be co-located with main path steps**
549
+ All `pwStep` templates (main path + sub-path steps) live in the same `<pw-shell>`. When a sub-path is active, the shell renders the sub-path's step templates. This means:
550
+ - Parent and sub-path step IDs **must not collide** (e.g., don't use `summary` in both)
551
+ - The shell matches step IDs from the current path only (main or sub), but all templates are registered globally
552
+
553
+ **2. The `meta` correlation field**
554
+ `startSubPath` accepts an optional third argument (`meta`) that is returned unchanged to `onSubPathComplete` and `onSubPathCancel`. Use it to correlate which collection item triggered the sub-path:
555
+
556
+ ```typescript
557
+ facade.startSubPath(subPath, initialData, { itemIndex: 3, itemId: "abc" });
558
+
559
+ // In the parent step:
560
+ onSubPathComplete(subPathId, subPathData, ctx, meta) {
561
+ const itemIndex = meta?.itemIndex; // 3
562
+ }
563
+ ```
564
+
565
+ **3. Progress bar switches during sub-paths**
566
+ When `snapshot.nestingLevel > 0`, you're in a sub-path. The `steps` array in the snapshot contains the sub-path's steps, not the main path's. The default PathShell progress bar shows sub-path progress. You can check `nestingLevel` to show a breadcrumb or "back to main flow" indicator.
567
+
568
+ **4. Accessing parent path data from sub-path components**
569
+ There is currently no way to inject a "parent facade" in sub-path step components. If a sub-path step needs parent data (e.g., the document title), pass it via `initialData` when calling `startSubPath`:
570
+
571
+ ```typescript
572
+ facade.startSubPath(approverReviewPath, {
573
+ decision: "",
574
+ comments: "",
575
+ documentTitle: facade.snapshot()!.data.documentTitle // copy from parent
576
+ });
577
+ ```
578
+
579
+ ---
580
+
581
+ ## Guards and Lifecycle Hooks
582
+
583
+ ### Defensive Guards (Important!)
584
+
585
+ **Guards and `validationMessages` are evaluated *before* `onEnter` runs on first entry.**
586
+
587
+ If you access fields in a guard that `onEnter` is supposed to initialize, the guard will throw a `TypeError` on startup. Write guards defensively using nullish coalescing:
588
+
589
+ ```typescript
590
+ // ✗ Unsafe — crashes if data.name is undefined
591
+ canMoveNext: ({ data }) => data.name.trim().length > 0
592
+
593
+ // ✓ Safe — handles undefined gracefully
594
+ canMoveNext: ({ data }) => (data.name ?? "").trim().length > 0
595
+ ```
596
+
597
+ Alternatively, pass `initialData` to `start()` / `<pw-shell>` so all fields are present from the first snapshot:
598
+
599
+ ```typescript
600
+ <pw-shell [path]="myPath" [initialData]="{ name: '', age: 0 }" />
601
+ ```
602
+
603
+ If a guard throws, the engine catches it, logs a warning, and returns `true` (allow navigation) as a safe default.
604
+
605
+ ### Async Guards and Validation Messages
606
+
607
+ Guards and `validationMessages` must be **synchronous** for inclusion in snapshots. Async functions are detected and warned about:
608
+ - Async `canMoveNext` / `canMovePrevious` default to `true` (optimistic)
609
+ - Async `validationMessages` default to `[]`
610
+
611
+ The async version is still enforced during actual navigation (when you call `next()` / `previous()`), but the snapshot won't reflect the pending state. If you need async validation, perform it in the guard and store the result in `data` so the guard can read it synchronously.
612
+
613
+ ### `isFirstEntry` Flag
614
+
615
+ The `PathStepContext` passed to all hooks includes an `isFirstEntry: boolean` flag. It's `true` the first time a step is visited, `false` on re-entry (e.g., after navigating back then forward again).
616
+
617
+ Use it to distinguish initialization from re-entry:
618
+
619
+ ```typescript
620
+ {
621
+ id: "details",
622
+ onEnter: ({ isFirstEntry, data }) => {
623
+ if (isFirstEntry) {
624
+ // Only pre-fill on first visit, not when returning via Back
625
+ return { name: "Default Name" };
626
+ }
627
+ }
628
+ }
629
+ ```
630
+
631
+ **Important:** `onEnter` fires every time you enter the step. If you want "initialize once" behavior, either:
632
+ 1. Use `isFirstEntry` to conditionally return data
633
+ 2. Provide `initialData` to `start()` instead of using `onEnter`
634
+
286
635
  ---
287
636
 
288
637
  ## Styling
@@ -314,3 +663,36 @@ Override any `--pw-*` variable to customise the appearance:
314
663
  --pw-shell-radius: 12px;
315
664
  }
316
665
  ```
666
+
667
+ ### Available CSS Custom Properties
668
+
669
+ **Layout:**
670
+ - `--pw-shell-max-width` — Maximum width of the shell (default: `720px`)
671
+ - `--pw-shell-padding` — Internal padding (default: `24px`)
672
+ - `--pw-shell-gap` — Gap between header, body, footer (default: `20px`)
673
+ - `--pw-shell-radius` — Border radius for cards (default: `10px`)
674
+
675
+ **Colors:**
676
+ - `--pw-color-bg` — Background color (default: `#ffffff`)
677
+ - `--pw-color-border` — Border color (default: `#dbe4f0`)
678
+ - `--pw-color-text` — Primary text color (default: `#1f2937`)
679
+ - `--pw-color-muted` — Muted text color (default: `#5b677a`)
680
+ - `--pw-color-primary` — Primary/accent color (default: `#2563eb`)
681
+ - `--pw-color-primary-light` — Light primary for backgrounds (default: `rgba(37, 99, 235, 0.12)`)
682
+ - `--pw-color-btn-bg` — Button background (default: `#f8fbff`)
683
+ - `--pw-color-btn-border` — Button border (default: `#c2d0e5`)
684
+
685
+ **Validation:**
686
+ - `--pw-color-error` — Error text color (default: `#dc2626`)
687
+ - `--pw-color-error-bg` — Error background (default: `#fef2f2`)
688
+ - `--pw-color-error-border` — Error border (default: `#fecaca`)
689
+
690
+ **Progress Indicator:**
691
+ - `--pw-dot-size` — Step dot size (default: `32px`)
692
+ - `--pw-dot-font-size` — Font size inside dots (default: `13px`)
693
+ - `--pw-track-height` — Progress track height (default: `4px`)
694
+
695
+ **Buttons:**
696
+ - `--pw-btn-padding` — Button padding (default: `8px 16px`)
697
+ - `--pw-btn-radius` — Button border radius (default: `6px`)
698
+
package/dist/index.css CHANGED
@@ -250,6 +250,16 @@
250
250
  border-color: #1d4ed8;
251
251
  }
252
252
 
253
+ .pw-shell__btn--back {
254
+ background: transparent;
255
+ border-color: var(--pw-color-primary);
256
+ color: var(--pw-color-primary);
257
+ }
258
+
259
+ .pw-shell__btn--back:hover:not(:disabled) {
260
+ background: var(--pw-color-primary-light);
261
+ }
262
+
253
263
  .pw-shell__btn--cancel {
254
264
  color: var(--pw-color-muted);
255
265
  border-color: transparent;
package/dist/index.d.ts CHANGED
@@ -29,7 +29,14 @@ export declare class PathFacade<TData extends PathData = PathData> implements On
29
29
  constructor();
30
30
  ngOnDestroy(): void;
31
31
  start(path: PathDefinition<any>, initialData?: PathData): Promise<void>;
32
- startSubPath(path: PathDefinition<any>, initialData?: PathData): Promise<void>;
32
+ /**
33
+ * Tears down any active path (without firing lifecycle hooks) and immediately
34
+ * starts the given path fresh. Safe to call whether or not a path is running.
35
+ * Use for "Start over" / retry flows without destroying and re-creating the
36
+ * component that provides this facade.
37
+ */
38
+ restart(path: PathDefinition<any>, initialData?: PathData): Promise<void>;
39
+ startSubPath(path: PathDefinition<any>, initialData?: PathData, meta?: Record<string, unknown>): Promise<void>;
33
40
  next(): Promise<void>;
34
41
  previous(): Promise<void>;
35
42
  cancel(): Promise<void>;
@@ -84,3 +91,4 @@ export interface FormGroupLike {
84
91
  * ```
85
92
  */
86
93
  export declare function syncFormGroup<TData extends PathData = PathData>(facade: PathFacade<TData>, formGroup: FormGroupLike, destroyRef?: DestroyRef): () => void;
94
+ export type { PathData, PathDefinition, PathEvent, PathSnapshot, PathStep, PathStepContext, SerializedPathState } from "@daltonr/pathwrite-core";
package/dist/index.js CHANGED
@@ -46,8 +46,17 @@ export class PathFacade {
46
46
  start(path, initialData = {}) {
47
47
  return this.engine.start(path, initialData);
48
48
  }
49
- startSubPath(path, initialData = {}) {
50
- return this.engine.startSubPath(path, initialData);
49
+ /**
50
+ * Tears down any active path (without firing lifecycle hooks) and immediately
51
+ * starts the given path fresh. Safe to call whether or not a path is running.
52
+ * Use for "Start over" / retry flows without destroying and re-creating the
53
+ * component that provides this facade.
54
+ */
55
+ restart(path, initialData = {}) {
56
+ return this.engine.restart(path, initialData);
57
+ }
58
+ startSubPath(path, initialData = {}, meta) {
59
+ return this.engine.startSubPath(path, initialData, meta);
51
60
  }
52
61
  next() {
53
62
  return this.engine.next();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/shell.d.ts CHANGED
@@ -1,7 +1,22 @@
1
1
  import { TemplateRef, EventEmitter, QueryList, OnInit, OnDestroy, Injector } from "@angular/core";
2
- import { PathData, PathDefinition, PathEvent } from "@daltonr/pathwrite-core";
2
+ import { PathData, PathDefinition, PathEvent, PathSnapshot } from "@daltonr/pathwrite-core";
3
3
  import { PathFacade } from "./index";
4
4
  import * as i0 from "@angular/core";
5
+ /**
6
+ * Navigation actions passed as template context to custom `pwShellFooter`
7
+ * templates. Mirrors what React's `renderFooter` and Vue's `#footer` slot
8
+ * receive, using promises so it is consistent with the Angular facade.
9
+ */
10
+ export interface PathShellActions {
11
+ next: () => Promise<void>;
12
+ previous: () => Promise<void>;
13
+ cancel: () => Promise<void>;
14
+ goToStep: (stepId: string) => Promise<void>;
15
+ goToStepChecked: (stepId: string) => Promise<void>;
16
+ setData: (key: string, value: unknown) => Promise<void>;
17
+ /** Restart the shell's current path with its current `initialData`. */
18
+ restart: () => Promise<void>;
19
+ }
5
20
  /**
6
21
  * Structural directive that associates a template with a step ID.
7
22
  * Used inside `<pw-shell>` to define per-step content.
@@ -20,6 +35,56 @@ export declare class PathStepDirective {
20
35
  static ɵfac: i0.ɵɵFactoryDeclaration<PathStepDirective, never>;
21
36
  static ɵdir: i0.ɵɵDirectiveDeclaration<PathStepDirective, "[pwStep]", never, { "stepId": { "alias": "pwStep"; "required": true; }; }, {}, never, never, true, never>;
22
37
  }
38
+ /**
39
+ * Replaces the default progress header inside `<pw-shell>`.
40
+ * The template receives the current `PathSnapshot` as the implicit context.
41
+ *
42
+ * ```html
43
+ * <pw-shell [path]="myPath">
44
+ * <ng-template pwShellHeader let-s>
45
+ * <my-custom-progress [snapshot]="s" />
46
+ * </ng-template>
47
+ * <ng-template pwStep="details"><app-details-form /></ng-template>
48
+ * </pw-shell>
49
+ * ```
50
+ */
51
+ export declare class PathShellHeaderDirective {
52
+ readonly templateRef: TemplateRef<{
53
+ $implicit: PathSnapshot;
54
+ }>;
55
+ constructor(templateRef: TemplateRef<{
56
+ $implicit: PathSnapshot;
57
+ }>);
58
+ static ɵfac: i0.ɵɵFactoryDeclaration<PathShellHeaderDirective, never>;
59
+ static ɵdir: i0.ɵɵDirectiveDeclaration<PathShellHeaderDirective, "[pwShellHeader]", never, {}, {}, never, never, true, never>;
60
+ }
61
+ /**
62
+ * Replaces the default navigation footer inside `<pw-shell>`.
63
+ * The template receives the current `PathSnapshot` as the implicit context
64
+ * and a `actions` variable containing all navigation actions.
65
+ *
66
+ * ```html
67
+ * <pw-shell [path]="myPath">
68
+ * <ng-template pwShellFooter let-s let-actions="actions">
69
+ * <button (click)="actions.previous()" [disabled]="s.isFirstStep">Back</button>
70
+ * <button (click)="actions.next()" [disabled]="!s.canMoveNext">Next</button>
71
+ * </ng-template>
72
+ * <ng-template pwStep="details"><app-details-form /></ng-template>
73
+ * </pw-shell>
74
+ * ```
75
+ */
76
+ export declare class PathShellFooterDirective {
77
+ readonly templateRef: TemplateRef<{
78
+ $implicit: PathSnapshot;
79
+ actions: PathShellActions;
80
+ }>;
81
+ constructor(templateRef: TemplateRef<{
82
+ $implicit: PathSnapshot;
83
+ actions: PathShellActions;
84
+ }>);
85
+ static ɵfac: i0.ɵɵFactoryDeclaration<PathShellFooterDirective, never>;
86
+ static ɵdir: i0.ɵɵDirectiveDeclaration<PathShellFooterDirective, "[pwShellFooter]", never, {}, {}, never, never, true, never>;
87
+ }
23
88
  /**
24
89
  * Default UI shell component. Renders a progress indicator, step content,
25
90
  * and navigation buttons.
@@ -54,15 +119,19 @@ export declare class PathShellComponent implements OnInit, OnDestroy {
54
119
  cancelled: EventEmitter<PathData>;
55
120
  pathEvent: EventEmitter<PathEvent>;
56
121
  stepDirectives: QueryList<PathStepDirective>;
122
+ customHeader?: PathShellHeaderDirective;
123
+ customFooter?: PathShellFooterDirective;
57
124
  readonly facade: PathFacade<PathData>;
58
125
  /** The shell's own component-level injector. Passed to ngTemplateOutlet so that
59
126
  * step components can resolve PathFacade (provided by this shell) via inject(). */
60
127
  protected readonly shellInjector: Injector;
61
128
  started: boolean;
129
+ /** Navigation actions passed to custom `pwShellFooter` templates. */
130
+ protected readonly shellActions: PathShellActions;
62
131
  private readonly destroy$;
63
132
  ngOnInit(): void;
64
133
  ngOnDestroy(): void;
65
134
  doStart(): void;
66
135
  static ɵfac: i0.ɵɵFactoryDeclaration<PathShellComponent, never>;
67
- static ɵcmp: i0.ɵɵComponentDeclaration<PathShellComponent, "pw-shell", never, { "path": { "alias": "path"; "required": true; }; "initialData": { "alias": "initialData"; "required": false; }; "autoStart": { "alias": "autoStart"; "required": false; }; "backLabel": { "alias": "backLabel"; "required": false; }; "nextLabel": { "alias": "nextLabel"; "required": false; }; "finishLabel": { "alias": "finishLabel"; "required": false; }; "cancelLabel": { "alias": "cancelLabel"; "required": false; }; "hideCancel": { "alias": "hideCancel"; "required": false; }; "hideProgress": { "alias": "hideProgress"; "required": false; }; }, { "completed": "completed"; "cancelled": "cancelled"; "pathEvent": "pathEvent"; }, ["stepDirectives"], never, true, never>;
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; }; "finishLabel": { "alias": "finishLabel"; "required": false; }; "cancelLabel": { "alias": "cancelLabel"; "required": false; }; "hideCancel": { "alias": "hideCancel"; "required": false; }; "hideProgress": { "alias": "hideProgress"; "required": false; }; }, { "completed": "completed"; "cancelled": "cancelled"; "pathEvent": "pathEvent"; }, ["customHeader", "customFooter", "stepDirectives"], never, true, never>;
68
137
  }
package/dist/shell.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Component, Directive, Input, Output, EventEmitter, ContentChildren, inject, Injector, ChangeDetectionStrategy } from "@angular/core";
1
+ import { Component, Directive, Input, Output, EventEmitter, ContentChild, ContentChildren, inject, Injector, ChangeDetectionStrategy } from "@angular/core";
2
2
  import { CommonModule } from "@angular/common";
3
3
  import { Subject } from "rxjs";
4
4
  import { takeUntil } from "rxjs/operators";
@@ -34,6 +34,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
34
34
  args: [{ required: true, alias: "pwStep" }]
35
35
  }] } });
36
36
  // ---------------------------------------------------------------------------
37
+ // PathShellHeaderDirective
38
+ // ---------------------------------------------------------------------------
39
+ /**
40
+ * Replaces the default progress header inside `<pw-shell>`.
41
+ * The template receives the current `PathSnapshot` as the implicit context.
42
+ *
43
+ * ```html
44
+ * <pw-shell [path]="myPath">
45
+ * <ng-template pwShellHeader let-s>
46
+ * <my-custom-progress [snapshot]="s" />
47
+ * </ng-template>
48
+ * <ng-template pwStep="details"><app-details-form /></ng-template>
49
+ * </pw-shell>
50
+ * ```
51
+ */
52
+ export class PathShellHeaderDirective {
53
+ constructor(templateRef) {
54
+ this.templateRef = templateRef;
55
+ }
56
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathShellHeaderDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); }
57
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: PathShellHeaderDirective, isStandalone: true, selector: "[pwShellHeader]", ngImport: i0 }); }
58
+ }
59
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathShellHeaderDirective, decorators: [{
60
+ type: Directive,
61
+ args: [{ selector: "[pwShellHeader]", standalone: true }]
62
+ }], ctorParameters: () => [{ type: i0.TemplateRef }] });
63
+ // ---------------------------------------------------------------------------
64
+ // PathShellFooterDirective
65
+ // ---------------------------------------------------------------------------
66
+ /**
67
+ * Replaces the default navigation footer inside `<pw-shell>`.
68
+ * The template receives the current `PathSnapshot` as the implicit context
69
+ * and a `actions` variable containing all navigation actions.
70
+ *
71
+ * ```html
72
+ * <pw-shell [path]="myPath">
73
+ * <ng-template pwShellFooter let-s let-actions="actions">
74
+ * <button (click)="actions.previous()" [disabled]="s.isFirstStep">Back</button>
75
+ * <button (click)="actions.next()" [disabled]="!s.canMoveNext">Next</button>
76
+ * </ng-template>
77
+ * <ng-template pwStep="details"><app-details-form /></ng-template>
78
+ * </pw-shell>
79
+ * ```
80
+ */
81
+ export class PathShellFooterDirective {
82
+ constructor(templateRef) {
83
+ this.templateRef = templateRef;
84
+ }
85
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathShellFooterDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); }
86
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: PathShellFooterDirective, isStandalone: true, selector: "[pwShellFooter]", ngImport: i0 }); }
87
+ }
88
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathShellFooterDirective, decorators: [{
89
+ type: Directive,
90
+ args: [{ selector: "[pwShellFooter]", standalone: true }]
91
+ }], ctorParameters: () => [{ type: i0.TemplateRef }] });
92
+ // ---------------------------------------------------------------------------
37
93
  // PathShellComponent
38
94
  // ---------------------------------------------------------------------------
39
95
  /**
@@ -73,6 +129,16 @@ export class PathShellComponent {
73
129
  * step components can resolve PathFacade (provided by this shell) via inject(). */
74
130
  this.shellInjector = inject(Injector);
75
131
  this.started = false;
132
+ /** Navigation actions passed to custom `pwShellFooter` templates. */
133
+ this.shellActions = {
134
+ next: () => this.facade.next(),
135
+ previous: () => this.facade.previous(),
136
+ cancel: () => this.facade.cancel(),
137
+ goToStep: (id) => this.facade.goToStep(id),
138
+ goToStepChecked: (id) => this.facade.goToStepChecked(id),
139
+ setData: (key, value) => this.facade.setData(key, value),
140
+ restart: () => this.facade.restart(this.path, this.initialData),
141
+ };
76
142
  this.destroy$ = new Subject();
77
143
  }
78
144
  ngOnInit() {
@@ -96,7 +162,7 @@ export class PathShellComponent {
96
162
  this.facade.start(this.path, this.initialData);
97
163
  }
98
164
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
99
- 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", finishLabel: "finishLabel", cancelLabel: "cancelLabel", hideCancel: "hideCancel", hideProgress: "hideProgress" }, outputs: { completed: "completed", cancelled: "cancelled", pathEvent: "pathEvent" }, providers: [PathFacade], queries: [{ propertyName: "stepDirectives", predicate: PathStepDirective }], ngImport: i0, template: `
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", finishLabel: "finishLabel", 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: `
100
166
  <!-- Empty state -->
101
167
  <div class="pw-shell" *ngIf="!(facade.state$ | async)">
102
168
  <div class="pw-shell__empty" *ngIf="!started">
@@ -107,22 +173,27 @@ export class PathShellComponent {
107
173
 
108
174
  <!-- Active path -->
109
175
  <div class="pw-shell" *ngIf="facade.state$ | async as s">
110
- <!-- Header — progress indicator -->
111
- <div class="pw-shell__header" *ngIf="!hideProgress">
112
- <div class="pw-shell__steps">
113
- <div
114
- *ngFor="let step of s.steps; let i = index"
115
- class="pw-shell__step"
116
- [ngClass]="'pw-shell__step--' + step.status"
117
- >
118
- <span class="pw-shell__step-dot">{{ step.status === 'completed' ? '✓' : (i + 1) }}</span>
119
- <span class="pw-shell__step-label">{{ step.title ?? step.id }}</span>
176
+ <!-- Header — custom or default progress indicator -->
177
+ <ng-container *ngIf="customHeader; else defaultHeader">
178
+ <ng-container *ngTemplateOutlet="customHeader.templateRef; context: { $implicit: s }"></ng-container>
179
+ </ng-container>
180
+ <ng-template #defaultHeader>
181
+ <div class="pw-shell__header" *ngIf="!hideProgress">
182
+ <div class="pw-shell__steps">
183
+ <div
184
+ *ngFor="let step of s.steps; let i = index"
185
+ class="pw-shell__step"
186
+ [ngClass]="'pw-shell__step--' + step.status"
187
+ >
188
+ <span class="pw-shell__step-dot">{{ step.status === 'completed' ? '✓' : (i + 1) }}</span>
189
+ <span class="pw-shell__step-label">{{ step.title ?? step.id }}</span>
190
+ </div>
191
+ </div>
192
+ <div class="pw-shell__track">
193
+ <div class="pw-shell__track-fill" [style.width.%]="s.progress * 100"></div>
120
194
  </div>
121
195
  </div>
122
- <div class="pw-shell__track">
123
- <div class="pw-shell__track-fill" [style.width.%]="s.progress * 100"></div>
124
- </div>
125
- </div>
196
+ </ng-template>
126
197
 
127
198
  <!-- Body — step content -->
128
199
  <div class="pw-shell__body">
@@ -138,33 +209,38 @@ export class PathShellComponent {
138
209
  <li *ngFor="let msg of s.validationMessages" class="pw-shell__validation-item">{{ msg }}</li>
139
210
  </ul>
140
211
 
141
- <!-- Footer — navigation buttons -->
142
- <div class="pw-shell__footer">
143
- <div class="pw-shell__footer-left">
144
- <button
145
- *ngIf="!s.isFirstStep"
146
- type="button"
147
- class="pw-shell__btn pw-shell__btn--back"
148
- [disabled]="s.isNavigating || !s.canMovePrevious"
149
- (click)="facade.previous()"
150
- >{{ backLabel }}</button>
151
- </div>
152
- <div class="pw-shell__footer-right">
153
- <button
154
- *ngIf="!hideCancel"
155
- type="button"
156
- class="pw-shell__btn pw-shell__btn--cancel"
157
- [disabled]="s.isNavigating"
158
- (click)="facade.cancel()"
159
- >{{ cancelLabel }}</button>
160
- <button
161
- type="button"
162
- class="pw-shell__btn pw-shell__btn--next"
163
- [disabled]="s.isNavigating || !s.canMoveNext"
164
- (click)="facade.next()"
165
- >{{ s.isLastStep ? finishLabel : nextLabel }}</button>
212
+ <!-- Footer — custom or default navigation buttons -->
213
+ <ng-container *ngIf="customFooter; else defaultFooter">
214
+ <ng-container *ngTemplateOutlet="customFooter.templateRef; context: { $implicit: s, actions: shellActions }"></ng-container>
215
+ </ng-container>
216
+ <ng-template #defaultFooter>
217
+ <div class="pw-shell__footer">
218
+ <div class="pw-shell__footer-left">
219
+ <button
220
+ *ngIf="!s.isFirstStep"
221
+ type="button"
222
+ class="pw-shell__btn pw-shell__btn--back"
223
+ [disabled]="s.isNavigating || !s.canMovePrevious"
224
+ (click)="facade.previous()"
225
+ >{{ backLabel }}</button>
226
+ </div>
227
+ <div class="pw-shell__footer-right">
228
+ <button
229
+ *ngIf="!hideCancel"
230
+ type="button"
231
+ class="pw-shell__btn pw-shell__btn--cancel"
232
+ [disabled]="s.isNavigating"
233
+ (click)="facade.cancel()"
234
+ >{{ cancelLabel }}</button>
235
+ <button
236
+ type="button"
237
+ class="pw-shell__btn pw-shell__btn--next"
238
+ [disabled]="s.isNavigating || !s.canMoveNext"
239
+ (click)="facade.next()"
240
+ >{{ s.isLastStep ? finishLabel : nextLabel }}</button>
241
+ </div>
166
242
  </div>
167
- </div>
243
+ </ng-template>
168
244
  </div>
169
245
  `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.Default }); }
170
246
  }
@@ -187,22 +263,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
187
263
 
188
264
  <!-- Active path -->
189
265
  <div class="pw-shell" *ngIf="facade.state$ | async as s">
190
- <!-- Header — progress indicator -->
191
- <div class="pw-shell__header" *ngIf="!hideProgress">
192
- <div class="pw-shell__steps">
193
- <div
194
- *ngFor="let step of s.steps; let i = index"
195
- class="pw-shell__step"
196
- [ngClass]="'pw-shell__step--' + step.status"
197
- >
198
- <span class="pw-shell__step-dot">{{ step.status === 'completed' ? '✓' : (i + 1) }}</span>
199
- <span class="pw-shell__step-label">{{ step.title ?? step.id }}</span>
266
+ <!-- Header — custom or default progress indicator -->
267
+ <ng-container *ngIf="customHeader; else defaultHeader">
268
+ <ng-container *ngTemplateOutlet="customHeader.templateRef; context: { $implicit: s }"></ng-container>
269
+ </ng-container>
270
+ <ng-template #defaultHeader>
271
+ <div class="pw-shell__header" *ngIf="!hideProgress">
272
+ <div class="pw-shell__steps">
273
+ <div
274
+ *ngFor="let step of s.steps; let i = index"
275
+ class="pw-shell__step"
276
+ [ngClass]="'pw-shell__step--' + step.status"
277
+ >
278
+ <span class="pw-shell__step-dot">{{ step.status === 'completed' ? '✓' : (i + 1) }}</span>
279
+ <span class="pw-shell__step-label">{{ step.title ?? step.id }}</span>
280
+ </div>
281
+ </div>
282
+ <div class="pw-shell__track">
283
+ <div class="pw-shell__track-fill" [style.width.%]="s.progress * 100"></div>
200
284
  </div>
201
285
  </div>
202
- <div class="pw-shell__track">
203
- <div class="pw-shell__track-fill" [style.width.%]="s.progress * 100"></div>
204
- </div>
205
- </div>
286
+ </ng-template>
206
287
 
207
288
  <!-- Body — step content -->
208
289
  <div class="pw-shell__body">
@@ -218,33 +299,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
218
299
  <li *ngFor="let msg of s.validationMessages" class="pw-shell__validation-item">{{ msg }}</li>
219
300
  </ul>
220
301
 
221
- <!-- Footer — navigation buttons -->
222
- <div class="pw-shell__footer">
223
- <div class="pw-shell__footer-left">
224
- <button
225
- *ngIf="!s.isFirstStep"
226
- type="button"
227
- class="pw-shell__btn pw-shell__btn--back"
228
- [disabled]="s.isNavigating || !s.canMovePrevious"
229
- (click)="facade.previous()"
230
- >{{ backLabel }}</button>
231
- </div>
232
- <div class="pw-shell__footer-right">
233
- <button
234
- *ngIf="!hideCancel"
235
- type="button"
236
- class="pw-shell__btn pw-shell__btn--cancel"
237
- [disabled]="s.isNavigating"
238
- (click)="facade.cancel()"
239
- >{{ cancelLabel }}</button>
240
- <button
241
- type="button"
242
- class="pw-shell__btn pw-shell__btn--next"
243
- [disabled]="s.isNavigating || !s.canMoveNext"
244
- (click)="facade.next()"
245
- >{{ s.isLastStep ? finishLabel : nextLabel }}</button>
302
+ <!-- Footer — custom or default navigation buttons -->
303
+ <ng-container *ngIf="customFooter; else defaultFooter">
304
+ <ng-container *ngTemplateOutlet="customFooter.templateRef; context: { $implicit: s, actions: shellActions }"></ng-container>
305
+ </ng-container>
306
+ <ng-template #defaultFooter>
307
+ <div class="pw-shell__footer">
308
+ <div class="pw-shell__footer-left">
309
+ <button
310
+ *ngIf="!s.isFirstStep"
311
+ type="button"
312
+ class="pw-shell__btn pw-shell__btn--back"
313
+ [disabled]="s.isNavigating || !s.canMovePrevious"
314
+ (click)="facade.previous()"
315
+ >{{ backLabel }}</button>
316
+ </div>
317
+ <div class="pw-shell__footer-right">
318
+ <button
319
+ *ngIf="!hideCancel"
320
+ type="button"
321
+ class="pw-shell__btn pw-shell__btn--cancel"
322
+ [disabled]="s.isNavigating"
323
+ (click)="facade.cancel()"
324
+ >{{ cancelLabel }}</button>
325
+ <button
326
+ type="button"
327
+ class="pw-shell__btn pw-shell__btn--next"
328
+ [disabled]="s.isNavigating || !s.canMoveNext"
329
+ (click)="facade.next()"
330
+ >{{ s.isLastStep ? finishLabel : nextLabel }}</button>
331
+ </div>
246
332
  </div>
247
- </div>
333
+ </ng-template>
248
334
  </div>
249
335
  `
250
336
  }]
@@ -276,5 +362,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
276
362
  }], stepDirectives: [{
277
363
  type: ContentChildren,
278
364
  args: [PathStepDirective]
365
+ }], customHeader: [{
366
+ type: ContentChild,
367
+ args: [PathShellHeaderDirective]
368
+ }], customFooter: [{
369
+ type: ContentChild,
370
+ args: [PathShellFooterDirective]
279
371
  }] } });
280
372
  //# sourceMappingURL=shell.js.map
package/dist/shell.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"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"}
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.2.1",
3
+ "version": "0.4.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.",
@@ -60,7 +60,7 @@
60
60
  "@angular/compiler-cli": "^17.0.0"
61
61
  },
62
62
  "dependencies": {
63
- "@daltonr/pathwrite-core": "^0.2.1"
63
+ "@daltonr/pathwrite-core": "^0.4.0"
64
64
  },
65
65
  "publishConfig": {
66
66
  "access": "public"
package/src/index.ts CHANGED
@@ -58,8 +58,18 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
58
58
  return this.engine.start(path, initialData);
59
59
  }
60
60
 
61
- public startSubPath(path: PathDefinition<any>, initialData: PathData = {}): Promise<void> {
62
- return this.engine.startSubPath(path, initialData);
61
+ /**
62
+ * Tears down any active path (without firing lifecycle hooks) and immediately
63
+ * starts the given path fresh. Safe to call whether or not a path is running.
64
+ * Use for "Start over" / retry flows without destroying and re-creating the
65
+ * component that provides this facade.
66
+ */
67
+ public restart(path: PathDefinition<any>, initialData: PathData = {}): Promise<void> {
68
+ return this.engine.restart(path, initialData);
69
+ }
70
+
71
+ public startSubPath(path: PathDefinition<any>, initialData: PathData = {}, meta?: Record<string, unknown>): Promise<void> {
72
+ return this.engine.startSubPath(path, initialData, meta);
63
73
  }
64
74
 
65
75
  public next(): Promise<void> {
@@ -162,3 +172,15 @@ export function syncFormGroup<TData extends PathData = PathData>(
162
172
  destroyRef?.onDestroy(cleanup);
163
173
  return cleanup;
164
174
  }
175
+
176
+ // Re-export core types for convenience (users don't need to import from @daltonr/pathwrite-core)
177
+ export type {
178
+ PathData,
179
+ PathDefinition,
180
+ PathEvent,
181
+ PathSnapshot,
182
+ PathStep,
183
+ PathStepContext,
184
+ SerializedPathState
185
+ } from "@daltonr/pathwrite-core";
186
+
package/src/shell.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  Input,
6
6
  Output,
7
7
  EventEmitter,
8
+ ContentChild,
8
9
  ContentChildren,
9
10
  QueryList,
10
11
  OnInit,
@@ -19,10 +20,31 @@ import { takeUntil } from "rxjs/operators";
19
20
  import {
20
21
  PathData,
21
22
  PathDefinition,
22
- PathEvent
23
+ PathEvent,
24
+ PathSnapshot
23
25
  } from "@daltonr/pathwrite-core";
24
26
  import { PathFacade } from "./index";
25
27
 
28
+ // ---------------------------------------------------------------------------
29
+ // PathShellActions
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Navigation actions passed as template context to custom `pwShellFooter`
34
+ * templates. Mirrors what React's `renderFooter` and Vue's `#footer` slot
35
+ * receive, using promises so it is consistent with the Angular facade.
36
+ */
37
+ export interface PathShellActions {
38
+ next: () => Promise<void>;
39
+ previous: () => Promise<void>;
40
+ cancel: () => Promise<void>;
41
+ goToStep: (stepId: string) => Promise<void>;
42
+ goToStepChecked: (stepId: string) => Promise<void>;
43
+ setData: (key: string, value: unknown) => Promise<void>;
44
+ /** Restart the shell's current path with its current `initialData`. */
45
+ restart: () => Promise<void>;
46
+ }
47
+
26
48
  // ---------------------------------------------------------------------------
27
49
  // PathStepDirective
28
50
  // ---------------------------------------------------------------------------
@@ -44,6 +66,56 @@ export class PathStepDirective {
44
66
  public constructor(public readonly templateRef: TemplateRef<unknown>) {}
45
67
  }
46
68
 
69
+ // ---------------------------------------------------------------------------
70
+ // PathShellHeaderDirective
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Replaces the default progress header inside `<pw-shell>`.
75
+ * The template receives the current `PathSnapshot` as the implicit context.
76
+ *
77
+ * ```html
78
+ * <pw-shell [path]="myPath">
79
+ * <ng-template pwShellHeader let-s>
80
+ * <my-custom-progress [snapshot]="s" />
81
+ * </ng-template>
82
+ * <ng-template pwStep="details"><app-details-form /></ng-template>
83
+ * </pw-shell>
84
+ * ```
85
+ */
86
+ @Directive({ selector: "[pwShellHeader]", standalone: true })
87
+ export class PathShellHeaderDirective {
88
+ public constructor(
89
+ public readonly templateRef: TemplateRef<{ $implicit: PathSnapshot }>
90
+ ) {}
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // PathShellFooterDirective
95
+ // ---------------------------------------------------------------------------
96
+
97
+ /**
98
+ * Replaces the default navigation footer inside `<pw-shell>`.
99
+ * The template receives the current `PathSnapshot` as the implicit context
100
+ * and a `actions` variable containing all navigation actions.
101
+ *
102
+ * ```html
103
+ * <pw-shell [path]="myPath">
104
+ * <ng-template pwShellFooter let-s let-actions="actions">
105
+ * <button (click)="actions.previous()" [disabled]="s.isFirstStep">Back</button>
106
+ * <button (click)="actions.next()" [disabled]="!s.canMoveNext">Next</button>
107
+ * </ng-template>
108
+ * <ng-template pwStep="details"><app-details-form /></ng-template>
109
+ * </pw-shell>
110
+ * ```
111
+ */
112
+ @Directive({ selector: "[pwShellFooter]", standalone: true })
113
+ export class PathShellFooterDirective {
114
+ public constructor(
115
+ public readonly templateRef: TemplateRef<{ $implicit: PathSnapshot; actions: PathShellActions }>
116
+ ) {}
117
+ }
118
+
47
119
  // ---------------------------------------------------------------------------
48
120
  // PathShellComponent
49
121
  // ---------------------------------------------------------------------------
@@ -76,22 +148,27 @@ export class PathStepDirective {
76
148
 
77
149
  <!-- Active path -->
78
150
  <div class="pw-shell" *ngIf="facade.state$ | async as s">
79
- <!-- Header — progress indicator -->
80
- <div class="pw-shell__header" *ngIf="!hideProgress">
81
- <div class="pw-shell__steps">
82
- <div
83
- *ngFor="let step of s.steps; let i = index"
84
- class="pw-shell__step"
85
- [ngClass]="'pw-shell__step--' + step.status"
86
- >
87
- <span class="pw-shell__step-dot">{{ step.status === 'completed' ? '✓' : (i + 1) }}</span>
88
- <span class="pw-shell__step-label">{{ step.title ?? step.id }}</span>
151
+ <!-- Header — custom or default progress indicator -->
152
+ <ng-container *ngIf="customHeader; else defaultHeader">
153
+ <ng-container *ngTemplateOutlet="customHeader.templateRef; context: { $implicit: s }"></ng-container>
154
+ </ng-container>
155
+ <ng-template #defaultHeader>
156
+ <div class="pw-shell__header" *ngIf="!hideProgress">
157
+ <div class="pw-shell__steps">
158
+ <div
159
+ *ngFor="let step of s.steps; let i = index"
160
+ class="pw-shell__step"
161
+ [ngClass]="'pw-shell__step--' + step.status"
162
+ >
163
+ <span class="pw-shell__step-dot">{{ step.status === 'completed' ? '✓' : (i + 1) }}</span>
164
+ <span class="pw-shell__step-label">{{ step.title ?? step.id }}</span>
165
+ </div>
166
+ </div>
167
+ <div class="pw-shell__track">
168
+ <div class="pw-shell__track-fill" [style.width.%]="s.progress * 100"></div>
89
169
  </div>
90
170
  </div>
91
- <div class="pw-shell__track">
92
- <div class="pw-shell__track-fill" [style.width.%]="s.progress * 100"></div>
93
- </div>
94
- </div>
171
+ </ng-template>
95
172
 
96
173
  <!-- Body — step content -->
97
174
  <div class="pw-shell__body">
@@ -107,33 +184,38 @@ export class PathStepDirective {
107
184
  <li *ngFor="let msg of s.validationMessages" class="pw-shell__validation-item">{{ msg }}</li>
108
185
  </ul>
109
186
 
110
- <!-- Footer — navigation buttons -->
111
- <div class="pw-shell__footer">
112
- <div class="pw-shell__footer-left">
113
- <button
114
- *ngIf="!s.isFirstStep"
115
- type="button"
116
- class="pw-shell__btn pw-shell__btn--back"
117
- [disabled]="s.isNavigating || !s.canMovePrevious"
118
- (click)="facade.previous()"
119
- >{{ backLabel }}</button>
120
- </div>
121
- <div class="pw-shell__footer-right">
122
- <button
123
- *ngIf="!hideCancel"
124
- type="button"
125
- class="pw-shell__btn pw-shell__btn--cancel"
126
- [disabled]="s.isNavigating"
127
- (click)="facade.cancel()"
128
- >{{ cancelLabel }}</button>
129
- <button
130
- type="button"
131
- class="pw-shell__btn pw-shell__btn--next"
132
- [disabled]="s.isNavigating || !s.canMoveNext"
133
- (click)="facade.next()"
134
- >{{ s.isLastStep ? finishLabel : nextLabel }}</button>
187
+ <!-- Footer — custom or default navigation buttons -->
188
+ <ng-container *ngIf="customFooter; else defaultFooter">
189
+ <ng-container *ngTemplateOutlet="customFooter.templateRef; context: { $implicit: s, actions: shellActions }"></ng-container>
190
+ </ng-container>
191
+ <ng-template #defaultFooter>
192
+ <div class="pw-shell__footer">
193
+ <div class="pw-shell__footer-left">
194
+ <button
195
+ *ngIf="!s.isFirstStep"
196
+ type="button"
197
+ class="pw-shell__btn pw-shell__btn--back"
198
+ [disabled]="s.isNavigating || !s.canMovePrevious"
199
+ (click)="facade.previous()"
200
+ >{{ backLabel }}</button>
201
+ </div>
202
+ <div class="pw-shell__footer-right">
203
+ <button
204
+ *ngIf="!hideCancel"
205
+ type="button"
206
+ class="pw-shell__btn pw-shell__btn--cancel"
207
+ [disabled]="s.isNavigating"
208
+ (click)="facade.cancel()"
209
+ >{{ cancelLabel }}</button>
210
+ <button
211
+ type="button"
212
+ class="pw-shell__btn pw-shell__btn--next"
213
+ [disabled]="s.isNavigating || !s.canMoveNext"
214
+ (click)="facade.next()"
215
+ >{{ s.isLastStep ? finishLabel : nextLabel }}</button>
216
+ </div>
135
217
  </div>
136
- </div>
218
+ </ng-template>
137
219
  </div>
138
220
  `
139
221
  })
@@ -162,6 +244,8 @@ export class PathShellComponent implements OnInit, OnDestroy {
162
244
  @Output() pathEvent = new EventEmitter<PathEvent>();
163
245
 
164
246
  @ContentChildren(PathStepDirective) stepDirectives!: QueryList<PathStepDirective>;
247
+ @ContentChild(PathShellHeaderDirective) customHeader?: PathShellHeaderDirective;
248
+ @ContentChild(PathShellFooterDirective) customFooter?: PathShellFooterDirective;
165
249
 
166
250
  public readonly facade = inject(PathFacade);
167
251
  /** The shell's own component-level injector. Passed to ngTemplateOutlet so that
@@ -169,6 +253,17 @@ export class PathShellComponent implements OnInit, OnDestroy {
169
253
  protected readonly shellInjector = inject(Injector);
170
254
  public started = false;
171
255
 
256
+ /** Navigation actions passed to custom `pwShellFooter` templates. */
257
+ protected readonly shellActions: PathShellActions = {
258
+ next: () => this.facade.next(),
259
+ previous: () => this.facade.previous(),
260
+ cancel: () => this.facade.cancel(),
261
+ goToStep: (id) => this.facade.goToStep(id),
262
+ goToStepChecked: (id) => this.facade.goToStepChecked(id),
263
+ setData: (key, value) => this.facade.setData(key, value as never),
264
+ restart: () => this.facade.restart(this.path, this.initialData),
265
+ };
266
+
172
267
  private readonly destroy$ = new Subject<void>();
173
268
 
174
269
  public ngOnInit(): void {
@@ -193,5 +288,3 @@ export class PathShellComponent implements OnInit, OnDestroy {
193
288
  this.facade.start(this.path, this.initialData);
194
289
  }
195
290
  }
196
-
197
-