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.
@@ -7,460 +7,460 @@ 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
- &middot; {{ 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
- &middot; {{ 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
+ &middot; {{ 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
+ &middot; {{ 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
- var _a;
119
- this.clear(formId);
120
- (_a = this.resetRegistry.get(formId)) === null || _a === void 0 ? void 0 : _a();
121
- }
122
- buildKey(formId) {
123
- return `${this.STORAGE_PREFIX}${formId}`;
124
- }
125
- save(formId, values) {
126
- try {
127
- const draft = { values, savedAt: Date.now(), formId };
128
- localStorage.setItem(this.buildKey(formId), JSON.stringify(draft));
129
- }
130
- catch (e) {
131
- console.warn('[FormDraft] Could not save draft:', e);
132
- }
133
- }
134
- load(formId) {
135
- try {
136
- const raw = localStorage.getItem(this.buildKey(formId));
137
- if (!raw)
138
- return null;
139
- const draft = JSON.parse(raw);
140
- if (Date.now() - draft.savedAt > this.MAX_AGE_MS) {
141
- this.clear(formId);
142
- return null;
143
- }
144
- return draft;
145
- }
146
- catch (e) {
147
- console.warn('[FormDraft] Could not load draft:', e);
148
- return null;
149
- }
150
- }
151
- clear(formId) {
152
- try {
153
- localStorage.removeItem(this.buildKey(formId));
154
- }
155
- catch (e) {
156
- console.warn('[FormDraft] Could not clear draft:', e);
157
- }
158
- }
159
- formatTimestamp(timestamp) {
160
- const seconds = Math.floor((Date.now() - timestamp) / 1000);
161
- if (seconds < 60)
162
- return 'just now';
163
- const minutes = Math.floor(seconds / 60);
164
- if (minutes < 60)
165
- return `${minutes}m ago`;
166
- const hours = Math.floor(minutes / 60);
167
- if (hours < 24)
168
- return `${hours}h ago`;
169
- const days = Math.floor(hours / 24);
170
- return `${days}d ago`;
171
- }
172
- }
173
- FormDraftService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
174
- FormDraftService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, providedIn: 'root' });
175
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, decorators: [{
176
- type: Injectable,
177
- args: [{
178
- providedIn: 'root'
179
- }]
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
+ var _a;
119
+ this.clear(formId);
120
+ (_a = this.resetRegistry.get(formId)) === null || _a === void 0 ? void 0 : _a();
121
+ }
122
+ buildKey(formId) {
123
+ return `${this.STORAGE_PREFIX}${formId}`;
124
+ }
125
+ save(formId, values) {
126
+ try {
127
+ const draft = { values, savedAt: Date.now(), formId };
128
+ localStorage.setItem(this.buildKey(formId), JSON.stringify(draft));
129
+ }
130
+ catch (e) {
131
+ console.warn('[FormDraft] Could not save draft:', e);
132
+ }
133
+ }
134
+ load(formId) {
135
+ try {
136
+ const raw = localStorage.getItem(this.buildKey(formId));
137
+ if (!raw)
138
+ return null;
139
+ const draft = JSON.parse(raw);
140
+ if (Date.now() - draft.savedAt > this.MAX_AGE_MS) {
141
+ this.clear(formId);
142
+ return null;
143
+ }
144
+ return draft;
145
+ }
146
+ catch (e) {
147
+ console.warn('[FormDraft] Could not load draft:', e);
148
+ return null;
149
+ }
150
+ }
151
+ clear(formId) {
152
+ try {
153
+ localStorage.removeItem(this.buildKey(formId));
154
+ }
155
+ catch (e) {
156
+ console.warn('[FormDraft] Could not clear draft:', e);
157
+ }
158
+ }
159
+ formatTimestamp(timestamp) {
160
+ const seconds = Math.floor((Date.now() - timestamp) / 1000);
161
+ if (seconds < 60)
162
+ return 'just now';
163
+ const minutes = Math.floor(seconds / 60);
164
+ if (minutes < 60)
165
+ return `${minutes}m ago`;
166
+ const hours = Math.floor(minutes / 60);
167
+ if (hours < 24)
168
+ return `${hours}h ago`;
169
+ const days = Math.floor(hours / 24);
170
+ return `${days}d ago`;
171
+ }
172
+ }
173
+ FormDraftService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
174
+ FormDraftService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, providedIn: 'root' });
175
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, decorators: [{
176
+ type: Injectable,
177
+ args: [{
178
+ providedIn: 'root'
179
+ }]
180
180
  }] });
181
181
 
182
- /**
183
- * Auto-saves and restores form drafts
184
- *
185
- * @example
186
- * <form [formGroup]="myForm" ngxFormDraft="myFormId">
187
- *
188
- * @example
189
- * <form [formGroup]="myForm" [ngxFormDraft]="'edit_' + entityId" [draftExcludeFields]="['password']">
190
- */
191
- class FormDraftDirective {
192
- constructor(formGroupDir, ngForm, draftService, viewContainerRef, cdRef, elRef, renderer) {
193
- this.formGroupDir = formGroupDir;
194
- this.ngForm = ngForm;
195
- this.draftService = draftService;
196
- this.viewContainerRef = viewContainerRef;
197
- this.cdRef = cdRef;
198
- this.elRef = elRef;
199
- this.renderer = renderer;
200
- this.draftDebounce = 800;
201
- this.draftExcludeFields = [];
202
- this.draftShowOnChange = false;
203
- this.draftRestoredText = 'Draft restored';
204
- this.draftSavedText = 'Draft saved';
205
- this.draftSavedLabel = 'saved';
206
- this.draftDiscardText = 'Discard';
207
- this.destroy$ = new Subject();
208
- this.bannerRef = null;
209
- this.formControl = null;
210
- this.initialValues = {};
211
- this.isRestoring = false;
212
- }
213
- ngOnInit() {
214
- var _a, _b;
215
- this.formControl = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form) || null;
216
- if (!this.formControl || !this.formId)
217
- return;
218
- this.draftService.registerReset(this.formId, () => this.performResetAndDestroyBanner());
219
- // For reactive forms, capture initial values and restore draft immediately
220
- if (this.formGroupDir) {
221
- this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
222
- const draft = this.draftService.load(this.formId);
223
- if (draft) {
224
- this.restoreDraft(draft.values);
225
- this.showBanner(draft.savedAt, true);
226
- }
227
- }
228
- let hasUserInteraction = false;
229
- this.formControl.valueChanges
230
- .pipe(filter(() => {
231
- var _a;
232
- if (this.isRestoring)
233
- return false;
234
- if (this.ngForm && !hasUserInteraction) {
235
- const currentValues = ((_a = this.formControl) === null || _a === void 0 ? void 0 : _a.value) || {};
236
- const isDifferent = JSON.stringify(currentValues) !== JSON.stringify(this.initialValues);
237
- if (isDifferent) {
238
- hasUserInteraction = true;
239
- return true;
240
- }
241
- return false;
242
- }
243
- return true;
244
- }), debounceTime(this.draftDebounce), takeUntil(this.destroy$))
245
- .subscribe((values) => {
246
- this.saveDraft(values);
247
- });
248
- }
249
- ngAfterViewInit() {
250
- if (this.ngForm && this.formControl) {
251
- const draft = this.draftService.load(this.formId);
252
- if (draft) {
253
- this.isRestoring = true;
254
- }
255
- setTimeout(() => {
256
- if (draft) {
257
- this.restoreDraft(draft.values);
258
- this.showBanner(draft.savedAt, true);
259
- this.initialValues = {};
260
- }
261
- else {
262
- this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
263
- }
264
- }, 0);
265
- }
266
- }
267
- ngOnDestroy() {
268
- this.draftService.unregisterReset(this.formId);
269
- this.destroy$.next();
270
- this.destroy$.complete();
271
- this.destroyBanner();
272
- }
273
- saveDraft(values) {
274
- const filtered = this.filterFields(values);
275
- // Don't save if empty
276
- if (this.isAllEmpty(filtered)) {
277
- return;
278
- }
279
- // Don't save if matches initial values (even if initial is empty)
280
- if (this.matchesInitialValues(filtered)) {
281
- return;
282
- }
283
- this.draftService.save(this.formId, filtered);
284
- if (this.draftShowOnChange && !this.bannerRef) {
285
- this.showBanner(Date.now(), false);
286
- }
287
- }
288
- filterFields(values) {
289
- if (!this.draftExcludeFields.length)
290
- return values;
291
- const result = Object.assign({}, values);
292
- this.draftExcludeFields.forEach(field => delete result[field]);
293
- return result;
294
- }
295
- isAllEmpty(values) {
296
- return Object.values(values).every(v => v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0));
297
- }
298
- matchesInitialValues(values) {
299
- return JSON.stringify(values) === JSON.stringify(this.initialValues);
300
- }
301
- restoreDraft(values) {
302
- var _a, _b;
303
- if (!this.formControl)
304
- return;
305
- this.isRestoring = true;
306
- const form = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form);
307
- if (form) {
308
- this.prepareFormArrays(form, values);
309
- form.patchValue(values);
310
- }
311
- setTimeout(() => this.isRestoring = false, 100);
312
- }
313
- prepareFormArrays(control, value) {
314
- if (!control || value == null)
315
- return;
316
- if (control instanceof FormGroup && value && typeof value === 'object' && !Array.isArray(value)) {
317
- Object.keys(value).forEach(key => {
318
- const childControl = control.get(key);
319
- if (childControl)
320
- this.prepareFormArrays(childControl, value[key]);
321
- });
322
- return;
323
- }
324
- if (control instanceof FormArray && Array.isArray(value)) {
325
- const formArray = control;
326
- while (formArray.length < value.length) {
327
- const template = formArray.at(0);
328
- if (template instanceof FormGroup) {
329
- const newGroup = new FormGroup({});
330
- Object.keys(template.controls).forEach(ctrlName => {
331
- const existing = template.get(ctrlName);
332
- if (existing instanceof FormArray) {
333
- newGroup.addControl(ctrlName, new FormArray([]));
334
- }
335
- else if (existing instanceof FormGroup) {
336
- newGroup.addControl(ctrlName, new FormGroup({}));
337
- }
338
- else {
339
- newGroup.addControl(ctrlName, new FormControl(null));
340
- }
341
- });
342
- formArray.push(newGroup);
343
- }
344
- else if (template) {
345
- formArray.push(new FormControl(null));
346
- }
347
- else {
348
- const firstValue = value[0];
349
- if (firstValue && typeof firstValue === 'object' && !Array.isArray(firstValue)) {
350
- const group = new FormGroup({});
351
- Object.keys(firstValue).forEach(key => group.addControl(key, new FormControl(null)));
352
- formArray.push(group);
353
- }
354
- else {
355
- formArray.push(new FormControl(null));
356
- }
357
- }
358
- }
359
- while (formArray.length > value.length) {
360
- formArray.removeAt(formArray.length - 1);
361
- }
362
- value.forEach((childValue, index) => {
363
- this.prepareFormArrays(formArray.at(index), childValue);
364
- });
365
- }
366
- }
367
- discardDraft() {
368
- this.draftService.clear(this.formId);
369
- this.performResetAndDestroyBanner();
370
- }
371
- /**
372
- * Resets the form to initial values and destroys the draft banner.
373
- * Used by the service when clearAndReset(formId) is called (e.g. on submit).
374
- */
375
- performResetAndDestroyBanner() {
376
- var _a, _b;
377
- if (this.formControl) {
378
- this.isRestoring = true;
379
- const form = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form);
380
- if (form) {
381
- this.prepareFormArrays(form, this.initialValues);
382
- form.reset(this.initialValues);
383
- }
384
- setTimeout(() => {
385
- this.isRestoring = false;
386
- }, this.draftDebounce + 200);
387
- }
388
- this.destroyBanner();
389
- }
390
- showBanner(savedAt, isRestored = false) {
391
- this.bannerRef = this.viewContainerRef.createComponent(FormDraftBannerComponent);
392
- this.bannerRef.setInput('visible', true);
393
- this.bannerRef.setInput('isRestored', isRestored);
394
- this.bannerRef.setInput('timeLabel', isRestored ? this.draftService.formatTimestamp(savedAt) : '');
395
- this.bannerRef.setInput('restoredText', this.draftRestoredText);
396
- this.bannerRef.setInput('savedText', this.draftSavedText);
397
- this.bannerRef.setInput('savedLabel', this.draftSavedLabel);
398
- this.bannerRef.setInput('discardText', this.draftDiscardText);
399
- this.bannerRef.instance.discard.subscribe(() => this.discardDraft());
400
- const bannerEl = this.bannerRef.location.nativeElement;
401
- const formEl = this.elRef.nativeElement;
402
- this.renderer.insertBefore(formEl, bannerEl, formEl.firstChild);
403
- this.cdRef.detectChanges();
404
- }
405
- destroyBanner() {
406
- if (this.bannerRef) {
407
- this.bannerRef.destroy();
408
- this.bannerRef = null;
409
- }
410
- }
411
- clearDraft() {
412
- this.draftService.clear(this.formId);
413
- this.destroyBanner();
414
- }
415
- }
416
- 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 });
417
- 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 });
418
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftDirective, decorators: [{
419
- type: Directive,
420
- args: [{
421
- selector: '[ngxFormDraft]',
422
- }]
423
- }], ctorParameters: function () {
424
- return [{ type: i1$1.FormGroupDirective, decorators: [{
425
- type: Optional
426
- }] }, { type: i1$1.NgForm, decorators: [{
427
- type: Optional
428
- }] }, { type: FormDraftService }, { type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.Renderer2 }];
429
- }, propDecorators: { formId: [{
430
- type: Input,
431
- args: ['ngxFormDraft']
432
- }], draftDebounce: [{
433
- type: Input
434
- }], draftExcludeFields: [{
435
- type: Input
436
- }], draftShowOnChange: [{
437
- type: Input
438
- }], draftRestoredText: [{
439
- type: Input
440
- }], draftSavedText: [{
441
- type: Input
442
- }], draftSavedLabel: [{
443
- type: Input
444
- }], draftDiscardText: [{
445
- type: Input
182
+ /**
183
+ * Auto-saves and restores form drafts
184
+ *
185
+ * @example
186
+ * <form [formGroup]="myForm" ngxFormDraft="myFormId">
187
+ *
188
+ * @example
189
+ * <form [formGroup]="myForm" [ngxFormDraft]="'edit_' + entityId" [draftExcludeFields]="['password']">
190
+ */
191
+ class FormDraftDirective {
192
+ constructor(formGroupDir, ngForm, draftService, viewContainerRef, cdRef, elRef, renderer) {
193
+ this.formGroupDir = formGroupDir;
194
+ this.ngForm = ngForm;
195
+ this.draftService = draftService;
196
+ this.viewContainerRef = viewContainerRef;
197
+ this.cdRef = cdRef;
198
+ this.elRef = elRef;
199
+ this.renderer = renderer;
200
+ this.draftDebounce = 800;
201
+ this.draftExcludeFields = [];
202
+ this.draftShowOnChange = false;
203
+ this.draftRestoredText = 'Draft restored';
204
+ this.draftSavedText = 'Draft saved';
205
+ this.draftSavedLabel = 'saved';
206
+ this.draftDiscardText = 'Discard';
207
+ this.destroy$ = new Subject();
208
+ this.bannerRef = null;
209
+ this.formControl = null;
210
+ this.initialValues = {};
211
+ this.isRestoring = false;
212
+ }
213
+ ngOnInit() {
214
+ var _a, _b;
215
+ this.formControl = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form) || null;
216
+ if (!this.formControl || !this.formId)
217
+ return;
218
+ this.draftService.registerReset(this.formId, () => this.performResetAndDestroyBanner());
219
+ // For reactive forms, capture initial values and restore draft immediately
220
+ if (this.formGroupDir) {
221
+ this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
222
+ const draft = this.draftService.load(this.formId);
223
+ if (draft) {
224
+ this.restoreDraft(draft.values);
225
+ this.showBanner(draft.savedAt, true);
226
+ }
227
+ }
228
+ let hasUserInteraction = false;
229
+ this.formControl.valueChanges
230
+ .pipe(filter(() => {
231
+ var _a;
232
+ if (this.isRestoring)
233
+ return false;
234
+ if (this.ngForm && !hasUserInteraction) {
235
+ const currentValues = ((_a = this.formControl) === null || _a === void 0 ? void 0 : _a.value) || {};
236
+ const isDifferent = JSON.stringify(currentValues) !== JSON.stringify(this.initialValues);
237
+ if (isDifferent) {
238
+ hasUserInteraction = true;
239
+ return true;
240
+ }
241
+ return false;
242
+ }
243
+ return true;
244
+ }), debounceTime(this.draftDebounce), takeUntil(this.destroy$))
245
+ .subscribe((values) => {
246
+ this.saveDraft(values);
247
+ });
248
+ }
249
+ ngAfterViewInit() {
250
+ if (this.ngForm && this.formControl) {
251
+ const draft = this.draftService.load(this.formId);
252
+ if (draft) {
253
+ this.isRestoring = true;
254
+ }
255
+ setTimeout(() => {
256
+ if (draft) {
257
+ this.restoreDraft(draft.values);
258
+ this.showBanner(draft.savedAt, true);
259
+ this.initialValues = {};
260
+ }
261
+ else {
262
+ this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
263
+ }
264
+ }, 0);
265
+ }
266
+ }
267
+ ngOnDestroy() {
268
+ this.draftService.unregisterReset(this.formId);
269
+ this.destroy$.next();
270
+ this.destroy$.complete();
271
+ this.destroyBanner();
272
+ }
273
+ saveDraft(values) {
274
+ const filtered = this.filterFields(values);
275
+ // Don't save if empty
276
+ if (this.isAllEmpty(filtered)) {
277
+ return;
278
+ }
279
+ // Don't save if matches initial values (even if initial is empty)
280
+ if (this.matchesInitialValues(filtered)) {
281
+ return;
282
+ }
283
+ this.draftService.save(this.formId, filtered);
284
+ if (this.draftShowOnChange && !this.bannerRef) {
285
+ this.showBanner(Date.now(), false);
286
+ }
287
+ }
288
+ filterFields(values) {
289
+ if (!this.draftExcludeFields.length)
290
+ return values;
291
+ const result = Object.assign({}, values);
292
+ this.draftExcludeFields.forEach(field => delete result[field]);
293
+ return result;
294
+ }
295
+ isAllEmpty(values) {
296
+ return Object.values(values).every(v => v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0));
297
+ }
298
+ matchesInitialValues(values) {
299
+ return JSON.stringify(values) === JSON.stringify(this.initialValues);
300
+ }
301
+ restoreDraft(values) {
302
+ var _a, _b;
303
+ if (!this.formControl)
304
+ return;
305
+ this.isRestoring = true;
306
+ const form = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form);
307
+ if (form) {
308
+ this.prepareFormArrays(form, values);
309
+ form.patchValue(values);
310
+ }
311
+ setTimeout(() => this.isRestoring = false, 100);
312
+ }
313
+ prepareFormArrays(control, value) {
314
+ if (!control || value == null)
315
+ return;
316
+ if (control instanceof FormGroup && value && typeof value === 'object' && !Array.isArray(value)) {
317
+ Object.keys(value).forEach(key => {
318
+ const childControl = control.get(key);
319
+ if (childControl)
320
+ this.prepareFormArrays(childControl, value[key]);
321
+ });
322
+ return;
323
+ }
324
+ if (control instanceof FormArray && Array.isArray(value)) {
325
+ const formArray = control;
326
+ while (formArray.length < value.length) {
327
+ const template = formArray.at(0);
328
+ if (template instanceof FormGroup) {
329
+ const newGroup = new FormGroup({});
330
+ Object.keys(template.controls).forEach(ctrlName => {
331
+ const existing = template.get(ctrlName);
332
+ if (existing instanceof FormArray) {
333
+ newGroup.addControl(ctrlName, new FormArray([]));
334
+ }
335
+ else if (existing instanceof FormGroup) {
336
+ newGroup.addControl(ctrlName, new FormGroup({}));
337
+ }
338
+ else {
339
+ newGroup.addControl(ctrlName, new FormControl(null));
340
+ }
341
+ });
342
+ formArray.push(newGroup);
343
+ }
344
+ else if (template) {
345
+ formArray.push(new FormControl(null));
346
+ }
347
+ else {
348
+ const firstValue = value[0];
349
+ if (firstValue && typeof firstValue === 'object' && !Array.isArray(firstValue)) {
350
+ const group = new FormGroup({});
351
+ Object.keys(firstValue).forEach(key => group.addControl(key, new FormControl(null)));
352
+ formArray.push(group);
353
+ }
354
+ else {
355
+ formArray.push(new FormControl(null));
356
+ }
357
+ }
358
+ }
359
+ while (formArray.length > value.length) {
360
+ formArray.removeAt(formArray.length - 1);
361
+ }
362
+ value.forEach((childValue, index) => {
363
+ this.prepareFormArrays(formArray.at(index), childValue);
364
+ });
365
+ }
366
+ }
367
+ discardDraft() {
368
+ this.draftService.clear(this.formId);
369
+ this.performResetAndDestroyBanner();
370
+ }
371
+ /**
372
+ * Resets the form to initial values and destroys the draft banner.
373
+ * Used by the service when clearAndReset(formId) is called (e.g. on submit).
374
+ */
375
+ performResetAndDestroyBanner() {
376
+ var _a, _b;
377
+ if (this.formControl) {
378
+ this.isRestoring = true;
379
+ const form = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form);
380
+ if (form) {
381
+ this.prepareFormArrays(form, this.initialValues);
382
+ form.reset(this.initialValues);
383
+ }
384
+ setTimeout(() => {
385
+ this.isRestoring = false;
386
+ }, this.draftDebounce + 200);
387
+ }
388
+ this.destroyBanner();
389
+ }
390
+ showBanner(savedAt, isRestored = false) {
391
+ this.bannerRef = this.viewContainerRef.createComponent(FormDraftBannerComponent);
392
+ this.bannerRef.setInput('visible', true);
393
+ this.bannerRef.setInput('isRestored', isRestored);
394
+ this.bannerRef.setInput('timeLabel', isRestored ? this.draftService.formatTimestamp(savedAt) : '');
395
+ this.bannerRef.setInput('restoredText', this.draftRestoredText);
396
+ this.bannerRef.setInput('savedText', this.draftSavedText);
397
+ this.bannerRef.setInput('savedLabel', this.draftSavedLabel);
398
+ this.bannerRef.setInput('discardText', this.draftDiscardText);
399
+ this.bannerRef.instance.discard.subscribe(() => this.discardDraft());
400
+ const bannerEl = this.bannerRef.location.nativeElement;
401
+ const formEl = this.elRef.nativeElement;
402
+ this.renderer.insertBefore(formEl, bannerEl, formEl.firstChild);
403
+ this.cdRef.detectChanges();
404
+ }
405
+ destroyBanner() {
406
+ if (this.bannerRef) {
407
+ this.bannerRef.destroy();
408
+ this.bannerRef = null;
409
+ }
410
+ }
411
+ clearDraft() {
412
+ this.draftService.clear(this.formId);
413
+ this.destroyBanner();
414
+ }
415
+ }
416
+ 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 });
417
+ 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 });
418
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftDirective, decorators: [{
419
+ type: Directive,
420
+ args: [{
421
+ selector: '[ngxFormDraft]',
422
+ }]
423
+ }], ctorParameters: function () {
424
+ return [{ type: i1$1.FormGroupDirective, decorators: [{
425
+ type: Optional
426
+ }] }, { type: i1$1.NgForm, decorators: [{
427
+ type: Optional
428
+ }] }, { type: FormDraftService }, { type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.Renderer2 }];
429
+ }, propDecorators: { formId: [{
430
+ type: Input,
431
+ args: ['ngxFormDraft']
432
+ }], draftDebounce: [{
433
+ type: Input
434
+ }], draftExcludeFields: [{
435
+ type: Input
436
+ }], draftShowOnChange: [{
437
+ type: Input
438
+ }], draftRestoredText: [{
439
+ type: Input
440
+ }], draftSavedText: [{
441
+ type: Input
442
+ }], draftSavedLabel: [{
443
+ type: Input
444
+ }], draftDiscardText: [{
445
+ type: Input
446
446
  }] } });
447
447
 
448
- class NgxFormDraftModule {
449
- }
450
- NgxFormDraftModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
451
- NgxFormDraftModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, declarations: [FormDraftDirective, FormDraftBannerComponent], imports: [CommonModule], exports: [FormDraftDirective, FormDraftBannerComponent] });
452
- NgxFormDraftModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, imports: [CommonModule] });
453
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, decorators: [{
454
- type: NgModule,
455
- args: [{
456
- declarations: [FormDraftDirective, FormDraftBannerComponent],
457
- imports: [CommonModule],
458
- exports: [FormDraftDirective, FormDraftBannerComponent],
459
- }]
448
+ class NgxFormDraftModule {
449
+ }
450
+ NgxFormDraftModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
451
+ NgxFormDraftModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, declarations: [FormDraftDirective, FormDraftBannerComponent], imports: [CommonModule], exports: [FormDraftDirective, FormDraftBannerComponent] });
452
+ NgxFormDraftModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, imports: [CommonModule] });
453
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, decorators: [{
454
+ type: NgModule,
455
+ args: [{
456
+ declarations: [FormDraftDirective, FormDraftBannerComponent],
457
+ imports: [CommonModule],
458
+ exports: [FormDraftDirective, FormDraftBannerComponent],
459
+ }]
460
460
  }] });
461
461
 
462
- /**
463
- * Generated bundle index. Do not edit.
462
+ /**
463
+ * Generated bundle index. Do not edit.
464
464
  */
465
465
 
466
466
  export { FormDraftBannerComponent, FormDraftDirective, FormDraftService, NgxFormDraftModule };