@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 +384 -2
- package/dist/index.css +10 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/shell.d.ts +71 -2
- package/dist/shell.js +174 -82
- package/dist/shell.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +24 -2
- package/src/shell.ts +136 -43
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
|
-
| `
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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":"AAAA,OAAO,EAAE,UAAU,EAAyB,MAAM,EAAU,MAAM,eAAe,CAAC;AAClF,OAAO,EAAE,eAAe,EAAc,OAAO,EAAE,MAAM,MAAM,CAAC;AAC5D,OAAO,EAGL,UAAU,EAGX,MAAM,yBAAyB,CAAC;;AAEjC;;;;;;;;;;;;;GAaG;AAEH,MAAM,OAAO,UAAU;IAYrB;QAXiB,WAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC1B,YAAO,GAAG,IAAI,eAAe,CAA6B,IAAI,CAAC,CAAC;QAChE,aAAQ,GAAG,IAAI,OAAO,EAAa,CAAC;QAEpC,iBAAY,GAAG,MAAM,CAA6B,IAAI,CAAC,CAAC;QAEzD,WAAM,GAA2C,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;QAC7E,YAAO,GAA0B,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,CAAC;QAC9E,0FAA0F;QAC1E,gBAAW,GAAuC,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,CAAC;QAG/F,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;YAC3D,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC9D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,QAA+B,CAAC,CAAC;gBACzD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,QAA+B,CAAC,CAAC;YAC/D,CAAC;iBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;gBACpE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACxB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAEM,WAAW;QAChB,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAC7B,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACzB,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;IAC1B,CAAC;IAEM,KAAK,CAAC,IAAyB,EAAE,cAAwB,EAAE;QAChE,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC9C,CAAC;IAEM,YAAY,CAAC,IAAyB,EAAE,cAAwB,EAAE;
|
|
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
|
-
<
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
</
|
|
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
|
-
<
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
</
|
|
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":"AAAA,OAAO,EACL,SAAS,EACT,SAAS,EAET,KAAK,EACL,MAAM,EACN,YAAY,EACZ,eAAe,EAIf,MAAM,EACN,QAAQ,EACR,uBAAuB,EACxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daltonr/pathwrite-angular",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.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.
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
<
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
</
|
|
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
|
-
|