@daltonr/pathwrite-angular 0.9.0 → 0.10.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 +93 -930
- package/dist/index.css +86 -0
- package/dist/index.d.ts +15 -7
- package/dist/index.js +16 -6
- package/dist/index.js.map +1 -1
- package/dist/shell.d.ts +10 -5
- package/dist/shell.js +92 -22
- package/dist/shell.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +26 -10
- package/src/shell.ts +55 -14
package/src/index.ts
CHANGED
|
@@ -98,6 +98,16 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
|
|
|
98
98
|
return this._engine.restart();
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/** Re-runs the operation that set `snapshot().error`. Increments `retryCount` on repeated failure. No-op when there is no pending error. */
|
|
102
|
+
public retry(): Promise<void> {
|
|
103
|
+
return this._engine.retry();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Pauses the path with intent to return. Emits `suspended`. All state is preserved. */
|
|
107
|
+
public suspend(): Promise<void> {
|
|
108
|
+
return this._engine.suspend();
|
|
109
|
+
}
|
|
110
|
+
|
|
101
111
|
public startSubPath(path: PathDefinition<any>, initialData: PathData = {}, meta?: Record<string, unknown>): Promise<void> {
|
|
102
112
|
return this._engine.startSubPath(path, initialData, meta);
|
|
103
113
|
}
|
|
@@ -141,15 +151,15 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
|
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
// ---------------------------------------------------------------------------
|
|
144
|
-
//
|
|
154
|
+
// usePathContext() - Signal-based path access
|
|
145
155
|
// ---------------------------------------------------------------------------
|
|
146
156
|
|
|
147
157
|
/**
|
|
148
|
-
* Return type of `
|
|
158
|
+
* Return type of `usePathContext()`. Provides signal-based reactive access to the
|
|
149
159
|
* path state and strongly-typed navigation actions. Mirrors React's `usePathContext()`
|
|
150
160
|
* return type for consistency across adapters.
|
|
151
161
|
*/
|
|
152
|
-
export interface
|
|
162
|
+
export interface UsePathContextReturn<TData extends PathData = PathData> {
|
|
153
163
|
/** Current path snapshot as a signal. Returns `null` when no path is active. */
|
|
154
164
|
snapshot: Signal<PathSnapshot<TData> | null>;
|
|
155
165
|
/** Start (or restart) a path. */
|
|
@@ -175,16 +185,20 @@ export interface InjectPathReturn<TData extends PathData = PathData> {
|
|
|
175
185
|
* Use for "Start over" / retry flows.
|
|
176
186
|
*/
|
|
177
187
|
restart: () => Promise<void>;
|
|
188
|
+
/** Re-run the operation that set `snapshot().error`. */
|
|
189
|
+
retry: () => Promise<void>;
|
|
190
|
+
/** Pause with intent to return, preserving all state. Emits `suspended`. */
|
|
191
|
+
suspend: () => Promise<void>;
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
/**
|
|
181
|
-
*
|
|
195
|
+
* Access the nearest `PathFacade`'s path instance for use in Angular step components.
|
|
182
196
|
* Requires `PathFacade` to be provided in the component's injector tree (either via
|
|
183
197
|
* `providers: [PathFacade]` in the component or a parent component).
|
|
184
198
|
*
|
|
185
199
|
* **This is the recommended way to consume Pathwrite in Angular components** — it
|
|
186
|
-
* provides the same ergonomic
|
|
187
|
-
*
|
|
200
|
+
* provides the same ergonomic API as React's `usePathContext()` and Vue's `usePathContext()`.
|
|
201
|
+
* No template references or manual facade injection needed.
|
|
188
202
|
*
|
|
189
203
|
* The optional generic `TData` narrows `snapshot().data` and `setData()` to your
|
|
190
204
|
* data shape. It is a **type-level assertion**, not a runtime guarantee.
|
|
@@ -203,7 +217,7 @@ export interface InjectPathReturn<TData extends PathData = PathData> {
|
|
|
203
217
|
* `
|
|
204
218
|
* })
|
|
205
219
|
* export class ContactStepComponent {
|
|
206
|
-
* protected readonly path =
|
|
220
|
+
* protected readonly path = usePathContext<ContactData>();
|
|
207
221
|
*
|
|
208
222
|
* updateName(name: string) {
|
|
209
223
|
* this.path.setData('name', name);
|
|
@@ -213,12 +227,12 @@ export interface InjectPathReturn<TData extends PathData = PathData> {
|
|
|
213
227
|
*
|
|
214
228
|
* @throws Error if PathFacade is not provided in the injector tree
|
|
215
229
|
*/
|
|
216
|
-
export function
|
|
230
|
+
export function usePathContext<TData extends PathData = PathData>(): UsePathContextReturn<TData> {
|
|
217
231
|
const facade = inject(PathFacade, { optional: true }) as PathFacade<TData> | null;
|
|
218
|
-
|
|
232
|
+
|
|
219
233
|
if (!facade) {
|
|
220
234
|
throw new Error(
|
|
221
|
-
"
|
|
235
|
+
"usePathContext() requires PathFacade to be provided. " +
|
|
222
236
|
"Add 'providers: [PathFacade]' to your component or a parent component."
|
|
223
237
|
);
|
|
224
238
|
}
|
|
@@ -235,6 +249,8 @@ export function injectPath<TData extends PathData = PathData>(): InjectPathRetur
|
|
|
235
249
|
goToStep: (stepId) => facade.goToStep(stepId),
|
|
236
250
|
goToStepChecked: (stepId) => facade.goToStepChecked(stepId),
|
|
237
251
|
restart: () => facade.restart(),
|
|
252
|
+
retry: () => facade.retry(),
|
|
253
|
+
suspend: () => facade.suspend(),
|
|
238
254
|
};
|
|
239
255
|
}
|
|
240
256
|
|
package/src/shell.ts
CHANGED
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
PathEvent,
|
|
27
27
|
PathSnapshot,
|
|
28
28
|
ProgressLayout,
|
|
29
|
-
RootProgress
|
|
29
|
+
RootProgress,
|
|
30
|
+
formatFieldKey,
|
|
31
|
+
errorPhaseMessage,
|
|
30
32
|
} from "@daltonr/pathwrite-core";
|
|
31
33
|
import { PathFacade } from "./index";
|
|
32
34
|
|
|
@@ -48,6 +50,10 @@ export interface PathShellActions {
|
|
|
48
50
|
setData: (key: string, value: unknown) => Promise<void>;
|
|
49
51
|
/** Restart the shell's current path with its current `initialData`. */
|
|
50
52
|
restart: () => Promise<void>;
|
|
53
|
+
/** Re-run the operation that set `snapshot.error`. */
|
|
54
|
+
retry: () => Promise<void>;
|
|
55
|
+
/** Pause with intent to return, preserving all state. Emits `suspended`. */
|
|
56
|
+
suspend: () => Promise<void>;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
// ---------------------------------------------------------------------------
|
|
@@ -216,10 +222,43 @@ export class PathShellFooterDirective {
|
|
|
216
222
|
</li>
|
|
217
223
|
</ul>
|
|
218
224
|
|
|
225
|
+
<!-- Blocking error — guard returned { allowed: false, reason } -->
|
|
226
|
+
<p class="pw-shell__blocking-error"
|
|
227
|
+
*ngIf="validationDisplay !== 'inline' && s.hasAttemptedNext && s.blockingError">
|
|
228
|
+
{{ s.blockingError }}
|
|
229
|
+
</p>
|
|
230
|
+
|
|
231
|
+
<!-- Error panel — replaces footer when an async operation has failed -->
|
|
232
|
+
<div class="pw-shell__error" *ngIf="s.status === 'error' && s.error; else footerOrCustom">
|
|
233
|
+
<div class="pw-shell__error-title">{{ s.error!.retryCount >= 2 ? 'Still having trouble.' : 'Something went wrong.' }}</div>
|
|
234
|
+
<div class="pw-shell__error-message">{{ errorPhaseMessage(s.error!.phase) }}{{ s.error!.message ? ' ' + s.error!.message : '' }}</div>
|
|
235
|
+
<div class="pw-shell__error-actions">
|
|
236
|
+
<button
|
|
237
|
+
*ngIf="s.error!.retryCount < 2"
|
|
238
|
+
type="button"
|
|
239
|
+
class="pw-shell__btn pw-shell__btn--retry"
|
|
240
|
+
(click)="facade.retry()"
|
|
241
|
+
>Try again</button>
|
|
242
|
+
<button
|
|
243
|
+
*ngIf="s.hasPersistence"
|
|
244
|
+
type="button"
|
|
245
|
+
[class]="'pw-shell__btn ' + (s.error!.retryCount >= 2 ? 'pw-shell__btn--retry' : 'pw-shell__btn--suspend')"
|
|
246
|
+
(click)="facade.suspend()"
|
|
247
|
+
>Save and come back later</button>
|
|
248
|
+
<button
|
|
249
|
+
*ngIf="s.error!.retryCount >= 2 && !s.hasPersistence"
|
|
250
|
+
type="button"
|
|
251
|
+
class="pw-shell__btn pw-shell__btn--retry"
|
|
252
|
+
(click)="facade.retry()"
|
|
253
|
+
>Try again</button>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
219
256
|
<!-- Footer — custom or default navigation buttons -->
|
|
220
|
-
<ng-
|
|
221
|
-
<ng-container *
|
|
222
|
-
|
|
257
|
+
<ng-template #footerOrCustom>
|
|
258
|
+
<ng-container *ngIf="customFooter; else defaultFooter">
|
|
259
|
+
<ng-container *ngTemplateOutlet="customFooter.templateRef; context: { $implicit: s, actions: shellActions }"></ng-container>
|
|
260
|
+
</ng-container>
|
|
261
|
+
</ng-template>
|
|
223
262
|
<ng-template #defaultFooter>
|
|
224
263
|
<div class="pw-shell__footer">
|
|
225
264
|
<div class="pw-shell__footer-left">
|
|
@@ -228,7 +267,7 @@ export class PathShellFooterDirective {
|
|
|
228
267
|
*ngIf="getResolvedFooterLayout(s) === 'form' && !hideCancel"
|
|
229
268
|
type="button"
|
|
230
269
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
231
|
-
[disabled]="s.
|
|
270
|
+
[disabled]="s.status !== 'idle'"
|
|
232
271
|
(click)="facade.cancel()"
|
|
233
272
|
>{{ cancelLabel }}</button>
|
|
234
273
|
<!-- Wizard mode: Back on the left -->
|
|
@@ -236,7 +275,7 @@ export class PathShellFooterDirective {
|
|
|
236
275
|
*ngIf="getResolvedFooterLayout(s) === 'wizard' && !s.isFirstStep"
|
|
237
276
|
type="button"
|
|
238
277
|
class="pw-shell__btn pw-shell__btn--back"
|
|
239
|
-
[disabled]="s.
|
|
278
|
+
[disabled]="s.status !== 'idle' || !s.canMovePrevious"
|
|
240
279
|
(click)="facade.previous()"
|
|
241
280
|
>{{ backLabel }}</button>
|
|
242
281
|
</div>
|
|
@@ -246,16 +285,17 @@ export class PathShellFooterDirective {
|
|
|
246
285
|
*ngIf="getResolvedFooterLayout(s) === 'wizard' && !hideCancel"
|
|
247
286
|
type="button"
|
|
248
287
|
class="pw-shell__btn pw-shell__btn--cancel"
|
|
249
|
-
[disabled]="s.
|
|
288
|
+
[disabled]="s.status !== 'idle'"
|
|
250
289
|
(click)="facade.cancel()"
|
|
251
290
|
>{{ cancelLabel }}</button>
|
|
252
291
|
<!-- Both modes: Submit on the right -->
|
|
253
292
|
<button
|
|
254
293
|
type="button"
|
|
255
294
|
class="pw-shell__btn pw-shell__btn--next"
|
|
256
|
-
[
|
|
295
|
+
[class.pw-shell__btn--loading]="s.status !== 'idle'"
|
|
296
|
+
[disabled]="s.status !== 'idle'"
|
|
257
297
|
(click)="facade.next()"
|
|
258
|
-
>{{ s.isLastStep ? completeLabel : nextLabel }}</button>
|
|
298
|
+
>{{ s.status !== 'idle' && loadingLabel ? loadingLabel : s.isLastStep ? completeLabel : nextLabel }}</button>
|
|
259
299
|
</div>
|
|
260
300
|
</div>
|
|
261
301
|
</ng-template>
|
|
@@ -290,6 +330,8 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
290
330
|
@Input() nextLabel = "Next";
|
|
291
331
|
/** Label for the Next button when on the last step. */
|
|
292
332
|
@Input() completeLabel = "Complete";
|
|
333
|
+
/** Label shown on the Next/Complete button while an async operation is in progress. When undefined, the button keeps its label and shows a CSS spinner only. */
|
|
334
|
+
@Input() loadingLabel?: string;
|
|
293
335
|
/** Label for the Cancel button. */
|
|
294
336
|
@Input() cancelLabel = "Cancel";
|
|
295
337
|
/** Hide the Cancel button entirely. */
|
|
@@ -342,6 +384,8 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
342
384
|
goToStepChecked: (id) => this.facade.goToStepChecked(id),
|
|
343
385
|
setData: (key, value) => this.facade.setData(key, value as never),
|
|
344
386
|
restart: () => this.facade.restart(),
|
|
387
|
+
retry: () => this.facade.retry(),
|
|
388
|
+
suspend: () => this.facade.suspend(),
|
|
345
389
|
};
|
|
346
390
|
|
|
347
391
|
private readonly destroy$ = new Subject<void>();
|
|
@@ -405,9 +449,6 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
|
|
|
405
449
|
: this.footerLayout;
|
|
406
450
|
}
|
|
407
451
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
protected formatFieldKey(key: string): string {
|
|
411
|
-
return key.replace(/([A-Z])/g, " $1").replace(/^./, c => c.toUpperCase()).trim();
|
|
412
|
-
}
|
|
452
|
+
protected errorPhaseMessage = errorPhaseMessage;
|
|
453
|
+
protected formatFieldKey = formatFieldKey;
|
|
413
454
|
}
|