ngx-form-draft 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/form-draft-banner.component.d.ts +11 -11
- package/dist/form-draft-banner.component.js +67 -67
- package/dist/form-draft.directive.d.ts +47 -47
- package/dist/form-draft.directive.js +240 -240
- package/dist/form-draft.service.d.ts +14 -14
- package/dist/form-draft.service.js +64 -64
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/ngx-form-draft.module.d.ts +2 -2
- package/dist/ngx-form-draft.module.js +15 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,18 @@ Zero-dependency Angular form draft auto-save and restore. Works with any Angular
|
|
|
18
18
|
npm install ngx-form-draft
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
### Angular 12 Users
|
|
22
|
+
|
|
23
|
+
If you're using Angular 12, enable Ivy in your `tsconfig.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"angularCompilerOptions": {
|
|
28
|
+
"enableIvy": true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
21
33
|
## Usage
|
|
22
34
|
|
|
23
35
|
### 1. Import the module
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { EventEmitter } from '@angular/core';
|
|
2
|
-
export declare class FormDraftBannerComponent {
|
|
3
|
-
visible: boolean;
|
|
4
|
-
timeLabel: string;
|
|
5
|
-
isRestored: boolean;
|
|
6
|
-
restoredText: string;
|
|
7
|
-
savedText: string;
|
|
8
|
-
savedLabel: string;
|
|
9
|
-
discardText: string;
|
|
10
|
-
discard: EventEmitter<void>;
|
|
11
|
-
}
|
|
1
|
+
import { EventEmitter } from '@angular/core';
|
|
2
|
+
export declare class FormDraftBannerComponent {
|
|
3
|
+
visible: boolean;
|
|
4
|
+
timeLabel: string;
|
|
5
|
+
isRestored: boolean;
|
|
6
|
+
restoredText: string;
|
|
7
|
+
savedText: string;
|
|
8
|
+
savedLabel: string;
|
|
9
|
+
discardText: string;
|
|
10
|
+
discard: EventEmitter<void>;
|
|
11
|
+
}
|
|
@@ -1,53 +1,53 @@
|
|
|
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',
|
|
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
51
|
template: `
|
|
52
52
|
<div class="form-draft-banner" *ngIf="visible" [@slideDown]>
|
|
53
53
|
<div class="form-draft-banner__icon">
|
|
@@ -71,7 +71,7 @@ FormDraftBannerComponent = __decorate([
|
|
|
71
71
|
</button>
|
|
72
72
|
</div>
|
|
73
73
|
</div>
|
|
74
|
-
`,
|
|
74
|
+
`,
|
|
75
75
|
styles: [`
|
|
76
76
|
:host { display: block; width: 100%; }
|
|
77
77
|
.form-draft-banner {
|
|
@@ -93,19 +93,19 @@ FormDraftBannerComponent = __decorate([
|
|
|
93
93
|
}
|
|
94
94
|
.form-draft-banner__btn--discard { background: transparent; color: #8899a6; border: 1px solid #d0dce6; }
|
|
95
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 };
|
|
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,47 +1,47 @@
|
|
|
1
|
-
import { OnInit, OnDestroy, ViewContainerRef, ChangeDetectorRef, ElementRef, Renderer2 } from '@angular/core';
|
|
2
|
-
import { NgForm, FormGroupDirective } from '@angular/forms';
|
|
3
|
-
import { FormDraftService } from './form-draft.service';
|
|
4
|
-
/**
|
|
5
|
-
* Auto-saves and restores form drafts
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* <form [formGroup]="myForm" ngxFormDraft="myFormId">
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* <form [formGroup]="myForm" [ngxFormDraft]="'edit_' + entityId" [draftExcludeFields]="['password']">
|
|
12
|
-
*/
|
|
13
|
-
export declare class FormDraftDirective implements OnInit, OnDestroy {
|
|
14
|
-
private formGroupDir;
|
|
15
|
-
private ngForm;
|
|
16
|
-
private draftService;
|
|
17
|
-
private viewContainerRef;
|
|
18
|
-
private cdRef;
|
|
19
|
-
private elRef;
|
|
20
|
-
private renderer;
|
|
21
|
-
formId: string;
|
|
22
|
-
draftDebounce: number;
|
|
23
|
-
draftExcludeFields: string[];
|
|
24
|
-
draftShowOnChange: boolean;
|
|
25
|
-
draftRestoredText: string;
|
|
26
|
-
draftSavedText: string;
|
|
27
|
-
draftSavedLabel: string;
|
|
28
|
-
draftDiscardText: string;
|
|
29
|
-
private destroy$;
|
|
30
|
-
private bannerRef;
|
|
31
|
-
private formControl;
|
|
32
|
-
private initialValues;
|
|
33
|
-
private isRestoring;
|
|
34
|
-
constructor(formGroupDir: FormGroupDirective, ngForm: NgForm, draftService: FormDraftService, viewContainerRef: ViewContainerRef, cdRef: ChangeDetectorRef, elRef: ElementRef, renderer: Renderer2);
|
|
35
|
-
ngOnInit(): void;
|
|
36
|
-
ngOnDestroy(): void;
|
|
37
|
-
private saveDraft;
|
|
38
|
-
private filterFields;
|
|
39
|
-
private isAllEmpty;
|
|
40
|
-
private matchesInitialValues;
|
|
41
|
-
private restoreDraft;
|
|
42
|
-
private prepareFormArrays;
|
|
43
|
-
private discardDraft;
|
|
44
|
-
private showBanner;
|
|
45
|
-
private destroyBanner;
|
|
46
|
-
clearDraft(): void;
|
|
47
|
-
}
|
|
1
|
+
import { OnInit, OnDestroy, ViewContainerRef, ChangeDetectorRef, ElementRef, Renderer2 } from '@angular/core';
|
|
2
|
+
import { NgForm, FormGroupDirective } from '@angular/forms';
|
|
3
|
+
import { FormDraftService } from './form-draft.service';
|
|
4
|
+
/**
|
|
5
|
+
* Auto-saves and restores form drafts
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <form [formGroup]="myForm" ngxFormDraft="myFormId">
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <form [formGroup]="myForm" [ngxFormDraft]="'edit_' + entityId" [draftExcludeFields]="['password']">
|
|
12
|
+
*/
|
|
13
|
+
export declare class FormDraftDirective implements OnInit, OnDestroy {
|
|
14
|
+
private formGroupDir;
|
|
15
|
+
private ngForm;
|
|
16
|
+
private draftService;
|
|
17
|
+
private viewContainerRef;
|
|
18
|
+
private cdRef;
|
|
19
|
+
private elRef;
|
|
20
|
+
private renderer;
|
|
21
|
+
formId: string;
|
|
22
|
+
draftDebounce: number;
|
|
23
|
+
draftExcludeFields: string[];
|
|
24
|
+
draftShowOnChange: boolean;
|
|
25
|
+
draftRestoredText: string;
|
|
26
|
+
draftSavedText: string;
|
|
27
|
+
draftSavedLabel: string;
|
|
28
|
+
draftDiscardText: string;
|
|
29
|
+
private destroy$;
|
|
30
|
+
private bannerRef;
|
|
31
|
+
private formControl;
|
|
32
|
+
private initialValues;
|
|
33
|
+
private isRestoring;
|
|
34
|
+
constructor(formGroupDir: FormGroupDirective, ngForm: NgForm, draftService: FormDraftService, viewContainerRef: ViewContainerRef, cdRef: ChangeDetectorRef, elRef: ElementRef, renderer: Renderer2);
|
|
35
|
+
ngOnInit(): void;
|
|
36
|
+
ngOnDestroy(): void;
|
|
37
|
+
private saveDraft;
|
|
38
|
+
private filterFields;
|
|
39
|
+
private isAllEmpty;
|
|
40
|
+
private matchesInitialValues;
|
|
41
|
+
private restoreDraft;
|
|
42
|
+
private prepareFormArrays;
|
|
43
|
+
private discardDraft;
|
|
44
|
+
private showBanner;
|
|
45
|
+
private destroyBanner;
|
|
46
|
+
clearDraft(): void;
|
|
47
|
+
}
|
|
@@ -1,240 +1,240 @@
|
|
|
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
|
+
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,14 +1,14 @@
|
|
|
1
|
-
export interface FormDraftData {
|
|
2
|
-
values: Record<string, any>;
|
|
3
|
-
savedAt: number;
|
|
4
|
-
formId: string;
|
|
5
|
-
}
|
|
6
|
-
export declare class FormDraftService {
|
|
7
|
-
private readonly STORAGE_PREFIX;
|
|
8
|
-
private readonly MAX_AGE_MS;
|
|
9
|
-
private buildKey;
|
|
10
|
-
save(formId: string, values: Record<string, any>): void;
|
|
11
|
-
load(formId: string): FormDraftData | null;
|
|
12
|
-
clear(formId: string): void;
|
|
13
|
-
formatTimestamp(timestamp: number): string;
|
|
14
|
-
}
|
|
1
|
+
export interface FormDraftData {
|
|
2
|
+
values: Record<string, any>;
|
|
3
|
+
savedAt: number;
|
|
4
|
+
formId: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class FormDraftService {
|
|
7
|
+
private readonly STORAGE_PREFIX;
|
|
8
|
+
private readonly MAX_AGE_MS;
|
|
9
|
+
private buildKey;
|
|
10
|
+
save(formId: string, values: Record<string, any>): void;
|
|
11
|
+
load(formId: string): FormDraftData | null;
|
|
12
|
+
clear(formId: string): void;
|
|
13
|
+
formatTimestamp(timestamp: number): string;
|
|
14
|
+
}
|
|
@@ -1,64 +1,64 @@
|
|
|
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 };
|
|
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.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
|
+
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';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
|
+
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,2 +1,2 @@
|
|
|
1
|
-
export declare class NgxFormDraftModule {
|
|
2
|
-
}
|
|
1
|
+
export declare class NgxFormDraftModule {
|
|
2
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
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 };
|
|
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 };
|