@daltonr/pathwrite-angular 0.5.0 → 0.6.1

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
@@ -87,6 +87,128 @@ facade.snapshot()?.data.name; // typed as string (or whatever MyData defines)
87
87
 
88
88
  ---
89
89
 
90
+ ## `injectPath()` — Recommended API for Components
91
+
92
+ **New in v0.6.0** — `injectPath()` provides an ergonomic, signal-based API for accessing the path engine inside Angular components. This is the **recommended approach** for step components and forms — it mirrors React's `usePathContext()` and Vue's `usePath()` for consistency across frameworks.
93
+
94
+ ### Quick start
95
+
96
+ ```typescript
97
+ import { Component } from "@angular/core";
98
+ import { PathFacade, injectPath } from "@daltonr/pathwrite-angular";
99
+
100
+ @Component({
101
+ selector: "app-contact-step",
102
+ standalone: true,
103
+ providers: [PathFacade], // ← Required: provide at this component or a parent
104
+ template: `
105
+ @if (path.snapshot(); as s) {
106
+ <h2>{{ s.activeStep?.title }}</h2>
107
+ <input
108
+ type="text"
109
+ [value]="name"
110
+ (input)="updateName($any($event.target).value)"
111
+ />
112
+ <button (click)="path.next()">Next</button>
113
+ }
114
+ `
115
+ })
116
+ export class ContactStepComponent {
117
+ protected readonly path = injectPath<ContactData>();
118
+ protected name = "";
119
+
120
+ protected updateName(value: string): void {
121
+ this.name = value;
122
+ this.path.setData("name", value); // ← No facade or template ref needed
123
+ }
124
+ }
125
+ ```
126
+
127
+ ### API
128
+
129
+ `injectPath()` returns an object with:
130
+
131
+ | Member | Type | Description |
132
+ |--------|------|-------------|
133
+ | `snapshot` | `Signal<PathSnapshot \| null>` | Current path snapshot as a signal. `null` when no path is active. |
134
+ | `start(path, data?)` | `Promise<void>` | Start or restart a path. |
135
+ | `restart(path, data?)` | `Promise<void>` | Tear down and restart fresh. |
136
+ | `startSubPath(path, data?, meta?)` | `Promise<void>` | Push a sub-path onto the stack. |
137
+ | `next()` | `Promise<void>` | Advance one step. |
138
+ | `previous()` | `Promise<void>` | Go back one step. |
139
+ | `cancel()` | `Promise<void>` | Cancel the active path. |
140
+ | `setData(key, value)` | `Promise<void>` | Update a single data field. Type-safe when `TData` is specified. |
141
+ | `goToStep(stepId)` | `Promise<void>` | Jump to a step by ID (no guard checks). |
142
+ | `goToStepChecked(stepId)` | `Promise<void>` | Jump to a step by ID (guard-checked). |
143
+
144
+ ### Type safety
145
+
146
+ Pass your data type as a generic to get full type checking:
147
+
148
+ ```typescript
149
+ interface ContactData {
150
+ name: string;
151
+ email: string;
152
+ }
153
+
154
+ const path = injectPath<ContactData>();
155
+
156
+ path.setData("name", "Jane"); // ✅ OK
157
+ path.setData("foo", 123); // ❌ Type error: "foo" not in ContactData
158
+ path.snapshot()?.data.name; // ✅ Typed as string
159
+ ```
160
+
161
+ ### Requirements
162
+
163
+ - **Angular 16+** (signals required)
164
+ - `PathFacade` must be provided in the injector tree (either at the component or a parent component)
165
+
166
+ ### Compared to manual facade injection
167
+
168
+ **Before** (manual facade injection + template reference):
169
+ ```typescript
170
+ @Component({
171
+ providers: [PathFacade],
172
+ template: `
173
+ <pw-shell #shell ...>
174
+ <ng-template pwStep="contact">
175
+ <input (input)="shell.facade.setData('name', $any($event.target).value)" />
176
+ </ng-template>
177
+ </pw-shell>
178
+ `
179
+ })
180
+ export class MyComponent {
181
+ protected readonly facade = inject(PathFacade);
182
+ }
183
+ ```
184
+
185
+ **After** (with `injectPath()`):
186
+ ```typescript
187
+ @Component({
188
+ providers: [PathFacade],
189
+ template: `
190
+ <input (input)="updateName($any($event.target).value)" />
191
+ <button (click)="path.next()">Next</button>
192
+ `
193
+ })
194
+ export class MyStepComponent {
195
+ protected readonly path = injectPath<MyData>();
196
+
197
+ protected updateName(value: string): void {
198
+ this.path.setData("name", value); // ← Clean, no template ref
199
+ }
200
+ }
201
+ ```
202
+
203
+ The `injectPath()` approach:
204
+ - **No template references** — access the engine directly from the component class
205
+ - **Signal-native** — `path.snapshot()` returns the reactive signal directly
206
+ - **Type-safe** — generic parameter flows through to all methods
207
+ - **Framework-consistent** — matches React's `usePathContext()` and Vue's `usePath()`
208
+ - **Less Angular-specific knowledge** — just `inject()` and signals, patterns Angular developers already know
209
+
210
+ ---
211
+
90
212
  ## Angular Forms integration — `syncFormGroup`
91
213
 
92
214
  `syncFormGroup` eliminates the boilerplate of manually wiring an Angular
@@ -255,6 +377,38 @@ export class MyComponent {
255
377
 
256
378
  Each `<ng-template pwStep="<stepId>">` is rendered when the active step matches `stepId`. The shell handles all navigation internally.
257
379
 
380
+ > **⚠️ Important: `pwStep` Values Must Match Step IDs**
381
+ >
382
+ > The string value passed to the `pwStep` directive **must exactly match** the corresponding step's `id`:
383
+ >
384
+ > ```typescript
385
+ > const myPath: PathDefinition = {
386
+ > id: 'signup',
387
+ > steps: [
388
+ > { id: 'details' }, // ← Step ID
389
+ > { id: 'review' } // ← Step ID
390
+ > ]
391
+ > };
392
+ > ```
393
+ >
394
+ > ```html
395
+ > <pw-shell [path]="myPath">
396
+ > <ng-template pwStep="details"> <!-- ✅ Matches "details" step -->
397
+ > <app-details-form />
398
+ > </ng-template>
399
+ > <ng-template pwStep="review"> <!-- ✅ Matches "review" step -->
400
+ > <app-review-panel />
401
+ > </ng-template>
402
+ > <ng-template pwStep="foo"> <!-- ❌ No step with id "foo" -->
403
+ > <app-foo-panel />
404
+ > </ng-template>
405
+ > </pw-shell>
406
+ > ```
407
+ >
408
+ > If a `pwStep` value doesn't match any step ID, that template will never be rendered (silent — no error message).
409
+ >
410
+ > **💡 Tip:** Use your IDE's "Go to Definition" on the step ID in your path definition, then copy-paste the exact string when creating the `pwStep` directive. This ensures perfect matching and avoids typos.
411
+
258
412
  ### Context sharing
259
413
 
260
414
  `PathShellComponent` provides a `PathFacade` instance in its own `providers` array
@@ -306,7 +460,8 @@ export class MyComponent {
306
460
  | `completeLabel` | `string` | `"Complete"` | Complete button label (last step). |
307
461
  | `cancelLabel` | `string` | `"Cancel"` | Cancel button label. |
308
462
  | `hideCancel` | `boolean` | `false` | Hide the Cancel button. |
309
- | `hideProgress` | `boolean` | `false` | Hide the progress indicator. |
463
+ | `hideProgress` | `boolean` | `false` | Hide the progress indicator. Also hidden automatically for single-step top-level paths. |
464
+ | `footerLayout` | `"wizard" \| "form" \| "auto"` | `"auto"` | Footer button layout. `"auto"` uses `"form"` for single-step top-level paths, `"wizard"` otherwise. `"wizard"`: Back on left, Cancel+Submit on right. `"form"`: Cancel on left, Submit on right, no Back button. |
310
465
 
311
466
  ### Outputs
312
467
 
@@ -364,6 +519,38 @@ export class MyComponent { ... }
364
519
 
365
520
  Both directives can be combined. Only the sections you override are replaced — a custom header still shows the default footer, and vice versa.
366
521
 
522
+ ### Resetting the path
523
+
524
+ There are two ways to reset `<pw-shell>` to step 1.
525
+
526
+ **Option 1 — Toggle mount** (simplest, always correct)
527
+
528
+ Toggle an `@if` flag to destroy and recreate the shell. Every child component resets from scratch:
529
+
530
+ ```html
531
+ @if (isActive) {
532
+ <pw-shell [path]="myPath" (completed)="isActive = false" (cancelled)="isActive = false">
533
+ <ng-template pwStep="details"><app-details-form /></ng-template>
534
+ </pw-shell>
535
+ } @else {
536
+ <button (click)="isActive = true">Try Again</button>
537
+ }
538
+ ```
539
+
540
+ **Option 2 — Call `restart()` on the shell ref** (in-place, no unmount)
541
+
542
+ Use the existing `#shell` template reference — `restart()` is a public method:
543
+
544
+ ```html
545
+ <pw-shell #shell [path]="myPath" (completed)="onDone($event)">
546
+ <ng-template pwStep="details"><app-details-form /></ng-template>
547
+ </pw-shell>
548
+
549
+ <button (click)="shell.restart()">Try Again</button>
550
+ ```
551
+
552
+ `restart()` resets the path engine to step 1 with the original `[initialData]` without unmounting the component. Use this when you need to keep the shell mounted — for example, to preserve scroll position in a parent container or to drive a CSS transition.
553
+
367
554
  ---
368
555
 
369
556
  ## Sub-Paths
package/dist/index.css CHANGED
@@ -204,6 +204,15 @@
204
204
  left: 4px;
205
205
  }
206
206
 
207
+ .pw-shell__validation-label {
208
+ font-weight: 600;
209
+ margin-right: 3px;
210
+ }
211
+
212
+ .pw-shell__validation-label::after {
213
+ content: ":";
214
+ }
215
+
207
216
  /* ------------------------------------------------------------------ */
208
217
  /* Footer — navigation buttons */
209
218
  /* ------------------------------------------------------------------ */
package/dist/index.d.ts CHANGED
@@ -65,6 +65,73 @@ export declare class PathFacade<TData extends PathData = PathData> implements On
65
65
  static ɵfac: i0.ɵɵFactoryDeclaration<PathFacade<any>, never>;
66
66
  static ɵprov: i0.ɵɵInjectableDeclaration<PathFacade<any>>;
67
67
  }
68
+ /**
69
+ * Return type of `injectPath()`. Provides signal-based reactive access to the
70
+ * path state and strongly-typed navigation actions. Mirrors React's `usePathContext()`
71
+ * return type for consistency across adapters.
72
+ */
73
+ export interface InjectPathReturn<TData extends PathData = PathData> {
74
+ /** Current path snapshot as a signal. Returns `null` when no path is active. */
75
+ snapshot: Signal<PathSnapshot<TData> | null>;
76
+ /** Start (or restart) a path. */
77
+ start: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
78
+ /** Push a sub-path onto the stack. */
79
+ startSubPath: (path: PathDefinition<any>, initialData?: PathData, meta?: Record<string, unknown>) => Promise<void>;
80
+ /** Advance one step. Completes the path on the last step. */
81
+ next: () => Promise<void>;
82
+ /** Go back one step. No-op when already on the first step. */
83
+ previous: () => Promise<void>;
84
+ /** Cancel the active path (or sub-path). */
85
+ cancel: () => Promise<void>;
86
+ /** Update a single data field. */
87
+ setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
88
+ /** Jump to a step by ID without checking guards. */
89
+ goToStep: (stepId: string) => Promise<void>;
90
+ /** Jump to a step by ID, checking guards first. */
91
+ goToStepChecked: (stepId: string) => Promise<void>;
92
+ /**
93
+ * Tears down any active path and immediately starts the given path fresh.
94
+ * Use for "Start over" / retry flows.
95
+ */
96
+ restart: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
97
+ }
98
+ /**
99
+ * Inject a PathFacade and return a signal-based API for use in Angular components.
100
+ * Requires `PathFacade` to be provided in the component's injector tree (either via
101
+ * `providers: [PathFacade]` in the component or a parent component).
102
+ *
103
+ * **This is the recommended way to consume Pathwrite in Angular components** — it
104
+ * provides the same ergonomic, framework-native API that React's `usePathContext()`
105
+ * and Vue's `usePath()` offer. No template references or manual facade injection needed.
106
+ *
107
+ * The optional generic `TData` narrows `snapshot().data` and `setData()` to your
108
+ * data shape. It is a **type-level assertion**, not a runtime guarantee.
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * @Component({
113
+ * selector: 'app-contact-step',
114
+ * standalone: true,
115
+ * providers: [PathFacade], // ← Provide at this component or a parent
116
+ * template: `
117
+ * @if (path.snapshot(); as s) {
118
+ * <div>Step: {{ s.activeStep?.title }}</div>
119
+ * <button (click)="path.next()">Next</button>
120
+ * }
121
+ * `
122
+ * })
123
+ * export class ContactStepComponent {
124
+ * protected readonly path = injectPath<ContactData>();
125
+ *
126
+ * updateName(name: string) {
127
+ * this.path.setData('name', name);
128
+ * }
129
+ * }
130
+ * ```
131
+ *
132
+ * @throws Error if PathFacade is not provided in the injector tree
133
+ */
134
+ export declare function injectPath<TData extends PathData = PathData>(): InjectPathReturn<TData>;
68
135
  /**
69
136
  * Minimal interface describing what syncFormGroup needs from an Angular
70
137
  * FormGroup. Typed as a duck interface so that @angular/forms is not a
@@ -106,5 +173,5 @@ export interface FormGroupLike {
106
173
  * ```
107
174
  */
108
175
  export declare function syncFormGroup<TData extends PathData = PathData>(facade: PathFacade<TData>, formGroup: FormGroupLike, destroyRef?: DestroyRef): () => void;
109
- export type { PathData, PathDefinition, PathEvent, PathSnapshot, PathStep, PathStepContext, SerializedPathState } from "@daltonr/pathwrite-core";
176
+ export type { PathData, FieldErrors, PathDefinition, PathEvent, PathSnapshot, PathStep, PathStepContext, SerializedPathState } from "@daltonr/pathwrite-core";
110
177
  export { PathEngine } from "@daltonr/pathwrite-core";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Injectable, signal } from "@angular/core";
1
+ import { Injectable, signal, inject } from "@angular/core";
2
2
  import { BehaviorSubject, Subject } from "rxjs";
3
3
  import { PathEngine } from "@daltonr/pathwrite-core";
4
4
  import * as i0 from "@angular/core";
@@ -116,6 +116,61 @@ export class PathFacade {
116
116
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathFacade, decorators: [{
117
117
  type: Injectable
118
118
  }], ctorParameters: () => [] });
119
+ /**
120
+ * Inject a PathFacade and return a signal-based API for use in Angular components.
121
+ * Requires `PathFacade` to be provided in the component's injector tree (either via
122
+ * `providers: [PathFacade]` in the component or a parent component).
123
+ *
124
+ * **This is the recommended way to consume Pathwrite in Angular components** — it
125
+ * provides the same ergonomic, framework-native API that React's `usePathContext()`
126
+ * and Vue's `usePath()` offer. No template references or manual facade injection needed.
127
+ *
128
+ * The optional generic `TData` narrows `snapshot().data` and `setData()` to your
129
+ * data shape. It is a **type-level assertion**, not a runtime guarantee.
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * @Component({
134
+ * selector: 'app-contact-step',
135
+ * standalone: true,
136
+ * providers: [PathFacade], // ← Provide at this component or a parent
137
+ * template: `
138
+ * @if (path.snapshot(); as s) {
139
+ * <div>Step: {{ s.activeStep?.title }}</div>
140
+ * <button (click)="path.next()">Next</button>
141
+ * }
142
+ * `
143
+ * })
144
+ * export class ContactStepComponent {
145
+ * protected readonly path = injectPath<ContactData>();
146
+ *
147
+ * updateName(name: string) {
148
+ * this.path.setData('name', name);
149
+ * }
150
+ * }
151
+ * ```
152
+ *
153
+ * @throws Error if PathFacade is not provided in the injector tree
154
+ */
155
+ export function injectPath() {
156
+ const facade = inject(PathFacade, { optional: true });
157
+ if (!facade) {
158
+ throw new Error("injectPath() requires PathFacade to be provided. " +
159
+ "Add 'providers: [PathFacade]' to your component or a parent component.");
160
+ }
161
+ return {
162
+ snapshot: facade.stateSignal,
163
+ start: (path, initialData = {}) => facade.start(path, initialData),
164
+ startSubPath: (path, initialData = {}, meta) => facade.startSubPath(path, initialData, meta),
165
+ next: () => facade.next(),
166
+ previous: () => facade.previous(),
167
+ cancel: () => facade.cancel(),
168
+ setData: (key, value) => facade.setData(key, value),
169
+ goToStep: (stepId) => facade.goToStep(stepId),
170
+ goToStepChecked: (stepId) => facade.goToStepChecked(stepId),
171
+ restart: (path, initialData = {}) => facade.restart(path, initialData),
172
+ };
173
+ }
119
174
  /**
120
175
  * Syncs every key of an Angular FormGroup to the path engine via setData.
121
176
  *
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
@@ -113,8 +113,15 @@ export declare class PathShellComponent implements OnInit, OnDestroy {
113
113
  cancelLabel: string;
114
114
  /** Hide the Cancel button entirely. */
115
115
  hideCancel: boolean;
116
- /** Hide the step progress indicator in the header. */
116
+ /** Hide the step progress indicator in the header. Also hidden automatically when the path has only one step. */
117
117
  hideProgress: boolean;
118
+ /**
119
+ * Footer layout mode:
120
+ * - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
121
+ * - "wizard": Back button on left, Cancel and Submit together on right.
122
+ * - "form": Cancel on left, Submit alone on right. Back button never shown.
123
+ */
124
+ footerLayout: "wizard" | "form" | "auto";
118
125
  completed: EventEmitter<PathData>;
119
126
  cancelled: EventEmitter<PathData>;
120
127
  pathEvent: EventEmitter<PathEvent>;
@@ -132,6 +139,23 @@ export declare class PathShellComponent implements OnInit, OnDestroy {
132
139
  ngOnInit(): void;
133
140
  ngOnDestroy(): void;
134
141
  doStart(): void;
142
+ /**
143
+ * Restart the active path from step 1 with the original `initialData`,
144
+ * without unmounting the shell. Call this via a `#shell` template reference:
145
+ *
146
+ * ```html
147
+ * <pw-shell #shell [path]="myPath" ...></pw-shell>
148
+ * <button (click)="shell.restart()">Try Again</button>
149
+ * ```
150
+ */
151
+ restart(): Promise<void>;
152
+ /** Returns Object.entries(s.fieldMessages) for use in *ngFor. */
153
+ protected fieldEntries(s: PathSnapshot): [string, string][];
154
+ /** Resolves "auto" footerLayout based on snapshot. Single-step top-level → "form", otherwise → "wizard". */
155
+ protected getResolvedFooterLayout(s: PathSnapshot): "wizard" | "form";
156
+ /** Converts a camelCase or lowercase field key to a display label.
157
+ * e.g. "firstName" → "First Name", "email" → "Email" */
158
+ protected formatFieldKey(key: string): string;
135
159
  static ɵfac: i0.ɵɵFactoryDeclaration<PathShellComponent, 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; }; "completeLabel": { "alias": "completeLabel"; "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>;
160
+ 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; }; "completeLabel": { "alias": "completeLabel"; "required": false; }; "cancelLabel": { "alias": "cancelLabel"; "required": false; }; "hideCancel": { "alias": "hideCancel"; "required": false; }; "hideProgress": { "alias": "hideProgress"; "required": false; }; "footerLayout": { "alias": "footerLayout"; "required": false; }; }, { "completed": "completed"; "cancelled": "cancelled"; "pathEvent": "pathEvent"; }, ["customHeader", "customFooter", "stepDirectives"], never, true, never>;
137
161
  }
package/dist/shell.js CHANGED
@@ -119,8 +119,15 @@ export class PathShellComponent {
119
119
  this.cancelLabel = "Cancel";
120
120
  /** Hide the Cancel button entirely. */
121
121
  this.hideCancel = false;
122
- /** Hide the step progress indicator in the header. */
122
+ /** Hide the step progress indicator in the header. Also hidden automatically when the path has only one step. */
123
123
  this.hideProgress = false;
124
+ /**
125
+ * Footer layout mode:
126
+ * - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
127
+ * - "wizard": Back button on left, Cancel and Submit together on right.
128
+ * - "form": Cancel on left, Submit alone on right. Back button never shown.
129
+ */
130
+ this.footerLayout = "auto";
124
131
  this.completed = new EventEmitter();
125
132
  this.cancelled = new EventEmitter();
126
133
  this.pathEvent = new EventEmitter();
@@ -161,8 +168,35 @@ export class PathShellComponent {
161
168
  this.started = true;
162
169
  this.facade.start(this.path, this.initialData);
163
170
  }
171
+ /**
172
+ * Restart the active path from step 1 with the original `initialData`,
173
+ * without unmounting the shell. Call this via a `#shell` template reference:
174
+ *
175
+ * ```html
176
+ * <pw-shell #shell [path]="myPath" ...></pw-shell>
177
+ * <button (click)="shell.restart()">Try Again</button>
178
+ * ```
179
+ */
180
+ restart() {
181
+ return this.facade.restart(this.path, this.initialData);
182
+ }
183
+ /** Returns Object.entries(s.fieldMessages) for use in *ngFor. */
184
+ fieldEntries(s) {
185
+ return Object.entries(s.fieldMessages);
186
+ }
187
+ /** Resolves "auto" footerLayout based on snapshot. Single-step top-level → "form", otherwise → "wizard". */
188
+ getResolvedFooterLayout(s) {
189
+ return this.footerLayout === "auto"
190
+ ? (s.stepCount === 1 && s.nestingLevel === 0 ? "form" : "wizard")
191
+ : this.footerLayout;
192
+ }
193
+ /** Converts a camelCase or lowercase field key to a display label.
194
+ * e.g. "firstName" → "First Name", "email" → "Email" */
195
+ formatFieldKey(key) {
196
+ return key.replace(/([A-Z])/g, " $1").replace(/^./, c => c.toUpperCase()).trim();
197
+ }
164
198
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PathShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
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", completeLabel: "completeLabel", 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: `
199
+ 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", completeLabel: "completeLabel", cancelLabel: "cancelLabel", hideCancel: "hideCancel", hideProgress: "hideProgress", footerLayout: "footerLayout" }, 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: `
166
200
  <!-- Empty state -->
167
201
  <div class="pw-shell" *ngIf="!(facade.state$ | async)">
168
202
  <div class="pw-shell__empty" *ngIf="!started">
@@ -178,7 +212,7 @@ export class PathShellComponent {
178
212
  <ng-container *ngTemplateOutlet="customHeader.templateRef; context: { $implicit: s }"></ng-container>
179
213
  </ng-container>
180
214
  <ng-template #defaultHeader>
181
- <div class="pw-shell__header" *ngIf="!hideProgress">
215
+ <div class="pw-shell__header" *ngIf="!hideProgress && (s.stepCount > 1 || s.nestingLevel > 0)">
182
216
  <div class="pw-shell__steps">
183
217
  <div
184
218
  *ngFor="let step of s.steps; let i = index"
@@ -204,9 +238,11 @@ export class PathShellComponent {
204
238
  </ng-container>
205
239
  </div>
206
240
 
207
- <!-- Validation messages -->
208
- <ul class="pw-shell__validation" *ngIf="s.validationMessages.length > 0">
209
- <li *ngFor="let msg of s.validationMessages" class="pw-shell__validation-item">{{ msg }}</li>
241
+ <!-- Validation messages — labeled by field name -->
242
+ <ul class="pw-shell__validation" *ngIf="s.hasAttemptedNext && fieldEntries(s).length > 0">
243
+ <li *ngFor="let entry of fieldEntries(s)" class="pw-shell__validation-item">
244
+ <span *ngIf="entry[0] !== '_'" class="pw-shell__validation-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
245
+ </li>
210
246
  </ul>
211
247
 
212
248
  <!-- Footer — custom or default navigation buttons -->
@@ -216,8 +252,17 @@ export class PathShellComponent {
216
252
  <ng-template #defaultFooter>
217
253
  <div class="pw-shell__footer">
218
254
  <div class="pw-shell__footer-left">
255
+ <!-- Form mode: Cancel on the left -->
256
+ <button
257
+ *ngIf="getResolvedFooterLayout(s) === 'form' && !hideCancel"
258
+ type="button"
259
+ class="pw-shell__btn pw-shell__btn--cancel"
260
+ [disabled]="s.isNavigating"
261
+ (click)="facade.cancel()"
262
+ >{{ cancelLabel }}</button>
263
+ <!-- Wizard mode: Back on the left -->
219
264
  <button
220
- *ngIf="!s.isFirstStep"
265
+ *ngIf="getResolvedFooterLayout(s) === 'wizard' && !s.isFirstStep"
221
266
  type="button"
222
267
  class="pw-shell__btn pw-shell__btn--back"
223
268
  [disabled]="s.isNavigating || !s.canMovePrevious"
@@ -225,17 +270,19 @@ export class PathShellComponent {
225
270
  >{{ backLabel }}</button>
226
271
  </div>
227
272
  <div class="pw-shell__footer-right">
273
+ <!-- Wizard mode: Cancel on the right -->
228
274
  <button
229
- *ngIf="!hideCancel"
275
+ *ngIf="getResolvedFooterLayout(s) === 'wizard' && !hideCancel"
230
276
  type="button"
231
277
  class="pw-shell__btn pw-shell__btn--cancel"
232
278
  [disabled]="s.isNavigating"
233
279
  (click)="facade.cancel()"
234
280
  >{{ cancelLabel }}</button>
281
+ <!-- Both modes: Submit on the right -->
235
282
  <button
236
283
  type="button"
237
284
  class="pw-shell__btn pw-shell__btn--next"
238
- [disabled]="s.isNavigating || !s.canMoveNext"
285
+ [disabled]="s.isNavigating"
239
286
  (click)="facade.next()"
240
287
  >{{ s.isLastStep ? completeLabel : nextLabel }}</button>
241
288
  </div>
@@ -268,7 +315,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
268
315
  <ng-container *ngTemplateOutlet="customHeader.templateRef; context: { $implicit: s }"></ng-container>
269
316
  </ng-container>
270
317
  <ng-template #defaultHeader>
271
- <div class="pw-shell__header" *ngIf="!hideProgress">
318
+ <div class="pw-shell__header" *ngIf="!hideProgress && (s.stepCount > 1 || s.nestingLevel > 0)">
272
319
  <div class="pw-shell__steps">
273
320
  <div
274
321
  *ngFor="let step of s.steps; let i = index"
@@ -294,9 +341,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
294
341
  </ng-container>
295
342
  </div>
296
343
 
297
- <!-- Validation messages -->
298
- <ul class="pw-shell__validation" *ngIf="s.validationMessages.length > 0">
299
- <li *ngFor="let msg of s.validationMessages" class="pw-shell__validation-item">{{ msg }}</li>
344
+ <!-- Validation messages — labeled by field name -->
345
+ <ul class="pw-shell__validation" *ngIf="s.hasAttemptedNext && fieldEntries(s).length > 0">
346
+ <li *ngFor="let entry of fieldEntries(s)" class="pw-shell__validation-item">
347
+ <span *ngIf="entry[0] !== '_'" class="pw-shell__validation-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
348
+ </li>
300
349
  </ul>
301
350
 
302
351
  <!-- Footer — custom or default navigation buttons -->
@@ -306,8 +355,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
306
355
  <ng-template #defaultFooter>
307
356
  <div class="pw-shell__footer">
308
357
  <div class="pw-shell__footer-left">
358
+ <!-- Form mode: Cancel on the left -->
309
359
  <button
310
- *ngIf="!s.isFirstStep"
360
+ *ngIf="getResolvedFooterLayout(s) === 'form' && !hideCancel"
361
+ type="button"
362
+ class="pw-shell__btn pw-shell__btn--cancel"
363
+ [disabled]="s.isNavigating"
364
+ (click)="facade.cancel()"
365
+ >{{ cancelLabel }}</button>
366
+ <!-- Wizard mode: Back on the left -->
367
+ <button
368
+ *ngIf="getResolvedFooterLayout(s) === 'wizard' && !s.isFirstStep"
311
369
  type="button"
312
370
  class="pw-shell__btn pw-shell__btn--back"
313
371
  [disabled]="s.isNavigating || !s.canMovePrevious"
@@ -315,17 +373,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
315
373
  >{{ backLabel }}</button>
316
374
  </div>
317
375
  <div class="pw-shell__footer-right">
376
+ <!-- Wizard mode: Cancel on the right -->
318
377
  <button
319
- *ngIf="!hideCancel"
378
+ *ngIf="getResolvedFooterLayout(s) === 'wizard' && !hideCancel"
320
379
  type="button"
321
380
  class="pw-shell__btn pw-shell__btn--cancel"
322
381
  [disabled]="s.isNavigating"
323
382
  (click)="facade.cancel()"
324
383
  >{{ cancelLabel }}</button>
384
+ <!-- Both modes: Submit on the right -->
325
385
  <button
326
386
  type="button"
327
387
  class="pw-shell__btn pw-shell__btn--next"
328
- [disabled]="s.isNavigating || !s.canMoveNext"
388
+ [disabled]="s.isNavigating"
329
389
  (click)="facade.next()"
330
390
  >{{ s.isLastStep ? completeLabel : nextLabel }}</button>
331
391
  </div>
@@ -353,6 +413,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
353
413
  type: Input
354
414
  }], hideProgress: [{
355
415
  type: Input
416
+ }], footerLayout: [{
417
+ type: Input
356
418
  }], completed: [{
357
419
  type: Output
358
420
  }], cancelled: [{
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.5.0",
3
+ "version": "0.6.1",
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.",
@@ -29,7 +29,8 @@
29
29
  "types": "./dist/shell.d.ts",
30
30
  "import": "./dist/shell.js"
31
31
  },
32
- "./styles.css": "./dist/index.css"
32
+ "./styles.css": "./dist/index.css",
33
+ "./dist/index.css": "./dist/index.css"
33
34
  },
34
35
  "main": "dist/index.js",
35
36
  "types": "dist/index.d.ts",
@@ -60,7 +61,7 @@
60
61
  "@angular/compiler-cli": "^17.0.0"
61
62
  },
62
63
  "dependencies": {
63
- "@daltonr/pathwrite-core": "^0.5.0"
64
+ "@daltonr/pathwrite-core": "^0.6.1"
64
65
  },
65
66
  "publishConfig": {
66
67
  "access": "public"
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Injectable, OnDestroy, DestroyRef, signal, Signal } from "@angular/core";
1
+ import { Injectable, OnDestroy, DestroyRef, signal, Signal, inject } from "@angular/core";
2
2
  import { BehaviorSubject, Observable, Subject } from "rxjs";
3
3
  import {
4
4
  PathData,
@@ -134,6 +134,101 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
134
134
  }
135
135
  }
136
136
 
137
+ // ---------------------------------------------------------------------------
138
+ // injectPath() - Signal-based path access
139
+ // ---------------------------------------------------------------------------
140
+
141
+ /**
142
+ * Return type of `injectPath()`. Provides signal-based reactive access to the
143
+ * path state and strongly-typed navigation actions. Mirrors React's `usePathContext()`
144
+ * return type for consistency across adapters.
145
+ */
146
+ export interface InjectPathReturn<TData extends PathData = PathData> {
147
+ /** Current path snapshot as a signal. Returns `null` when no path is active. */
148
+ snapshot: Signal<PathSnapshot<TData> | null>;
149
+ /** Start (or restart) a path. */
150
+ start: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
151
+ /** Push a sub-path onto the stack. */
152
+ startSubPath: (path: PathDefinition<any>, initialData?: PathData, meta?: Record<string, unknown>) => Promise<void>;
153
+ /** Advance one step. Completes the path on the last step. */
154
+ next: () => Promise<void>;
155
+ /** Go back one step. No-op when already on the first step. */
156
+ previous: () => Promise<void>;
157
+ /** Cancel the active path (or sub-path). */
158
+ cancel: () => Promise<void>;
159
+ /** Update a single data field. */
160
+ setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
161
+ /** Jump to a step by ID without checking guards. */
162
+ goToStep: (stepId: string) => Promise<void>;
163
+ /** Jump to a step by ID, checking guards first. */
164
+ goToStepChecked: (stepId: string) => Promise<void>;
165
+ /**
166
+ * Tears down any active path and immediately starts the given path fresh.
167
+ * Use for "Start over" / retry flows.
168
+ */
169
+ restart: (path: PathDefinition<any>, initialData?: PathData) => Promise<void>;
170
+ }
171
+
172
+ /**
173
+ * Inject a PathFacade and return a signal-based API for use in Angular components.
174
+ * Requires `PathFacade` to be provided in the component's injector tree (either via
175
+ * `providers: [PathFacade]` in the component or a parent component).
176
+ *
177
+ * **This is the recommended way to consume Pathwrite in Angular components** — it
178
+ * provides the same ergonomic, framework-native API that React's `usePathContext()`
179
+ * and Vue's `usePath()` offer. No template references or manual facade injection needed.
180
+ *
181
+ * The optional generic `TData` narrows `snapshot().data` and `setData()` to your
182
+ * data shape. It is a **type-level assertion**, not a runtime guarantee.
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * @Component({
187
+ * selector: 'app-contact-step',
188
+ * standalone: true,
189
+ * providers: [PathFacade], // ← Provide at this component or a parent
190
+ * template: `
191
+ * @if (path.snapshot(); as s) {
192
+ * <div>Step: {{ s.activeStep?.title }}</div>
193
+ * <button (click)="path.next()">Next</button>
194
+ * }
195
+ * `
196
+ * })
197
+ * export class ContactStepComponent {
198
+ * protected readonly path = injectPath<ContactData>();
199
+ *
200
+ * updateName(name: string) {
201
+ * this.path.setData('name', name);
202
+ * }
203
+ * }
204
+ * ```
205
+ *
206
+ * @throws Error if PathFacade is not provided in the injector tree
207
+ */
208
+ export function injectPath<TData extends PathData = PathData>(): InjectPathReturn<TData> {
209
+ const facade = inject(PathFacade, { optional: true }) as PathFacade<TData> | null;
210
+
211
+ if (!facade) {
212
+ throw new Error(
213
+ "injectPath() requires PathFacade to be provided. " +
214
+ "Add 'providers: [PathFacade]' to your component or a parent component."
215
+ );
216
+ }
217
+
218
+ return {
219
+ snapshot: facade.stateSignal,
220
+ start: (path, initialData = {}) => facade.start(path, initialData),
221
+ startSubPath: (path, initialData = {}, meta) => facade.startSubPath(path, initialData, meta),
222
+ next: () => facade.next(),
223
+ previous: () => facade.previous(),
224
+ cancel: () => facade.cancel(),
225
+ setData: (key, value) => facade.setData(key, value),
226
+ goToStep: (stepId) => facade.goToStep(stepId),
227
+ goToStepChecked: (stepId) => facade.goToStepChecked(stepId),
228
+ restart: (path, initialData = {}) => facade.restart(path, initialData),
229
+ };
230
+ }
231
+
137
232
  // ---------------------------------------------------------------------------
138
233
  // Forms integration
139
234
  // ---------------------------------------------------------------------------
@@ -206,6 +301,7 @@ export function syncFormGroup<TData extends PathData = PathData>(
206
301
  // Re-export core types for convenience (users don't need to import from @daltonr/pathwrite-core)
207
302
  export type {
208
303
  PathData,
304
+ FieldErrors,
209
305
  PathDefinition,
210
306
  PathEvent,
211
307
  PathSnapshot,
package/src/shell.ts CHANGED
@@ -153,7 +153,7 @@ export class PathShellFooterDirective {
153
153
  <ng-container *ngTemplateOutlet="customHeader.templateRef; context: { $implicit: s }"></ng-container>
154
154
  </ng-container>
155
155
  <ng-template #defaultHeader>
156
- <div class="pw-shell__header" *ngIf="!hideProgress">
156
+ <div class="pw-shell__header" *ngIf="!hideProgress && (s.stepCount > 1 || s.nestingLevel > 0)">
157
157
  <div class="pw-shell__steps">
158
158
  <div
159
159
  *ngFor="let step of s.steps; let i = index"
@@ -179,9 +179,11 @@ export class PathShellFooterDirective {
179
179
  </ng-container>
180
180
  </div>
181
181
 
182
- <!-- Validation messages -->
183
- <ul class="pw-shell__validation" *ngIf="s.validationMessages.length > 0">
184
- <li *ngFor="let msg of s.validationMessages" class="pw-shell__validation-item">{{ msg }}</li>
182
+ <!-- Validation messages — labeled by field name -->
183
+ <ul class="pw-shell__validation" *ngIf="s.hasAttemptedNext && fieldEntries(s).length > 0">
184
+ <li *ngFor="let entry of fieldEntries(s)" class="pw-shell__validation-item">
185
+ <span *ngIf="entry[0] !== '_'" class="pw-shell__validation-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
186
+ </li>
185
187
  </ul>
186
188
 
187
189
  <!-- Footer — custom or default navigation buttons -->
@@ -191,8 +193,17 @@ export class PathShellFooterDirective {
191
193
  <ng-template #defaultFooter>
192
194
  <div class="pw-shell__footer">
193
195
  <div class="pw-shell__footer-left">
196
+ <!-- Form mode: Cancel on the left -->
194
197
  <button
195
- *ngIf="!s.isFirstStep"
198
+ *ngIf="getResolvedFooterLayout(s) === 'form' && !hideCancel"
199
+ type="button"
200
+ class="pw-shell__btn pw-shell__btn--cancel"
201
+ [disabled]="s.isNavigating"
202
+ (click)="facade.cancel()"
203
+ >{{ cancelLabel }}</button>
204
+ <!-- Wizard mode: Back on the left -->
205
+ <button
206
+ *ngIf="getResolvedFooterLayout(s) === 'wizard' && !s.isFirstStep"
196
207
  type="button"
197
208
  class="pw-shell__btn pw-shell__btn--back"
198
209
  [disabled]="s.isNavigating || !s.canMovePrevious"
@@ -200,17 +211,19 @@ export class PathShellFooterDirective {
200
211
  >{{ backLabel }}</button>
201
212
  </div>
202
213
  <div class="pw-shell__footer-right">
214
+ <!-- Wizard mode: Cancel on the right -->
203
215
  <button
204
- *ngIf="!hideCancel"
216
+ *ngIf="getResolvedFooterLayout(s) === 'wizard' && !hideCancel"
205
217
  type="button"
206
218
  class="pw-shell__btn pw-shell__btn--cancel"
207
219
  [disabled]="s.isNavigating"
208
220
  (click)="facade.cancel()"
209
221
  >{{ cancelLabel }}</button>
222
+ <!-- Both modes: Submit on the right -->
210
223
  <button
211
224
  type="button"
212
225
  class="pw-shell__btn pw-shell__btn--next"
213
- [disabled]="s.isNavigating || !s.canMoveNext"
226
+ [disabled]="s.isNavigating"
214
227
  (click)="facade.next()"
215
228
  >{{ s.isLastStep ? completeLabel : nextLabel }}</button>
216
229
  </div>
@@ -236,8 +249,15 @@ export class PathShellComponent implements OnInit, OnDestroy {
236
249
  @Input() cancelLabel = "Cancel";
237
250
  /** Hide the Cancel button entirely. */
238
251
  @Input() hideCancel = false;
239
- /** Hide the step progress indicator in the header. */
252
+ /** Hide the step progress indicator in the header. Also hidden automatically when the path has only one step. */
240
253
  @Input() hideProgress = false;
254
+ /**
255
+ * Footer layout mode:
256
+ * - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
257
+ * - "wizard": Back button on left, Cancel and Submit together on right.
258
+ * - "form": Cancel on left, Submit alone on right. Back button never shown.
259
+ */
260
+ @Input() footerLayout: "wizard" | "form" | "auto" = "auto";
241
261
 
242
262
  @Output() completed = new EventEmitter<PathData>();
243
263
  @Output() cancelled = new EventEmitter<PathData>();
@@ -287,4 +307,35 @@ export class PathShellComponent implements OnInit, OnDestroy {
287
307
  this.started = true;
288
308
  this.facade.start(this.path, this.initialData);
289
309
  }
310
+
311
+ /**
312
+ * Restart the active path from step 1 with the original `initialData`,
313
+ * without unmounting the shell. Call this via a `#shell` template reference:
314
+ *
315
+ * ```html
316
+ * <pw-shell #shell [path]="myPath" ...></pw-shell>
317
+ * <button (click)="shell.restart()">Try Again</button>
318
+ * ```
319
+ */
320
+ public restart(): Promise<void> {
321
+ return this.facade.restart(this.path, this.initialData);
322
+ }
323
+
324
+ /** Returns Object.entries(s.fieldMessages) for use in *ngFor. */
325
+ protected fieldEntries(s: PathSnapshot): [string, string][] {
326
+ return Object.entries(s.fieldMessages) as [string, string][];
327
+ }
328
+
329
+ /** Resolves "auto" footerLayout based on snapshot. Single-step top-level → "form", otherwise → "wizard". */
330
+ protected getResolvedFooterLayout(s: PathSnapshot): "wizard" | "form" {
331
+ return this.footerLayout === "auto"
332
+ ? (s.stepCount === 1 && s.nestingLevel === 0 ? "form" : "wizard")
333
+ : this.footerLayout;
334
+ }
335
+
336
+ /** Converts a camelCase or lowercase field key to a display label.
337
+ * e.g. "firstName" → "First Name", "email" → "Email" */
338
+ protected formatFieldKey(key: string): string {
339
+ return key.replace(/([A-Z])/g, " $1").replace(/^./, c => c.toUpperCase()).trim();
340
+ }
290
341
  }