@daltonr/pathwrite-angular 0.1.4 → 0.1.5
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/package.json +3 -2
- package/src/index.ts +72 -0
- package/src/shell.ts +179 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daltonr/pathwrite-angular",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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.",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"types": "dist/index.d.ts",
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
|
38
|
+
"src",
|
|
38
39
|
"README.md",
|
|
39
40
|
"LICENSE"
|
|
40
41
|
],
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"rxjs": "^7.0.0"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
|
-
"@daltonr/pathwrite-core": "^0.1.
|
|
53
|
+
"@daltonr/pathwrite-core": "^0.1.5"
|
|
53
54
|
},
|
|
54
55
|
"publishConfig": {
|
|
55
56
|
"access": "public"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Injectable, OnDestroy } from "@angular/core";
|
|
2
|
+
import { BehaviorSubject, Observable, Subject } from "rxjs";
|
|
3
|
+
import {
|
|
4
|
+
PathData,
|
|
5
|
+
PathDefinition,
|
|
6
|
+
PathEngine,
|
|
7
|
+
PathEvent,
|
|
8
|
+
PathSnapshot
|
|
9
|
+
} from "@daltonr/pathwrite-core";
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class PathFacade implements OnDestroy {
|
|
13
|
+
private readonly engine = new PathEngine();
|
|
14
|
+
private readonly _state$ = new BehaviorSubject<PathSnapshot | null>(null);
|
|
15
|
+
private readonly _events$ = new Subject<PathEvent>();
|
|
16
|
+
private readonly unsubscribeFromEngine: () => void;
|
|
17
|
+
|
|
18
|
+
public readonly state$: Observable<PathSnapshot | null> = this._state$.asObservable();
|
|
19
|
+
public readonly events$: Observable<PathEvent> = this._events$.asObservable();
|
|
20
|
+
|
|
21
|
+
public constructor() {
|
|
22
|
+
this.unsubscribeFromEngine = this.engine.subscribe((event) => {
|
|
23
|
+
this._events$.next(event);
|
|
24
|
+
if (event.type === "stateChanged" || event.type === "resumed") {
|
|
25
|
+
this._state$.next(event.snapshot);
|
|
26
|
+
} else if (event.type === "completed" || event.type === "cancelled") {
|
|
27
|
+
this._state$.next(null);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public ngOnDestroy(): void {
|
|
33
|
+
this.unsubscribeFromEngine();
|
|
34
|
+
this._events$.complete();
|
|
35
|
+
this._state$.complete();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public start(path: PathDefinition, initialData: PathData = {}): Promise<void> {
|
|
39
|
+
return this.engine.start(path, initialData);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public startSubPath(path: PathDefinition, initialData: PathData = {}): Promise<void> {
|
|
43
|
+
return this.engine.startSubPath(path, initialData);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public next(): Promise<void> {
|
|
47
|
+
return this.engine.next();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public previous(): Promise<void> {
|
|
51
|
+
return this.engine.previous();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public cancel(): Promise<void> {
|
|
55
|
+
return this.engine.cancel();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public setData(key: string, value: unknown): Promise<void> {
|
|
59
|
+
return this.engine.setData(key, value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public goToStep(stepId: string): Promise<void> {
|
|
63
|
+
return this.engine.goToStep(stepId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public snapshot(): PathSnapshot | null {
|
|
67
|
+
return this._state$.getValue();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
package/src/shell.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
Directive,
|
|
4
|
+
TemplateRef,
|
|
5
|
+
Input,
|
|
6
|
+
Output,
|
|
7
|
+
EventEmitter,
|
|
8
|
+
ContentChildren,
|
|
9
|
+
QueryList,
|
|
10
|
+
OnInit,
|
|
11
|
+
OnDestroy,
|
|
12
|
+
inject,
|
|
13
|
+
ChangeDetectionStrategy
|
|
14
|
+
} from "@angular/core";
|
|
15
|
+
import { CommonModule } from "@angular/common";
|
|
16
|
+
import { Subject } from "rxjs";
|
|
17
|
+
import { takeUntil } from "rxjs/operators";
|
|
18
|
+
import {
|
|
19
|
+
PathData,
|
|
20
|
+
PathDefinition,
|
|
21
|
+
PathEvent
|
|
22
|
+
} from "@daltonr/pathwrite-core";
|
|
23
|
+
import { PathFacade } from "./index";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// PathStepDirective
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Structural directive that associates a template with a step ID.
|
|
31
|
+
* Used inside `<pw-shell>` to define per-step content.
|
|
32
|
+
*
|
|
33
|
+
* ```html
|
|
34
|
+
* <pw-shell [path]="myPath">
|
|
35
|
+
* <ng-template pwStep="details"><app-details-form /></ng-template>
|
|
36
|
+
* <ng-template pwStep="review"><app-review-panel /></ng-template>
|
|
37
|
+
* </pw-shell>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
@Directive({ selector: "[pwStep]", standalone: true })
|
|
41
|
+
export class PathStepDirective {
|
|
42
|
+
@Input({ required: true, alias: "pwStep" }) stepId!: string;
|
|
43
|
+
public constructor(public readonly templateRef: TemplateRef<unknown>) {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// PathShellComponent
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Default UI shell component. Renders a progress indicator, step content,
|
|
52
|
+
* and navigation buttons.
|
|
53
|
+
*
|
|
54
|
+
* ```html
|
|
55
|
+
* <pw-shell [path]="myPath" [initialData]="{ name: '' }" (completed)="onDone($event)">
|
|
56
|
+
* <ng-template pwStep="details"><app-details-form /></ng-template>
|
|
57
|
+
* <ng-template pwStep="review"><app-review-panel /></ng-template>
|
|
58
|
+
* </pw-shell>
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
@Component({
|
|
62
|
+
selector: "pw-shell",
|
|
63
|
+
standalone: true,
|
|
64
|
+
imports: [CommonModule],
|
|
65
|
+
providers: [PathFacade],
|
|
66
|
+
changeDetection: ChangeDetectionStrategy.Default,
|
|
67
|
+
template: `
|
|
68
|
+
<!-- Empty state -->
|
|
69
|
+
<div class="pw-shell" *ngIf="!(facade.state$ | async)">
|
|
70
|
+
<div class="pw-shell__empty" *ngIf="!started">
|
|
71
|
+
<p>No active path.</p>
|
|
72
|
+
<button *ngIf="!autoStart" type="button" class="pw-shell__start-btn" (click)="doStart()">Start</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- Active path -->
|
|
77
|
+
<div class="pw-shell" *ngIf="facade.state$ | async as s">
|
|
78
|
+
<!-- Header — progress indicator -->
|
|
79
|
+
<div class="pw-shell__header" *ngIf="!hideProgress">
|
|
80
|
+
<div class="pw-shell__steps">
|
|
81
|
+
<div
|
|
82
|
+
*ngFor="let step of s.steps; let i = index"
|
|
83
|
+
class="pw-shell__step"
|
|
84
|
+
[ngClass]="'pw-shell__step--' + step.status"
|
|
85
|
+
>
|
|
86
|
+
<span class="pw-shell__step-dot">{{ step.status === 'completed' ? '✓' : (i + 1) }}</span>
|
|
87
|
+
<span class="pw-shell__step-label">{{ step.title ?? step.id }}</span>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="pw-shell__track">
|
|
91
|
+
<div class="pw-shell__track-fill" [style.width.%]="s.progress * 100"></div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Body — step content -->
|
|
96
|
+
<div class="pw-shell__body">
|
|
97
|
+
<ng-container *ngFor="let stepDir of stepDirectives">
|
|
98
|
+
<ng-container *ngIf="stepDir.stepId === s.stepId">
|
|
99
|
+
<ng-container *ngTemplateOutlet="stepDir.templateRef"></ng-container>
|
|
100
|
+
</ng-container>
|
|
101
|
+
</ng-container>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- Footer — navigation buttons -->
|
|
105
|
+
<div class="pw-shell__footer">
|
|
106
|
+
<div class="pw-shell__footer-left">
|
|
107
|
+
<button
|
|
108
|
+
*ngIf="!s.isFirstStep"
|
|
109
|
+
type="button"
|
|
110
|
+
class="pw-shell__btn pw-shell__btn--back"
|
|
111
|
+
[disabled]="s.isNavigating || !s.canMovePrevious"
|
|
112
|
+
(click)="facade.previous()"
|
|
113
|
+
>{{ backLabel }}</button>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="pw-shell__footer-right">
|
|
116
|
+
<button
|
|
117
|
+
*ngIf="!hideCancel"
|
|
118
|
+
type="button"
|
|
119
|
+
class="pw-shell__btn pw-shell__btn--cancel"
|
|
120
|
+
[disabled]="s.isNavigating"
|
|
121
|
+
(click)="facade.cancel()"
|
|
122
|
+
>{{ cancelLabel }}</button>
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
class="pw-shell__btn pw-shell__btn--next"
|
|
126
|
+
[disabled]="s.isNavigating || !s.canMoveNext"
|
|
127
|
+
(click)="facade.next()"
|
|
128
|
+
>{{ s.isLastStep ? finishLabel : nextLabel }}</button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
`
|
|
133
|
+
})
|
|
134
|
+
export class PathShellComponent implements OnInit, OnDestroy {
|
|
135
|
+
@Input({ required: true }) path!: PathDefinition;
|
|
136
|
+
@Input() initialData: PathData = {};
|
|
137
|
+
@Input() autoStart = true;
|
|
138
|
+
@Input() backLabel = "Back";
|
|
139
|
+
@Input() nextLabel = "Next";
|
|
140
|
+
@Input() finishLabel = "Finish";
|
|
141
|
+
@Input() cancelLabel = "Cancel";
|
|
142
|
+
@Input() hideCancel = false;
|
|
143
|
+
@Input() hideProgress = false;
|
|
144
|
+
|
|
145
|
+
@Output() completed = new EventEmitter<PathData>();
|
|
146
|
+
@Output() cancelled = new EventEmitter<PathData>();
|
|
147
|
+
@Output() pathEvent = new EventEmitter<PathEvent>();
|
|
148
|
+
|
|
149
|
+
@ContentChildren(PathStepDirective) stepDirectives!: QueryList<PathStepDirective>;
|
|
150
|
+
|
|
151
|
+
public readonly facade = inject(PathFacade);
|
|
152
|
+
public started = false;
|
|
153
|
+
|
|
154
|
+
private readonly destroy$ = new Subject<void>();
|
|
155
|
+
|
|
156
|
+
public ngOnInit(): void {
|
|
157
|
+
this.facade.events$.pipe(takeUntil(this.destroy$)).subscribe((event) => {
|
|
158
|
+
this.pathEvent.emit(event);
|
|
159
|
+
if (event.type === "completed") this.completed.emit(event.data);
|
|
160
|
+
if (event.type === "cancelled") this.cancelled.emit(event.data);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (this.autoStart) {
|
|
164
|
+
this.doStart();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public ngOnDestroy(): void {
|
|
169
|
+
this.destroy$.next();
|
|
170
|
+
this.destroy$.complete();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public doStart(): void {
|
|
174
|
+
this.started = true;
|
|
175
|
+
this.facade.start(this.path, this.initialData);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|