ngx-form-draft 1.0.1 → 1.0.3

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.
@@ -1,111 +0,0 @@
1
- import { __decorate, __metadata } from "tslib";
2
- import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
3
- import { trigger, transition, style, animate } from '@angular/animations';
4
- let FormDraftBannerComponent = class FormDraftBannerComponent {
5
- constructor() {
6
- this.visible = false;
7
- this.timeLabel = '';
8
- this.isRestored = false;
9
- this.restoredText = 'Draft restored';
10
- this.savedText = 'Draft saved';
11
- this.savedLabel = 'saved';
12
- this.discardText = 'Discard';
13
- this.discard = new EventEmitter();
14
- }
15
- };
16
- __decorate([
17
- Input(),
18
- __metadata("design:type", Object)
19
- ], FormDraftBannerComponent.prototype, "visible", void 0);
20
- __decorate([
21
- Input(),
22
- __metadata("design:type", Object)
23
- ], FormDraftBannerComponent.prototype, "timeLabel", void 0);
24
- __decorate([
25
- Input(),
26
- __metadata("design:type", Object)
27
- ], FormDraftBannerComponent.prototype, "isRestored", void 0);
28
- __decorate([
29
- Input(),
30
- __metadata("design:type", Object)
31
- ], FormDraftBannerComponent.prototype, "restoredText", void 0);
32
- __decorate([
33
- Input(),
34
- __metadata("design:type", Object)
35
- ], FormDraftBannerComponent.prototype, "savedText", void 0);
36
- __decorate([
37
- Input(),
38
- __metadata("design:type", Object)
39
- ], FormDraftBannerComponent.prototype, "savedLabel", void 0);
40
- __decorate([
41
- Input(),
42
- __metadata("design:type", Object)
43
- ], FormDraftBannerComponent.prototype, "discardText", void 0);
44
- __decorate([
45
- Output(),
46
- __metadata("design:type", Object)
47
- ], FormDraftBannerComponent.prototype, "discard", void 0);
48
- FormDraftBannerComponent = __decorate([
49
- Component({
50
- selector: 'ngx-form-draft-banner',
51
- template: `
52
- <div class="form-draft-banner" *ngIf="visible" [@slideDown]>
53
- <div class="form-draft-banner__icon">
54
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
55
- <path d="M12 8V12L14.5 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
56
- <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
57
- </svg>
58
- </div>
59
- <div class="form-draft-banner__content">
60
- <span class="form-draft-banner__text">
61
- <span *ngIf="isRestored">{{ restoredText }}</span>
62
- <span *ngIf="!isRestored">{{ savedText }}</span>
63
- <span class="form-draft-banner__time" *ngIf="timeLabel && isRestored">
64
- &middot; {{ savedLabel }} {{ timeLabel }}
65
- </span>
66
- </span>
67
- </div>
68
- <div class="form-draft-banner__actions">
69
- <button class="form-draft-banner__btn form-draft-banner__btn--discard" (click)="discard.emit()" type="button">
70
- ✕ {{ discardText }}
71
- </button>
72
- </div>
73
- </div>
74
- `,
75
- styles: [`
76
- :host { display: block; width: 100%; }
77
- .form-draft-banner {
78
- display: flex; align-items: center; gap: 10px; padding: 10px 14px; margin-bottom: 12px;
79
- border-radius: 8px; background: linear-gradient(135deg, #eef6ff 0%, #f0f4ff 100%);
80
- border: 1px solid #c5ddf8; box-shadow: 0 2px 8px rgba(18, 138, 214, 0.08);
81
- }
82
- .form-draft-banner__icon {
83
- display: flex; align-items: center; justify-content: center; width: 32px; height: 32px;
84
- border-radius: 6px; background: linear-gradient(135deg, #128ad6, #22b9ff); color: #fff; flex-shrink: 0;
85
- }
86
- .form-draft-banner__content { flex: 1; min-width: 0; }
87
- .form-draft-banner__text { font-size: 13px; font-weight: 600; color: #1a3a5c; }
88
- .form-draft-banner__time { font-weight: 400; color: #6b8aaa; font-size: 12px; }
89
- .form-draft-banner__actions { flex-shrink: 0; }
90
- .form-draft-banner__btn {
91
- border: none; padding: 5px 12px; border-radius: 6px; font-size: 12px;
92
- font-weight: 600; cursor: pointer; transition: all 0.2s ease;
93
- }
94
- .form-draft-banner__btn--discard { background: transparent; color: #8899a6; border: 1px solid #d0dce6; }
95
- .form-draft-banner__btn--discard:hover { background: #fef2f2; color: #e62e43; border-color: #e62e43; }
96
- `],
97
- animations: [
98
- trigger('slideDown', [
99
- transition(':enter', [
100
- style({ opacity: 0, transform: 'translateY(-8px)' }),
101
- animate('250ms cubic-bezier(0.4, 0, 0.2, 1)', style({ opacity: 1, transform: 'translateY(0)' }))
102
- ]),
103
- transition(':leave', [
104
- animate('200ms cubic-bezier(0.4, 0, 0.2, 1)', style({ opacity: 0, transform: 'translateY(-8px)' }))
105
- ])
106
- ])
107
- ],
108
- changeDetection: ChangeDetectionStrategy.OnPush,
109
- })
110
- ], FormDraftBannerComponent);
111
- export { FormDraftBannerComponent };
@@ -1,240 +0,0 @@
1
- import { __decorate, __metadata, __param } from "tslib";
2
- import { Directive, Input, Optional, ViewContainerRef, ChangeDetectorRef, ElementRef, Renderer2, } from '@angular/core';
3
- import { NgForm, FormGroupDirective, FormArray, FormGroup, FormControl } from '@angular/forms';
4
- import { Subject } from 'rxjs';
5
- import { debounceTime, takeUntil } from 'rxjs/operators';
6
- import { FormDraftService } from './form-draft.service';
7
- import { FormDraftBannerComponent } from './form-draft-banner.component';
8
- /**
9
- * Auto-saves and restores form drafts
10
- *
11
- * @example
12
- * <form [formGroup]="myForm" ngxFormDraft="myFormId">
13
- *
14
- * @example
15
- * <form [formGroup]="myForm" [ngxFormDraft]="'edit_' + entityId" [draftExcludeFields]="['password']">
16
- */
17
- let FormDraftDirective = class FormDraftDirective {
18
- constructor(formGroupDir, ngForm, draftService, viewContainerRef, cdRef, elRef, renderer) {
19
- this.formGroupDir = formGroupDir;
20
- this.ngForm = ngForm;
21
- this.draftService = draftService;
22
- this.viewContainerRef = viewContainerRef;
23
- this.cdRef = cdRef;
24
- this.elRef = elRef;
25
- this.renderer = renderer;
26
- this.draftDebounce = 800;
27
- this.draftExcludeFields = [];
28
- this.draftShowOnChange = false;
29
- this.draftRestoredText = 'Draft restored';
30
- this.draftSavedText = 'Draft saved';
31
- this.draftSavedLabel = 'saved';
32
- this.draftDiscardText = 'Discard';
33
- this.destroy$ = new Subject();
34
- this.bannerRef = null;
35
- this.formControl = null;
36
- this.initialValues = {};
37
- this.isRestoring = false;
38
- }
39
- ngOnInit() {
40
- var _a, _b;
41
- 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;
42
- if (!this.formControl || !this.formId)
43
- return;
44
- this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
45
- const draft = this.draftService.load(this.formId);
46
- if (draft) {
47
- this.restoreDraft(draft.values);
48
- this.showBanner(draft.savedAt, true);
49
- }
50
- this.formControl.valueChanges
51
- .pipe(debounceTime(this.draftDebounce), takeUntil(this.destroy$))
52
- .subscribe((values) => {
53
- if (this.isRestoring)
54
- return;
55
- this.saveDraft(values);
56
- });
57
- }
58
- ngOnDestroy() {
59
- this.destroy$.next();
60
- this.destroy$.complete();
61
- this.destroyBanner();
62
- }
63
- saveDraft(values) {
64
- const filtered = this.filterFields(values);
65
- if (this.isAllEmpty(filtered) || this.matchesInitialValues(filtered)) {
66
- return;
67
- }
68
- this.draftService.save(this.formId, filtered);
69
- if (this.draftShowOnChange && !this.bannerRef) {
70
- this.showBanner(Date.now(), false);
71
- }
72
- }
73
- filterFields(values) {
74
- if (!this.draftExcludeFields.length)
75
- return values;
76
- const result = Object.assign({}, values);
77
- this.draftExcludeFields.forEach(field => delete result[field]);
78
- return result;
79
- }
80
- isAllEmpty(values) {
81
- return Object.values(values).every(v => v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0));
82
- }
83
- matchesInitialValues(values) {
84
- return JSON.stringify(values) === JSON.stringify(this.initialValues);
85
- }
86
- restoreDraft(values) {
87
- var _a, _b;
88
- if (!this.formControl)
89
- return;
90
- this.isRestoring = true;
91
- const form = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form);
92
- if (form) {
93
- this.prepareFormArrays(form, values);
94
- form.patchValue(values);
95
- }
96
- setTimeout(() => this.isRestoring = false, 100);
97
- }
98
- prepareFormArrays(control, value) {
99
- if (!control || value == null)
100
- return;
101
- if (control instanceof FormGroup && value && typeof value === 'object' && !Array.isArray(value)) {
102
- Object.keys(value).forEach(key => {
103
- const childControl = control.get(key);
104
- if (childControl)
105
- this.prepareFormArrays(childControl, value[key]);
106
- });
107
- return;
108
- }
109
- if (control instanceof FormArray && Array.isArray(value)) {
110
- const formArray = control;
111
- while (formArray.length < value.length) {
112
- const template = formArray.at(0);
113
- if (template instanceof FormGroup) {
114
- const newGroup = new FormGroup({});
115
- Object.keys(template.controls).forEach(ctrlName => {
116
- const existing = template.get(ctrlName);
117
- if (existing instanceof FormArray) {
118
- newGroup.addControl(ctrlName, new FormArray([]));
119
- }
120
- else if (existing instanceof FormGroup) {
121
- newGroup.addControl(ctrlName, new FormGroup({}));
122
- }
123
- else {
124
- newGroup.addControl(ctrlName, new FormControl(null));
125
- }
126
- });
127
- formArray.push(newGroup);
128
- }
129
- else if (template) {
130
- formArray.push(new FormControl(null));
131
- }
132
- else {
133
- const firstValue = value[0];
134
- if (firstValue && typeof firstValue === 'object' && !Array.isArray(firstValue)) {
135
- const group = new FormGroup({});
136
- Object.keys(firstValue).forEach(key => group.addControl(key, new FormControl(null)));
137
- formArray.push(group);
138
- }
139
- else {
140
- formArray.push(new FormControl(null));
141
- }
142
- }
143
- }
144
- while (formArray.length > value.length) {
145
- formArray.removeAt(formArray.length - 1);
146
- }
147
- value.forEach((childValue, index) => {
148
- this.prepareFormArrays(formArray.at(index), childValue);
149
- });
150
- }
151
- }
152
- discardDraft() {
153
- var _a, _b;
154
- this.draftService.clear(this.formId);
155
- if (this.formControl) {
156
- this.isRestoring = true;
157
- const form = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form);
158
- if (form) {
159
- this.prepareFormArrays(form, this.initialValues);
160
- form.reset(this.initialValues);
161
- }
162
- setTimeout(() => {
163
- this.isRestoring = false;
164
- }, this.draftDebounce + 200);
165
- }
166
- this.destroyBanner();
167
- }
168
- showBanner(savedAt, isRestored = false) {
169
- this.bannerRef = this.viewContainerRef.createComponent(FormDraftBannerComponent);
170
- this.bannerRef.setInput('visible', true);
171
- this.bannerRef.setInput('isRestored', isRestored);
172
- this.bannerRef.setInput('timeLabel', isRestored ? this.draftService.formatTimestamp(savedAt) : '');
173
- this.bannerRef.setInput('restoredText', this.draftRestoredText);
174
- this.bannerRef.setInput('savedText', this.draftSavedText);
175
- this.bannerRef.setInput('savedLabel', this.draftSavedLabel);
176
- this.bannerRef.setInput('discardText', this.draftDiscardText);
177
- this.bannerRef.instance.discard.subscribe(() => this.discardDraft());
178
- const bannerEl = this.bannerRef.location.nativeElement;
179
- const formEl = this.elRef.nativeElement;
180
- this.renderer.insertBefore(formEl, bannerEl, formEl.firstChild);
181
- this.cdRef.detectChanges();
182
- }
183
- destroyBanner() {
184
- if (this.bannerRef) {
185
- this.bannerRef.destroy();
186
- this.bannerRef = null;
187
- }
188
- }
189
- clearDraft() {
190
- this.draftService.clear(this.formId);
191
- this.destroyBanner();
192
- }
193
- };
194
- __decorate([
195
- Input('ngxFormDraft'),
196
- __metadata("design:type", String)
197
- ], FormDraftDirective.prototype, "formId", void 0);
198
- __decorate([
199
- Input(),
200
- __metadata("design:type", Object)
201
- ], FormDraftDirective.prototype, "draftDebounce", void 0);
202
- __decorate([
203
- Input(),
204
- __metadata("design:type", Array)
205
- ], FormDraftDirective.prototype, "draftExcludeFields", void 0);
206
- __decorate([
207
- Input(),
208
- __metadata("design:type", Object)
209
- ], FormDraftDirective.prototype, "draftShowOnChange", void 0);
210
- __decorate([
211
- Input(),
212
- __metadata("design:type", Object)
213
- ], FormDraftDirective.prototype, "draftRestoredText", void 0);
214
- __decorate([
215
- Input(),
216
- __metadata("design:type", Object)
217
- ], FormDraftDirective.prototype, "draftSavedText", void 0);
218
- __decorate([
219
- Input(),
220
- __metadata("design:type", Object)
221
- ], FormDraftDirective.prototype, "draftSavedLabel", void 0);
222
- __decorate([
223
- Input(),
224
- __metadata("design:type", Object)
225
- ], FormDraftDirective.prototype, "draftDiscardText", void 0);
226
- FormDraftDirective = __decorate([
227
- Directive({
228
- selector: '[ngxFormDraft]',
229
- }),
230
- __param(0, Optional()),
231
- __param(1, Optional()),
232
- __metadata("design:paramtypes", [FormGroupDirective,
233
- NgForm,
234
- FormDraftService,
235
- ViewContainerRef,
236
- ChangeDetectorRef,
237
- ElementRef,
238
- Renderer2])
239
- ], FormDraftDirective);
240
- export { FormDraftDirective };
@@ -1,64 +0,0 @@
1
- import { __decorate } from "tslib";
2
- import { Injectable } from '@angular/core';
3
- let FormDraftService = class FormDraftService {
4
- constructor() {
5
- this.STORAGE_PREFIX = 'form_draft_';
6
- this.MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
7
- }
8
- buildKey(formId) {
9
- return `${this.STORAGE_PREFIX}${formId}`;
10
- }
11
- save(formId, values) {
12
- try {
13
- const draft = { values, savedAt: Date.now(), formId };
14
- localStorage.setItem(this.buildKey(formId), JSON.stringify(draft));
15
- }
16
- catch (e) {
17
- console.warn('[FormDraft] Could not save draft:', e);
18
- }
19
- }
20
- load(formId) {
21
- try {
22
- const raw = localStorage.getItem(this.buildKey(formId));
23
- if (!raw)
24
- return null;
25
- const draft = JSON.parse(raw);
26
- if (Date.now() - draft.savedAt > this.MAX_AGE_MS) {
27
- this.clear(formId);
28
- return null;
29
- }
30
- return draft;
31
- }
32
- catch (e) {
33
- console.warn('[FormDraft] Could not load draft:', e);
34
- return null;
35
- }
36
- }
37
- clear(formId) {
38
- try {
39
- localStorage.removeItem(this.buildKey(formId));
40
- }
41
- catch (e) {
42
- console.warn('[FormDraft] Could not clear draft:', e);
43
- }
44
- }
45
- formatTimestamp(timestamp) {
46
- const seconds = Math.floor((Date.now() - timestamp) / 1000);
47
- if (seconds < 60)
48
- return 'just now';
49
- const minutes = Math.floor(seconds / 60);
50
- if (minutes < 60)
51
- return `${minutes}m ago`;
52
- const hours = Math.floor(minutes / 60);
53
- if (hours < 24)
54
- return `${hours}h ago`;
55
- const days = Math.floor(hours / 24);
56
- return `${days}d ago`;
57
- }
58
- };
59
- FormDraftService = __decorate([
60
- Injectable({
61
- providedIn: 'root'
62
- })
63
- ], FormDraftService);
64
- export { FormDraftService };
package/dist/index.js DELETED
@@ -1,4 +0,0 @@
1
- export * from './form-draft.directive';
2
- export * from './form-draft-banner.component';
3
- export * from './form-draft.service';
4
- export * from './ngx-form-draft.module';
@@ -1,15 +0,0 @@
1
- import { __decorate } from "tslib";
2
- import { NgModule } from '@angular/core';
3
- import { CommonModule } from '@angular/common';
4
- import { FormDraftDirective } from './form-draft.directive';
5
- import { FormDraftBannerComponent } from './form-draft-banner.component';
6
- let NgxFormDraftModule = class NgxFormDraftModule {
7
- };
8
- NgxFormDraftModule = __decorate([
9
- NgModule({
10
- declarations: [FormDraftDirective, FormDraftBannerComponent],
11
- imports: [CommonModule],
12
- exports: [FormDraftDirective, FormDraftBannerComponent],
13
- })
14
- ], NgxFormDraftModule);
15
- export { NgxFormDraftModule };