ngx-form-draft 2.2.11 → 2.2.13
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/LICENSE +21 -21
- package/README.md +206 -206
- package/esm2020/form-draft-banner.component.mjs +84 -84
- package/esm2020/form-draft.directive.mjs +267 -267
- package/esm2020/form-draft.service.mjs +92 -92
- package/esm2020/index.mjs +4 -4
- package/esm2020/ngx-form-draft.mjs +4 -4
- package/esm2020/ngx-form-draft.module.mjs +19 -19
- package/fesm2015/ngx-form-draft.mjs +446 -446
- package/fesm2015/ngx-form-draft.mjs.map +1 -1
- package/fesm2020/ngx-form-draft.mjs +439 -439
- package/fesm2020/ngx-form-draft.mjs.map +1 -1
- package/form-draft-banner.component.d.ts +14 -14
- package/form-draft.directive.d.ts +56 -56
- package/form-draft.service.d.ts +35 -35
- package/index.d.ts +4 -4
- package/ngx-form-draft.module.d.ts +9 -9
- package/package.json +1 -1
|
@@ -7,453 +7,453 @@ import { filter, debounceTime, takeUntil } from 'rxjs/operators';
|
|
|
7
7
|
import * as i1 from '@angular/common';
|
|
8
8
|
import { CommonModule } from '@angular/common';
|
|
9
9
|
|
|
10
|
-
class FormDraftBannerComponent {
|
|
11
|
-
constructor() {
|
|
12
|
-
this.visible = false;
|
|
13
|
-
this.timeLabel = '';
|
|
14
|
-
this.isRestored = false;
|
|
15
|
-
this.restoredText = 'Draft restored';
|
|
16
|
-
this.savedText = 'Draft saved';
|
|
17
|
-
this.savedLabel = 'saved';
|
|
18
|
-
this.discardText = 'Discard';
|
|
19
|
-
this.discard = new EventEmitter();
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
FormDraftBannerComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftBannerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
23
|
-
FormDraftBannerComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: FormDraftBannerComponent, selector: "ngx-form-draft-banner", inputs: { visible: "visible", timeLabel: "timeLabel", isRestored: "isRestored", restoredText: "restoredText", savedText: "savedText", savedLabel: "savedLabel", discardText: "discardText" }, outputs: { discard: "discard" }, ngImport: i0, template: `
|
|
24
|
-
<div class="form-draft-banner" *ngIf="visible">
|
|
25
|
-
<div class="form-draft-banner__icon">
|
|
26
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
27
|
-
<path d="M12 8V12L14.5 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
28
|
-
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
|
|
29
|
-
</svg>
|
|
30
|
-
</div>
|
|
31
|
-
<div class="form-draft-banner__content">
|
|
32
|
-
<span class="form-draft-banner__text">
|
|
33
|
-
<span *ngIf="isRestored">{{ restoredText }}</span>
|
|
34
|
-
<span *ngIf="!isRestored">{{ savedText }}</span>
|
|
35
|
-
<span class="form-draft-banner__time" *ngIf="timeLabel && isRestored">
|
|
36
|
-
· {{ savedLabel }} {{ timeLabel }}
|
|
37
|
-
</span>
|
|
38
|
-
</span>
|
|
39
|
-
</div>
|
|
40
|
-
<div class="form-draft-banner__actions">
|
|
41
|
-
<button class="form-draft-banner__btn form-draft-banner__btn--discard" (click)="discard.emit()" type="button">
|
|
42
|
-
✕ {{ discardText }}
|
|
43
|
-
</button>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
`, isInline: true, styles: [":host{display:block;width:100%}.form-draft-banner{display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:12px;border-radius:8px;background:linear-gradient(135deg,#eef6ff 0%,#f0f4ff 100%);border:1px solid #c5ddf8;box-shadow:0 2px 8px #128ad614;animation:slideDown .25s cubic-bezier(.4,0,.2,1)}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.form-draft-banner__icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,#128ad6,#22b9ff);color:#fff;flex-shrink:0}.form-draft-banner__content{flex:1;min-width:0}.form-draft-banner__text{font-size:13px;font-weight:600;color:#1a3a5c}.form-draft-banner__time{font-weight:400;color:#6b8aaa;font-size:12px}.form-draft-banner__actions{flex-shrink:0}.form-draft-banner__btn{border:none;padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s ease}.form-draft-banner__btn--discard{background:transparent;color:#8899a6;border:1px solid #d0dce6}.form-draft-banner__btn--discard:hover{background:#fef2f2;color:#e62e43;border-color:#e62e43}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
47
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftBannerComponent, decorators: [{
|
|
48
|
-
type: Component,
|
|
49
|
-
args: [{ selector: 'ngx-form-draft-banner', template: `
|
|
50
|
-
<div class="form-draft-banner" *ngIf="visible">
|
|
51
|
-
<div class="form-draft-banner__icon">
|
|
52
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
53
|
-
<path d="M12 8V12L14.5 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
54
|
-
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
|
|
55
|
-
</svg>
|
|
56
|
-
</div>
|
|
57
|
-
<div class="form-draft-banner__content">
|
|
58
|
-
<span class="form-draft-banner__text">
|
|
59
|
-
<span *ngIf="isRestored">{{ restoredText }}</span>
|
|
60
|
-
<span *ngIf="!isRestored">{{ savedText }}</span>
|
|
61
|
-
<span class="form-draft-banner__time" *ngIf="timeLabel && isRestored">
|
|
62
|
-
· {{ savedLabel }} {{ timeLabel }}
|
|
63
|
-
</span>
|
|
64
|
-
</span>
|
|
65
|
-
</div>
|
|
66
|
-
<div class="form-draft-banner__actions">
|
|
67
|
-
<button class="form-draft-banner__btn form-draft-banner__btn--discard" (click)="discard.emit()" type="button">
|
|
68
|
-
✕ {{ discardText }}
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%}.form-draft-banner{display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:12px;border-radius:8px;background:linear-gradient(135deg,#eef6ff 0%,#f0f4ff 100%);border:1px solid #c5ddf8;box-shadow:0 2px 8px #128ad614;animation:slideDown .25s cubic-bezier(.4,0,.2,1)}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.form-draft-banner__icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,#128ad6,#22b9ff);color:#fff;flex-shrink:0}.form-draft-banner__content{flex:1;min-width:0}.form-draft-banner__text{font-size:13px;font-weight:600;color:#1a3a5c}.form-draft-banner__time{font-weight:400;color:#6b8aaa;font-size:12px}.form-draft-banner__actions{flex-shrink:0}.form-draft-banner__btn{border:none;padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s ease}.form-draft-banner__btn--discard{background:transparent;color:#8899a6;border:1px solid #d0dce6}.form-draft-banner__btn--discard:hover{background:#fef2f2;color:#e62e43;border-color:#e62e43}\n"] }]
|
|
73
|
-
}], propDecorators: { visible: [{
|
|
74
|
-
type: Input
|
|
75
|
-
}], timeLabel: [{
|
|
76
|
-
type: Input
|
|
77
|
-
}], isRestored: [{
|
|
78
|
-
type: Input
|
|
79
|
-
}], restoredText: [{
|
|
80
|
-
type: Input
|
|
81
|
-
}], savedText: [{
|
|
82
|
-
type: Input
|
|
83
|
-
}], savedLabel: [{
|
|
84
|
-
type: Input
|
|
85
|
-
}], discardText: [{
|
|
86
|
-
type: Input
|
|
87
|
-
}], discard: [{
|
|
88
|
-
type: Output
|
|
10
|
+
class FormDraftBannerComponent {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.visible = false;
|
|
13
|
+
this.timeLabel = '';
|
|
14
|
+
this.isRestored = false;
|
|
15
|
+
this.restoredText = 'Draft restored';
|
|
16
|
+
this.savedText = 'Draft saved';
|
|
17
|
+
this.savedLabel = 'saved';
|
|
18
|
+
this.discardText = 'Discard';
|
|
19
|
+
this.discard = new EventEmitter();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
FormDraftBannerComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftBannerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
23
|
+
FormDraftBannerComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: FormDraftBannerComponent, selector: "ngx-form-draft-banner", inputs: { visible: "visible", timeLabel: "timeLabel", isRestored: "isRestored", restoredText: "restoredText", savedText: "savedText", savedLabel: "savedLabel", discardText: "discardText" }, outputs: { discard: "discard" }, ngImport: i0, template: `
|
|
24
|
+
<div class="form-draft-banner" *ngIf="visible">
|
|
25
|
+
<div class="form-draft-banner__icon">
|
|
26
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
27
|
+
<path d="M12 8V12L14.5 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
28
|
+
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
|
|
29
|
+
</svg>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="form-draft-banner__content">
|
|
32
|
+
<span class="form-draft-banner__text">
|
|
33
|
+
<span *ngIf="isRestored">{{ restoredText }}</span>
|
|
34
|
+
<span *ngIf="!isRestored">{{ savedText }}</span>
|
|
35
|
+
<span class="form-draft-banner__time" *ngIf="timeLabel && isRestored">
|
|
36
|
+
· {{ savedLabel }} {{ timeLabel }}
|
|
37
|
+
</span>
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-draft-banner__actions">
|
|
41
|
+
<button class="form-draft-banner__btn form-draft-banner__btn--discard" (click)="discard.emit()" type="button">
|
|
42
|
+
✕ {{ discardText }}
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
`, isInline: true, styles: [":host{display:block;width:100%}.form-draft-banner{display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:12px;border-radius:8px;background:linear-gradient(135deg,#eef6ff 0%,#f0f4ff 100%);border:1px solid #c5ddf8;box-shadow:0 2px 8px #128ad614;animation:slideDown .25s cubic-bezier(.4,0,.2,1)}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.form-draft-banner__icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,#128ad6,#22b9ff);color:#fff;flex-shrink:0}.form-draft-banner__content{flex:1;min-width:0}.form-draft-banner__text{font-size:13px;font-weight:600;color:#1a3a5c}.form-draft-banner__time{font-weight:400;color:#6b8aaa;font-size:12px}.form-draft-banner__actions{flex-shrink:0}.form-draft-banner__btn{border:none;padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s ease}.form-draft-banner__btn--discard{background:transparent;color:#8899a6;border:1px solid #d0dce6}.form-draft-banner__btn--discard:hover{background:#fef2f2;color:#e62e43;border-color:#e62e43}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
47
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftBannerComponent, decorators: [{
|
|
48
|
+
type: Component,
|
|
49
|
+
args: [{ selector: 'ngx-form-draft-banner', template: `
|
|
50
|
+
<div class="form-draft-banner" *ngIf="visible">
|
|
51
|
+
<div class="form-draft-banner__icon">
|
|
52
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
53
|
+
<path d="M12 8V12L14.5 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
54
|
+
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
|
|
55
|
+
</svg>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="form-draft-banner__content">
|
|
58
|
+
<span class="form-draft-banner__text">
|
|
59
|
+
<span *ngIf="isRestored">{{ restoredText }}</span>
|
|
60
|
+
<span *ngIf="!isRestored">{{ savedText }}</span>
|
|
61
|
+
<span class="form-draft-banner__time" *ngIf="timeLabel && isRestored">
|
|
62
|
+
· {{ savedLabel }} {{ timeLabel }}
|
|
63
|
+
</span>
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="form-draft-banner__actions">
|
|
67
|
+
<button class="form-draft-banner__btn form-draft-banner__btn--discard" (click)="discard.emit()" type="button">
|
|
68
|
+
✕ {{ discardText }}
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%}.form-draft-banner{display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:12px;border-radius:8px;background:linear-gradient(135deg,#eef6ff 0%,#f0f4ff 100%);border:1px solid #c5ddf8;box-shadow:0 2px 8px #128ad614;animation:slideDown .25s cubic-bezier(.4,0,.2,1)}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.form-draft-banner__icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,#128ad6,#22b9ff);color:#fff;flex-shrink:0}.form-draft-banner__content{flex:1;min-width:0}.form-draft-banner__text{font-size:13px;font-weight:600;color:#1a3a5c}.form-draft-banner__time{font-weight:400;color:#6b8aaa;font-size:12px}.form-draft-banner__actions{flex-shrink:0}.form-draft-banner__btn{border:none;padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s ease}.form-draft-banner__btn--discard{background:transparent;color:#8899a6;border:1px solid #d0dce6}.form-draft-banner__btn--discard:hover{background:#fef2f2;color:#e62e43;border-color:#e62e43}\n"] }]
|
|
73
|
+
}], propDecorators: { visible: [{
|
|
74
|
+
type: Input
|
|
75
|
+
}], timeLabel: [{
|
|
76
|
+
type: Input
|
|
77
|
+
}], isRestored: [{
|
|
78
|
+
type: Input
|
|
79
|
+
}], restoredText: [{
|
|
80
|
+
type: Input
|
|
81
|
+
}], savedText: [{
|
|
82
|
+
type: Input
|
|
83
|
+
}], savedLabel: [{
|
|
84
|
+
type: Input
|
|
85
|
+
}], discardText: [{
|
|
86
|
+
type: Input
|
|
87
|
+
}], discard: [{
|
|
88
|
+
type: Output
|
|
89
89
|
}] } });
|
|
90
90
|
|
|
91
|
-
class FormDraftService {
|
|
92
|
-
constructor() {
|
|
93
|
-
this.STORAGE_PREFIX = 'form_draft_';
|
|
94
|
-
this.MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
95
|
-
/** Registry of formId -> reset callback (used by directive to reset form + banner) */
|
|
96
|
-
this.resetRegistry = new Map();
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Register a callback to reset the form and draft UI for the given formId.
|
|
100
|
-
* Called by the directive; apps should use clearAndReset(formId) instead.
|
|
101
|
-
*/
|
|
102
|
-
registerReset(formId, resetFn) {
|
|
103
|
-
this.resetRegistry.set(formId, resetFn);
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Unregister the reset callback for the given formId.
|
|
107
|
-
* Called by the directive on destroy.
|
|
108
|
-
*/
|
|
109
|
-
unregisterReset(formId) {
|
|
110
|
-
this.resetRegistry.delete(formId);
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Clear the draft from storage and, if a directive is registered for this formId,
|
|
114
|
-
* reset the form and dismiss the draft banner in one call.
|
|
115
|
-
* Use this on form submit to clear local storage and reset the form in one line.
|
|
116
|
-
*/
|
|
117
|
-
clearAndReset(formId) {
|
|
118
|
-
this.clear(formId);
|
|
119
|
-
this.resetRegistry.get(formId)?.();
|
|
120
|
-
}
|
|
121
|
-
buildKey(formId) {
|
|
122
|
-
return `${this.STORAGE_PREFIX}${formId}`;
|
|
123
|
-
}
|
|
124
|
-
save(formId, values) {
|
|
125
|
-
try {
|
|
126
|
-
const draft = { values, savedAt: Date.now(), formId };
|
|
127
|
-
localStorage.setItem(this.buildKey(formId), JSON.stringify(draft));
|
|
128
|
-
}
|
|
129
|
-
catch (e) {
|
|
130
|
-
console.warn('[FormDraft] Could not save draft:', e);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
load(formId) {
|
|
134
|
-
try {
|
|
135
|
-
const raw = localStorage.getItem(this.buildKey(formId));
|
|
136
|
-
if (!raw)
|
|
137
|
-
return null;
|
|
138
|
-
const draft = JSON.parse(raw);
|
|
139
|
-
if (Date.now() - draft.savedAt > this.MAX_AGE_MS) {
|
|
140
|
-
this.clear(formId);
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
return draft;
|
|
144
|
-
}
|
|
145
|
-
catch (e) {
|
|
146
|
-
console.warn('[FormDraft] Could not load draft:', e);
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
clear(formId) {
|
|
151
|
-
try {
|
|
152
|
-
localStorage.removeItem(this.buildKey(formId));
|
|
153
|
-
}
|
|
154
|
-
catch (e) {
|
|
155
|
-
console.warn('[FormDraft] Could not clear draft:', e);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
formatTimestamp(timestamp) {
|
|
159
|
-
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
160
|
-
if (seconds < 60)
|
|
161
|
-
return 'just now';
|
|
162
|
-
const minutes = Math.floor(seconds / 60);
|
|
163
|
-
if (minutes < 60)
|
|
164
|
-
return `${minutes}m ago`;
|
|
165
|
-
const hours = Math.floor(minutes / 60);
|
|
166
|
-
if (hours < 24)
|
|
167
|
-
return `${hours}h ago`;
|
|
168
|
-
const days = Math.floor(hours / 24);
|
|
169
|
-
return `${days}d ago`;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
FormDraftService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
173
|
-
FormDraftService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, providedIn: 'root' });
|
|
174
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, decorators: [{
|
|
175
|
-
type: Injectable,
|
|
176
|
-
args: [{
|
|
177
|
-
providedIn: 'root'
|
|
178
|
-
}]
|
|
91
|
+
class FormDraftService {
|
|
92
|
+
constructor() {
|
|
93
|
+
this.STORAGE_PREFIX = 'form_draft_';
|
|
94
|
+
this.MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
95
|
+
/** Registry of formId -> reset callback (used by directive to reset form + banner) */
|
|
96
|
+
this.resetRegistry = new Map();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Register a callback to reset the form and draft UI for the given formId.
|
|
100
|
+
* Called by the directive; apps should use clearAndReset(formId) instead.
|
|
101
|
+
*/
|
|
102
|
+
registerReset(formId, resetFn) {
|
|
103
|
+
this.resetRegistry.set(formId, resetFn);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Unregister the reset callback for the given formId.
|
|
107
|
+
* Called by the directive on destroy.
|
|
108
|
+
*/
|
|
109
|
+
unregisterReset(formId) {
|
|
110
|
+
this.resetRegistry.delete(formId);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Clear the draft from storage and, if a directive is registered for this formId,
|
|
114
|
+
* reset the form and dismiss the draft banner in one call.
|
|
115
|
+
* Use this on form submit to clear local storage and reset the form in one line.
|
|
116
|
+
*/
|
|
117
|
+
clearAndReset(formId) {
|
|
118
|
+
this.clear(formId);
|
|
119
|
+
this.resetRegistry.get(formId)?.();
|
|
120
|
+
}
|
|
121
|
+
buildKey(formId) {
|
|
122
|
+
return `${this.STORAGE_PREFIX}${formId}`;
|
|
123
|
+
}
|
|
124
|
+
save(formId, values) {
|
|
125
|
+
try {
|
|
126
|
+
const draft = { values, savedAt: Date.now(), formId };
|
|
127
|
+
localStorage.setItem(this.buildKey(formId), JSON.stringify(draft));
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.warn('[FormDraft] Could not save draft:', e);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
load(formId) {
|
|
134
|
+
try {
|
|
135
|
+
const raw = localStorage.getItem(this.buildKey(formId));
|
|
136
|
+
if (!raw)
|
|
137
|
+
return null;
|
|
138
|
+
const draft = JSON.parse(raw);
|
|
139
|
+
if (Date.now() - draft.savedAt > this.MAX_AGE_MS) {
|
|
140
|
+
this.clear(formId);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return draft;
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
console.warn('[FormDraft] Could not load draft:', e);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
clear(formId) {
|
|
151
|
+
try {
|
|
152
|
+
localStorage.removeItem(this.buildKey(formId));
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
console.warn('[FormDraft] Could not clear draft:', e);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
formatTimestamp(timestamp) {
|
|
159
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
160
|
+
if (seconds < 60)
|
|
161
|
+
return 'just now';
|
|
162
|
+
const minutes = Math.floor(seconds / 60);
|
|
163
|
+
if (minutes < 60)
|
|
164
|
+
return `${minutes}m ago`;
|
|
165
|
+
const hours = Math.floor(minutes / 60);
|
|
166
|
+
if (hours < 24)
|
|
167
|
+
return `${hours}h ago`;
|
|
168
|
+
const days = Math.floor(hours / 24);
|
|
169
|
+
return `${days}d ago`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
FormDraftService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
173
|
+
FormDraftService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, providedIn: 'root' });
|
|
174
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, decorators: [{
|
|
175
|
+
type: Injectable,
|
|
176
|
+
args: [{
|
|
177
|
+
providedIn: 'root'
|
|
178
|
+
}]
|
|
179
179
|
}] });
|
|
180
180
|
|
|
181
|
-
/**
|
|
182
|
-
* Auto-saves and restores form drafts
|
|
183
|
-
*
|
|
184
|
-
* @example
|
|
185
|
-
* <form [formGroup]="myForm" ngxFormDraft="myFormId">
|
|
186
|
-
*
|
|
187
|
-
* @example
|
|
188
|
-
* <form [formGroup]="myForm" [ngxFormDraft]="'edit_' + entityId" [draftExcludeFields]="['password']">
|
|
189
|
-
*/
|
|
190
|
-
class FormDraftDirective {
|
|
191
|
-
constructor(formGroupDir, ngForm, draftService, viewContainerRef, cdRef, elRef, renderer) {
|
|
192
|
-
this.formGroupDir = formGroupDir;
|
|
193
|
-
this.ngForm = ngForm;
|
|
194
|
-
this.draftService = draftService;
|
|
195
|
-
this.viewContainerRef = viewContainerRef;
|
|
196
|
-
this.cdRef = cdRef;
|
|
197
|
-
this.elRef = elRef;
|
|
198
|
-
this.renderer = renderer;
|
|
199
|
-
this.draftDebounce = 800;
|
|
200
|
-
this.draftExcludeFields = [];
|
|
201
|
-
this.draftShowOnChange = false;
|
|
202
|
-
this.draftRestoredText = 'Draft restored';
|
|
203
|
-
this.draftSavedText = 'Draft saved';
|
|
204
|
-
this.draftSavedLabel = 'saved';
|
|
205
|
-
this.draftDiscardText = 'Discard';
|
|
206
|
-
this.destroy$ = new Subject();
|
|
207
|
-
this.bannerRef = null;
|
|
208
|
-
this.formControl = null;
|
|
209
|
-
this.initialValues = {};
|
|
210
|
-
this.isRestoring = false;
|
|
211
|
-
}
|
|
212
|
-
ngOnInit() {
|
|
213
|
-
this.formControl = this.formGroupDir?.form || this.ngForm?.form || null;
|
|
214
|
-
if (!this.formControl || !this.formId)
|
|
215
|
-
return;
|
|
216
|
-
this.draftService.registerReset(this.formId, () => this.performResetAndDestroyBanner());
|
|
217
|
-
// For reactive forms, capture initial values and restore draft immediately
|
|
218
|
-
if (this.formGroupDir) {
|
|
219
|
-
this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
|
|
220
|
-
const draft = this.draftService.load(this.formId);
|
|
221
|
-
if (draft) {
|
|
222
|
-
this.restoreDraft(draft.values);
|
|
223
|
-
this.showBanner(draft.savedAt, true);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
let hasUserInteraction = false;
|
|
227
|
-
this.formControl.valueChanges
|
|
228
|
-
.pipe(filter(() => {
|
|
229
|
-
if (this.isRestoring)
|
|
230
|
-
return false;
|
|
231
|
-
if (this.ngForm && !hasUserInteraction) {
|
|
232
|
-
const currentValues = this.formControl?.value || {};
|
|
233
|
-
const isDifferent = JSON.stringify(currentValues) !== JSON.stringify(this.initialValues);
|
|
234
|
-
if (isDifferent) {
|
|
235
|
-
hasUserInteraction = true;
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
return true;
|
|
241
|
-
}), debounceTime(this.draftDebounce), takeUntil(this.destroy$))
|
|
242
|
-
.subscribe((values) => {
|
|
243
|
-
this.saveDraft(values);
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
ngAfterViewInit() {
|
|
247
|
-
if (this.ngForm && this.formControl) {
|
|
248
|
-
const draft = this.draftService.load(this.formId);
|
|
249
|
-
if (draft) {
|
|
250
|
-
this.isRestoring = true;
|
|
251
|
-
}
|
|
252
|
-
setTimeout(() => {
|
|
253
|
-
if (draft) {
|
|
254
|
-
this.restoreDraft(draft.values);
|
|
255
|
-
this.showBanner(draft.savedAt, true);
|
|
256
|
-
this.initialValues = {};
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
|
|
260
|
-
}
|
|
261
|
-
}, 0);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
ngOnDestroy() {
|
|
265
|
-
this.draftService.unregisterReset(this.formId);
|
|
266
|
-
this.destroy$.next();
|
|
267
|
-
this.destroy$.complete();
|
|
268
|
-
this.destroyBanner();
|
|
269
|
-
}
|
|
270
|
-
saveDraft(values) {
|
|
271
|
-
const filtered = this.filterFields(values);
|
|
272
|
-
// Don't save if empty
|
|
273
|
-
if (this.isAllEmpty(filtered)) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
// Don't save if matches initial values (even if initial is empty)
|
|
277
|
-
if (this.matchesInitialValues(filtered)) {
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
this.draftService.save(this.formId, filtered);
|
|
281
|
-
if (this.draftShowOnChange && !this.bannerRef) {
|
|
282
|
-
this.showBanner(Date.now(), false);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
filterFields(values) {
|
|
286
|
-
if (!this.draftExcludeFields.length)
|
|
287
|
-
return values;
|
|
288
|
-
const result = { ...values };
|
|
289
|
-
this.draftExcludeFields.forEach(field => delete result[field]);
|
|
290
|
-
return result;
|
|
291
|
-
}
|
|
292
|
-
isAllEmpty(values) {
|
|
293
|
-
return Object.values(values).every(v => v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0));
|
|
294
|
-
}
|
|
295
|
-
matchesInitialValues(values) {
|
|
296
|
-
return JSON.stringify(values) === JSON.stringify(this.initialValues);
|
|
297
|
-
}
|
|
298
|
-
restoreDraft(values) {
|
|
299
|
-
if (!this.formControl)
|
|
300
|
-
return;
|
|
301
|
-
this.isRestoring = true;
|
|
302
|
-
const form = this.formGroupDir?.form || this.ngForm?.form;
|
|
303
|
-
if (form) {
|
|
304
|
-
this.prepareFormArrays(form, values);
|
|
305
|
-
form.patchValue(values);
|
|
306
|
-
}
|
|
307
|
-
setTimeout(() => this.isRestoring = false, 100);
|
|
308
|
-
}
|
|
309
|
-
prepareFormArrays(control, value) {
|
|
310
|
-
if (!control || value == null)
|
|
311
|
-
return;
|
|
312
|
-
if (control instanceof FormGroup && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
313
|
-
Object.keys(value).forEach(key => {
|
|
314
|
-
const childControl = control.get(key);
|
|
315
|
-
if (childControl)
|
|
316
|
-
this.prepareFormArrays(childControl, value[key]);
|
|
317
|
-
});
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
if (control instanceof FormArray && Array.isArray(value)) {
|
|
321
|
-
const formArray = control;
|
|
322
|
-
while (formArray.length < value.length) {
|
|
323
|
-
const template = formArray.at(0);
|
|
324
|
-
if (template instanceof FormGroup) {
|
|
325
|
-
const newGroup = new FormGroup({});
|
|
326
|
-
Object.keys(template.controls).forEach(ctrlName => {
|
|
327
|
-
const existing = template.get(ctrlName);
|
|
328
|
-
if (existing instanceof FormArray) {
|
|
329
|
-
newGroup.addControl(ctrlName, new FormArray([]));
|
|
330
|
-
}
|
|
331
|
-
else if (existing instanceof FormGroup) {
|
|
332
|
-
newGroup.addControl(ctrlName, new FormGroup({}));
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
newGroup.addControl(ctrlName, new FormControl(null));
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
formArray.push(newGroup);
|
|
339
|
-
}
|
|
340
|
-
else if (template) {
|
|
341
|
-
formArray.push(new FormControl(null));
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
const firstValue = value[0];
|
|
345
|
-
if (firstValue && typeof firstValue === 'object' && !Array.isArray(firstValue)) {
|
|
346
|
-
const group = new FormGroup({});
|
|
347
|
-
Object.keys(firstValue).forEach(key => group.addControl(key, new FormControl(null)));
|
|
348
|
-
formArray.push(group);
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
formArray.push(new FormControl(null));
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
while (formArray.length > value.length) {
|
|
356
|
-
formArray.removeAt(formArray.length - 1);
|
|
357
|
-
}
|
|
358
|
-
value.forEach((childValue, index) => {
|
|
359
|
-
this.prepareFormArrays(formArray.at(index), childValue);
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
discardDraft() {
|
|
364
|
-
this.draftService.clear(this.formId);
|
|
365
|
-
this.performResetAndDestroyBanner();
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Resets the form to initial values and destroys the draft banner.
|
|
369
|
-
* Used by the service when clearAndReset(formId) is called (e.g. on submit).
|
|
370
|
-
*/
|
|
371
|
-
performResetAndDestroyBanner() {
|
|
372
|
-
if (this.formControl) {
|
|
373
|
-
this.isRestoring = true;
|
|
374
|
-
const form = this.formGroupDir?.form || this.ngForm?.form;
|
|
375
|
-
if (form) {
|
|
376
|
-
this.prepareFormArrays(form, this.initialValues);
|
|
377
|
-
form.reset(this.initialValues);
|
|
378
|
-
}
|
|
379
|
-
setTimeout(() => {
|
|
380
|
-
this.isRestoring = false;
|
|
381
|
-
}, this.draftDebounce + 200);
|
|
382
|
-
}
|
|
383
|
-
this.destroyBanner();
|
|
384
|
-
}
|
|
385
|
-
showBanner(savedAt, isRestored = false) {
|
|
386
|
-
this.bannerRef = this.viewContainerRef.createComponent(FormDraftBannerComponent);
|
|
387
|
-
this.bannerRef.setInput('visible', true);
|
|
388
|
-
this.bannerRef.setInput('isRestored', isRestored);
|
|
389
|
-
this.bannerRef.setInput('timeLabel', isRestored ? this.draftService.formatTimestamp(savedAt) : '');
|
|
390
|
-
this.bannerRef.setInput('restoredText', this.draftRestoredText);
|
|
391
|
-
this.bannerRef.setInput('savedText', this.draftSavedText);
|
|
392
|
-
this.bannerRef.setInput('savedLabel', this.draftSavedLabel);
|
|
393
|
-
this.bannerRef.setInput('discardText', this.draftDiscardText);
|
|
394
|
-
this.bannerRef.instance.discard.subscribe(() => this.discardDraft());
|
|
395
|
-
const bannerEl = this.bannerRef.location.nativeElement;
|
|
396
|
-
const formEl = this.elRef.nativeElement;
|
|
397
|
-
this.renderer.insertBefore(formEl, bannerEl, formEl.firstChild);
|
|
398
|
-
this.cdRef.detectChanges();
|
|
399
|
-
}
|
|
400
|
-
destroyBanner() {
|
|
401
|
-
if (this.bannerRef) {
|
|
402
|
-
this.bannerRef.destroy();
|
|
403
|
-
this.bannerRef = null;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
clearDraft() {
|
|
407
|
-
this.draftService.clear(this.formId);
|
|
408
|
-
this.destroyBanner();
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
FormDraftDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftDirective, deps: [{ token: i1$1.FormGroupDirective, optional: true }, { token: i1$1.NgForm, optional: true }, { token: FormDraftService }, { token: i0.ViewContainerRef }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
|
|
412
|
-
FormDraftDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.3.0", type: FormDraftDirective, selector: "[ngxFormDraft]", inputs: { formId: ["ngxFormDraft", "formId"], draftDebounce: "draftDebounce", draftExcludeFields: "draftExcludeFields", draftShowOnChange: "draftShowOnChange", draftRestoredText: "draftRestoredText", draftSavedText: "draftSavedText", draftSavedLabel: "draftSavedLabel", draftDiscardText: "draftDiscardText" }, ngImport: i0 });
|
|
413
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftDirective, decorators: [{
|
|
414
|
-
type: Directive,
|
|
415
|
-
args: [{
|
|
416
|
-
selector: '[ngxFormDraft]',
|
|
417
|
-
}]
|
|
418
|
-
}], ctorParameters: function () { return [{ type: i1$1.FormGroupDirective, decorators: [{
|
|
419
|
-
type: Optional
|
|
420
|
-
}] }, { type: i1$1.NgForm, decorators: [{
|
|
421
|
-
type: Optional
|
|
422
|
-
}] }, { type: FormDraftService }, { type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.Renderer2 }]; }, propDecorators: { formId: [{
|
|
423
|
-
type: Input,
|
|
424
|
-
args: ['ngxFormDraft']
|
|
425
|
-
}], draftDebounce: [{
|
|
426
|
-
type: Input
|
|
427
|
-
}], draftExcludeFields: [{
|
|
428
|
-
type: Input
|
|
429
|
-
}], draftShowOnChange: [{
|
|
430
|
-
type: Input
|
|
431
|
-
}], draftRestoredText: [{
|
|
432
|
-
type: Input
|
|
433
|
-
}], draftSavedText: [{
|
|
434
|
-
type: Input
|
|
435
|
-
}], draftSavedLabel: [{
|
|
436
|
-
type: Input
|
|
437
|
-
}], draftDiscardText: [{
|
|
438
|
-
type: Input
|
|
181
|
+
/**
|
|
182
|
+
* Auto-saves and restores form drafts
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* <form [formGroup]="myForm" ngxFormDraft="myFormId">
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* <form [formGroup]="myForm" [ngxFormDraft]="'edit_' + entityId" [draftExcludeFields]="['password']">
|
|
189
|
+
*/
|
|
190
|
+
class FormDraftDirective {
|
|
191
|
+
constructor(formGroupDir, ngForm, draftService, viewContainerRef, cdRef, elRef, renderer) {
|
|
192
|
+
this.formGroupDir = formGroupDir;
|
|
193
|
+
this.ngForm = ngForm;
|
|
194
|
+
this.draftService = draftService;
|
|
195
|
+
this.viewContainerRef = viewContainerRef;
|
|
196
|
+
this.cdRef = cdRef;
|
|
197
|
+
this.elRef = elRef;
|
|
198
|
+
this.renderer = renderer;
|
|
199
|
+
this.draftDebounce = 800;
|
|
200
|
+
this.draftExcludeFields = [];
|
|
201
|
+
this.draftShowOnChange = false;
|
|
202
|
+
this.draftRestoredText = 'Draft restored';
|
|
203
|
+
this.draftSavedText = 'Draft saved';
|
|
204
|
+
this.draftSavedLabel = 'saved';
|
|
205
|
+
this.draftDiscardText = 'Discard';
|
|
206
|
+
this.destroy$ = new Subject();
|
|
207
|
+
this.bannerRef = null;
|
|
208
|
+
this.formControl = null;
|
|
209
|
+
this.initialValues = {};
|
|
210
|
+
this.isRestoring = false;
|
|
211
|
+
}
|
|
212
|
+
ngOnInit() {
|
|
213
|
+
this.formControl = this.formGroupDir?.form || this.ngForm?.form || null;
|
|
214
|
+
if (!this.formControl || !this.formId)
|
|
215
|
+
return;
|
|
216
|
+
this.draftService.registerReset(this.formId, () => this.performResetAndDestroyBanner());
|
|
217
|
+
// For reactive forms, capture initial values and restore draft immediately
|
|
218
|
+
if (this.formGroupDir) {
|
|
219
|
+
this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
|
|
220
|
+
const draft = this.draftService.load(this.formId);
|
|
221
|
+
if (draft) {
|
|
222
|
+
this.restoreDraft(draft.values);
|
|
223
|
+
this.showBanner(draft.savedAt, true);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
let hasUserInteraction = false;
|
|
227
|
+
this.formControl.valueChanges
|
|
228
|
+
.pipe(filter(() => {
|
|
229
|
+
if (this.isRestoring)
|
|
230
|
+
return false;
|
|
231
|
+
if (this.ngForm && !hasUserInteraction) {
|
|
232
|
+
const currentValues = this.formControl?.value || {};
|
|
233
|
+
const isDifferent = JSON.stringify(currentValues) !== JSON.stringify(this.initialValues);
|
|
234
|
+
if (isDifferent) {
|
|
235
|
+
hasUserInteraction = true;
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
}), debounceTime(this.draftDebounce), takeUntil(this.destroy$))
|
|
242
|
+
.subscribe((values) => {
|
|
243
|
+
this.saveDraft(values);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
ngAfterViewInit() {
|
|
247
|
+
if (this.ngForm && this.formControl) {
|
|
248
|
+
const draft = this.draftService.load(this.formId);
|
|
249
|
+
if (draft) {
|
|
250
|
+
this.isRestoring = true;
|
|
251
|
+
}
|
|
252
|
+
setTimeout(() => {
|
|
253
|
+
if (draft) {
|
|
254
|
+
this.restoreDraft(draft.values);
|
|
255
|
+
this.showBanner(draft.savedAt, true);
|
|
256
|
+
this.initialValues = {};
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
|
|
260
|
+
}
|
|
261
|
+
}, 0);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
ngOnDestroy() {
|
|
265
|
+
this.draftService.unregisterReset(this.formId);
|
|
266
|
+
this.destroy$.next();
|
|
267
|
+
this.destroy$.complete();
|
|
268
|
+
this.destroyBanner();
|
|
269
|
+
}
|
|
270
|
+
saveDraft(values) {
|
|
271
|
+
const filtered = this.filterFields(values);
|
|
272
|
+
// Don't save if empty
|
|
273
|
+
if (this.isAllEmpty(filtered)) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Don't save if matches initial values (even if initial is empty)
|
|
277
|
+
if (this.matchesInitialValues(filtered)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this.draftService.save(this.formId, filtered);
|
|
281
|
+
if (this.draftShowOnChange && !this.bannerRef) {
|
|
282
|
+
this.showBanner(Date.now(), false);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
filterFields(values) {
|
|
286
|
+
if (!this.draftExcludeFields.length)
|
|
287
|
+
return values;
|
|
288
|
+
const result = { ...values };
|
|
289
|
+
this.draftExcludeFields.forEach(field => delete result[field]);
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
isAllEmpty(values) {
|
|
293
|
+
return Object.values(values).every(v => v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0));
|
|
294
|
+
}
|
|
295
|
+
matchesInitialValues(values) {
|
|
296
|
+
return JSON.stringify(values) === JSON.stringify(this.initialValues);
|
|
297
|
+
}
|
|
298
|
+
restoreDraft(values) {
|
|
299
|
+
if (!this.formControl)
|
|
300
|
+
return;
|
|
301
|
+
this.isRestoring = true;
|
|
302
|
+
const form = this.formGroupDir?.form || this.ngForm?.form;
|
|
303
|
+
if (form) {
|
|
304
|
+
this.prepareFormArrays(form, values);
|
|
305
|
+
form.patchValue(values);
|
|
306
|
+
}
|
|
307
|
+
setTimeout(() => this.isRestoring = false, 100);
|
|
308
|
+
}
|
|
309
|
+
prepareFormArrays(control, value) {
|
|
310
|
+
if (!control || value == null)
|
|
311
|
+
return;
|
|
312
|
+
if (control instanceof FormGroup && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
313
|
+
Object.keys(value).forEach(key => {
|
|
314
|
+
const childControl = control.get(key);
|
|
315
|
+
if (childControl)
|
|
316
|
+
this.prepareFormArrays(childControl, value[key]);
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (control instanceof FormArray && Array.isArray(value)) {
|
|
321
|
+
const formArray = control;
|
|
322
|
+
while (formArray.length < value.length) {
|
|
323
|
+
const template = formArray.at(0);
|
|
324
|
+
if (template instanceof FormGroup) {
|
|
325
|
+
const newGroup = new FormGroup({});
|
|
326
|
+
Object.keys(template.controls).forEach(ctrlName => {
|
|
327
|
+
const existing = template.get(ctrlName);
|
|
328
|
+
if (existing instanceof FormArray) {
|
|
329
|
+
newGroup.addControl(ctrlName, new FormArray([]));
|
|
330
|
+
}
|
|
331
|
+
else if (existing instanceof FormGroup) {
|
|
332
|
+
newGroup.addControl(ctrlName, new FormGroup({}));
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
newGroup.addControl(ctrlName, new FormControl(null));
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
formArray.push(newGroup);
|
|
339
|
+
}
|
|
340
|
+
else if (template) {
|
|
341
|
+
formArray.push(new FormControl(null));
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
const firstValue = value[0];
|
|
345
|
+
if (firstValue && typeof firstValue === 'object' && !Array.isArray(firstValue)) {
|
|
346
|
+
const group = new FormGroup({});
|
|
347
|
+
Object.keys(firstValue).forEach(key => group.addControl(key, new FormControl(null)));
|
|
348
|
+
formArray.push(group);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
formArray.push(new FormControl(null));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
while (formArray.length > value.length) {
|
|
356
|
+
formArray.removeAt(formArray.length - 1);
|
|
357
|
+
}
|
|
358
|
+
value.forEach((childValue, index) => {
|
|
359
|
+
this.prepareFormArrays(formArray.at(index), childValue);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
discardDraft() {
|
|
364
|
+
this.draftService.clear(this.formId);
|
|
365
|
+
this.performResetAndDestroyBanner();
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Resets the form to initial values and destroys the draft banner.
|
|
369
|
+
* Used by the service when clearAndReset(formId) is called (e.g. on submit).
|
|
370
|
+
*/
|
|
371
|
+
performResetAndDestroyBanner() {
|
|
372
|
+
if (this.formControl) {
|
|
373
|
+
this.isRestoring = true;
|
|
374
|
+
const form = this.formGroupDir?.form || this.ngForm?.form;
|
|
375
|
+
if (form) {
|
|
376
|
+
this.prepareFormArrays(form, this.initialValues);
|
|
377
|
+
form.reset(this.initialValues);
|
|
378
|
+
}
|
|
379
|
+
setTimeout(() => {
|
|
380
|
+
this.isRestoring = false;
|
|
381
|
+
}, this.draftDebounce + 200);
|
|
382
|
+
}
|
|
383
|
+
this.destroyBanner();
|
|
384
|
+
}
|
|
385
|
+
showBanner(savedAt, isRestored = false) {
|
|
386
|
+
this.bannerRef = this.viewContainerRef.createComponent(FormDraftBannerComponent);
|
|
387
|
+
this.bannerRef.setInput('visible', true);
|
|
388
|
+
this.bannerRef.setInput('isRestored', isRestored);
|
|
389
|
+
this.bannerRef.setInput('timeLabel', isRestored ? this.draftService.formatTimestamp(savedAt) : '');
|
|
390
|
+
this.bannerRef.setInput('restoredText', this.draftRestoredText);
|
|
391
|
+
this.bannerRef.setInput('savedText', this.draftSavedText);
|
|
392
|
+
this.bannerRef.setInput('savedLabel', this.draftSavedLabel);
|
|
393
|
+
this.bannerRef.setInput('discardText', this.draftDiscardText);
|
|
394
|
+
this.bannerRef.instance.discard.subscribe(() => this.discardDraft());
|
|
395
|
+
const bannerEl = this.bannerRef.location.nativeElement;
|
|
396
|
+
const formEl = this.elRef.nativeElement;
|
|
397
|
+
this.renderer.insertBefore(formEl, bannerEl, formEl.firstChild);
|
|
398
|
+
this.cdRef.detectChanges();
|
|
399
|
+
}
|
|
400
|
+
destroyBanner() {
|
|
401
|
+
if (this.bannerRef) {
|
|
402
|
+
this.bannerRef.destroy();
|
|
403
|
+
this.bannerRef = null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
clearDraft() {
|
|
407
|
+
this.draftService.clear(this.formId);
|
|
408
|
+
this.destroyBanner();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
FormDraftDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftDirective, deps: [{ token: i1$1.FormGroupDirective, optional: true }, { token: i1$1.NgForm, optional: true }, { token: FormDraftService }, { token: i0.ViewContainerRef }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
|
|
412
|
+
FormDraftDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.3.0", type: FormDraftDirective, selector: "[ngxFormDraft]", inputs: { formId: ["ngxFormDraft", "formId"], draftDebounce: "draftDebounce", draftExcludeFields: "draftExcludeFields", draftShowOnChange: "draftShowOnChange", draftRestoredText: "draftRestoredText", draftSavedText: "draftSavedText", draftSavedLabel: "draftSavedLabel", draftDiscardText: "draftDiscardText" }, ngImport: i0 });
|
|
413
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftDirective, decorators: [{
|
|
414
|
+
type: Directive,
|
|
415
|
+
args: [{
|
|
416
|
+
selector: '[ngxFormDraft]',
|
|
417
|
+
}]
|
|
418
|
+
}], ctorParameters: function () { return [{ type: i1$1.FormGroupDirective, decorators: [{
|
|
419
|
+
type: Optional
|
|
420
|
+
}] }, { type: i1$1.NgForm, decorators: [{
|
|
421
|
+
type: Optional
|
|
422
|
+
}] }, { type: FormDraftService }, { type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.Renderer2 }]; }, propDecorators: { formId: [{
|
|
423
|
+
type: Input,
|
|
424
|
+
args: ['ngxFormDraft']
|
|
425
|
+
}], draftDebounce: [{
|
|
426
|
+
type: Input
|
|
427
|
+
}], draftExcludeFields: [{
|
|
428
|
+
type: Input
|
|
429
|
+
}], draftShowOnChange: [{
|
|
430
|
+
type: Input
|
|
431
|
+
}], draftRestoredText: [{
|
|
432
|
+
type: Input
|
|
433
|
+
}], draftSavedText: [{
|
|
434
|
+
type: Input
|
|
435
|
+
}], draftSavedLabel: [{
|
|
436
|
+
type: Input
|
|
437
|
+
}], draftDiscardText: [{
|
|
438
|
+
type: Input
|
|
439
439
|
}] } });
|
|
440
440
|
|
|
441
|
-
class NgxFormDraftModule {
|
|
442
|
-
}
|
|
443
|
-
NgxFormDraftModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
444
|
-
NgxFormDraftModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, declarations: [FormDraftDirective, FormDraftBannerComponent], imports: [CommonModule], exports: [FormDraftDirective, FormDraftBannerComponent] });
|
|
445
|
-
NgxFormDraftModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, imports: [CommonModule] });
|
|
446
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, decorators: [{
|
|
447
|
-
type: NgModule,
|
|
448
|
-
args: [{
|
|
449
|
-
declarations: [FormDraftDirective, FormDraftBannerComponent],
|
|
450
|
-
imports: [CommonModule],
|
|
451
|
-
exports: [FormDraftDirective, FormDraftBannerComponent],
|
|
452
|
-
}]
|
|
441
|
+
class NgxFormDraftModule {
|
|
442
|
+
}
|
|
443
|
+
NgxFormDraftModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
444
|
+
NgxFormDraftModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, declarations: [FormDraftDirective, FormDraftBannerComponent], imports: [CommonModule], exports: [FormDraftDirective, FormDraftBannerComponent] });
|
|
445
|
+
NgxFormDraftModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, imports: [CommonModule] });
|
|
446
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, decorators: [{
|
|
447
|
+
type: NgModule,
|
|
448
|
+
args: [{
|
|
449
|
+
declarations: [FormDraftDirective, FormDraftBannerComponent],
|
|
450
|
+
imports: [CommonModule],
|
|
451
|
+
exports: [FormDraftDirective, FormDraftBannerComponent],
|
|
452
|
+
}]
|
|
453
453
|
}] });
|
|
454
454
|
|
|
455
|
-
/**
|
|
456
|
-
* Generated bundle index. Do not edit.
|
|
455
|
+
/**
|
|
456
|
+
* Generated bundle index. Do not edit.
|
|
457
457
|
*/
|
|
458
458
|
|
|
459
459
|
export { FormDraftBannerComponent, FormDraftDirective, FormDraftService, NgxFormDraftModule };
|