@servicetitan/form-state 22.4.0
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/CHANGELOG.md +12 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.d.ts +12 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.d.ts.map +1 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.js +89 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.js.map +1 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.stories.d.ts +11 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.stories.d.ts.map +1 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.stories.js +55 -0
- package/dist/async-lazy-dropdown-state/async-lazy-dropdown-state.stories.js.map +1 -0
- package/dist/async-lazy-dropdown-state/index.d.ts +3 -0
- package/dist/async-lazy-dropdown-state/index.d.ts.map +1 -0
- package/dist/async-lazy-dropdown-state/index.js +3 -0
- package/dist/async-lazy-dropdown-state/index.js.map +1 -0
- package/dist/async-lazy-dropdown-state/use-async-lazy-dropdown-state.d.ts +5 -0
- package/dist/async-lazy-dropdown-state/use-async-lazy-dropdown-state.d.ts.map +1 -0
- package/dist/async-lazy-dropdown-state/use-async-lazy-dropdown-state.js +26 -0
- package/dist/async-lazy-dropdown-state/use-async-lazy-dropdown-state.js.map +1 -0
- package/dist/date-range.d.ts +5 -0
- package/dist/date-range.d.ts.map +1 -0
- package/dist/date-range.js +2 -0
- package/dist/date-range.js.map +1 -0
- package/dist/demo/dropdown-state.d.ts +3 -0
- package/dist/demo/dropdown-state.d.ts.map +1 -0
- package/dist/demo/dropdown-state.js +133 -0
- package/dist/demo/dropdown-state.js.map +1 -0
- package/dist/demo/index.d.ts +2 -0
- package/dist/demo/index.d.ts.map +1 -0
- package/dist/demo/index.js +2 -0
- package/dist/demo/index.js.map +1 -0
- package/dist/dropdown-state.d.ts +42 -0
- package/dist/dropdown-state.d.ts.map +1 -0
- package/dist/dropdown-state.js +314 -0
- package/dist/dropdown-state.js.map +1 -0
- package/dist/form-helpers.d.ts +70 -0
- package/dist/form-helpers.d.ts.map +1 -0
- package/dist/form-helpers.js +232 -0
- package/dist/form-helpers.js.map +1 -0
- package/dist/form-validators.d.ts +30 -0
- package/dist/form-validators.d.ts.map +1 -0
- package/dist/form-validators.js +56 -0
- package/dist/form-validators.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/persistent-form-state/domain-storage.d.ts +14 -0
- package/dist/persistent-form-state/domain-storage.d.ts.map +1 -0
- package/dist/persistent-form-state/domain-storage.js +42 -0
- package/dist/persistent-form-state/domain-storage.js.map +1 -0
- package/dist/persistent-form-state/in-memory-storage.d.ts +13 -0
- package/dist/persistent-form-state/in-memory-storage.d.ts.map +1 -0
- package/dist/persistent-form-state/in-memory-storage.js +30 -0
- package/dist/persistent-form-state/in-memory-storage.js.map +1 -0
- package/dist/persistent-form-state/index.d.ts +2 -0
- package/dist/persistent-form-state/index.d.ts.map +1 -0
- package/dist/persistent-form-state/index.js +2 -0
- package/dist/persistent-form-state/index.js.map +1 -0
- package/dist/persistent-form-state/persistent-form-state.d.ts +18 -0
- package/dist/persistent-form-state/persistent-form-state.d.ts.map +1 -0
- package/dist/persistent-form-state/persistent-form-state.js +93 -0
- package/dist/persistent-form-state/persistent-form-state.js.map +1 -0
- package/package.json +45 -0
- package/src/__tests__/__snapshots__/form-helpers.test.ts.snap +37 -0
- package/src/__tests__/form-helpers.test.ts +229 -0
- package/src/__tests__/form-validators.test.ts +55 -0
- package/src/async-lazy-dropdown-state/async-lazy-dropdown-state.stories.tsx +66 -0
- package/src/async-lazy-dropdown-state/async-lazy-dropdown-state.ts +77 -0
- package/src/async-lazy-dropdown-state/index.ts +2 -0
- package/src/async-lazy-dropdown-state/use-async-lazy-dropdown-state.ts +38 -0
- package/src/date-range.ts +4 -0
- package/src/demo/dropdown-state.tsx +233 -0
- package/src/demo/index.ts +1 -0
- package/src/dropdown-state.ts +205 -0
- package/src/form-helpers.ts +259 -0
- package/src/form-validators.ts +106 -0
- package/src/index.ts +5 -0
- package/src/persistent-form-state/__tests__/domain-storage.test.ts +81 -0
- package/src/persistent-form-state/domain-storage.ts +43 -0
- package/src/persistent-form-state/in-memory-storage.ts +32 -0
- package/src/persistent-form-state/index.ts +1 -0
- package/src/persistent-form-state/persistent-form-state.ts +68 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { DatetimeFieldState, FormValues } from './form-helpers';
|
|
2
|
+
import { isObservableArray } from 'mobx';
|
|
3
|
+
import { DateRange } from './date-range';
|
|
4
|
+
|
|
5
|
+
interface DateRangeFieldStates {
|
|
6
|
+
startDate: DatetimeFieldState;
|
|
7
|
+
endDate: DatetimeFieldState;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const isDefined = (value: FormValues | undefined) => {
|
|
11
|
+
if (value === undefined) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (Array.isArray(value) || isObservableArray(value)) {
|
|
16
|
+
return !!value.length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return typeof value === 'string' ? !!value.trim() : !!value;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const FormValidators = {
|
|
23
|
+
required: (value?: FormValues) =>
|
|
24
|
+
FormValidators.requiredWithCustomMessage('Value is required')(value),
|
|
25
|
+
|
|
26
|
+
requiredWithCustomMessage: (errorMessage: string) => (value?: FormValues) =>
|
|
27
|
+
!isDefined(value) && errorMessage,
|
|
28
|
+
|
|
29
|
+
hasLowerCase: (str: string) => /[a-z]/.test(str),
|
|
30
|
+
|
|
31
|
+
hasUpperCase: (str: string) => /[A-Z]/.test(str),
|
|
32
|
+
|
|
33
|
+
hasNumber: (str: string) => /\d/.test(str),
|
|
34
|
+
|
|
35
|
+
passwordIsValidFormat: (password: string) =>
|
|
36
|
+
password.length > 7 &&
|
|
37
|
+
FormValidators.hasLowerCase(password) &&
|
|
38
|
+
FormValidators.hasUpperCase(password) &&
|
|
39
|
+
FormValidators.hasNumber(password),
|
|
40
|
+
|
|
41
|
+
emailFormatIsValid: (email: string) => {
|
|
42
|
+
/* tslint:disable: ter-max-len */
|
|
43
|
+
const regex =
|
|
44
|
+
/^[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
|
|
45
|
+
return email.length !== 0 && regex.test(email);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
website: (errorMessage = 'Enter valid website') => {
|
|
49
|
+
return (value: string) =>
|
|
50
|
+
!/^(https?:\/\/)?(www\.)?([a-zA-Z0-9]+(-?[a-zA-Z0-9])*\.)+[\w]{2,}(\/\S*)?$/i.test(
|
|
51
|
+
value
|
|
52
|
+
) && errorMessage;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
minDate: new Date(1900, 0, 1),
|
|
56
|
+
|
|
57
|
+
maxDate: new Date(2099, 11, 31),
|
|
58
|
+
|
|
59
|
+
isDateValid: (date: Date | null) =>
|
|
60
|
+
(!date || date > FormValidators.maxDate || date < FormValidators.minDate) &&
|
|
61
|
+
'Please provide a valid date',
|
|
62
|
+
|
|
63
|
+
isDateRangeValid: (dateRange: DateRangeFieldStates) =>
|
|
64
|
+
dateRange.startDate.$ &&
|
|
65
|
+
dateRange.endDate.$ &&
|
|
66
|
+
dateRange.startDate.$ > dateRange.endDate.$ &&
|
|
67
|
+
'Start Date should not be after End Date',
|
|
68
|
+
|
|
69
|
+
dateRangeRequired:
|
|
70
|
+
(errorMessage = 'Date Range is required') =>
|
|
71
|
+
(dateRange: DateRange | undefined) =>
|
|
72
|
+
(!dateRange || !dateRange.from || !dateRange.to) && errorMessage,
|
|
73
|
+
|
|
74
|
+
dateRangeValid:
|
|
75
|
+
(errorMessage = 'Start cannot be after End') =>
|
|
76
|
+
(dateRange: DateRange | undefined) =>
|
|
77
|
+
!!dateRange &&
|
|
78
|
+
!!dateRange.from &&
|
|
79
|
+
!!dateRange.to &&
|
|
80
|
+
dateRange.from > dateRange.to &&
|
|
81
|
+
errorMessage,
|
|
82
|
+
|
|
83
|
+
isDateRangeLessThanMaxLength: (maxDays: number) => (val: DateRange | undefined) => {
|
|
84
|
+
const dayInMillseconds = 1000 * 60 * 60 * 24;
|
|
85
|
+
return (
|
|
86
|
+
val?.from &&
|
|
87
|
+
val.to &&
|
|
88
|
+
(val.to.getTime() - val.from.getTime()) / dayInMillseconds >= maxDays &&
|
|
89
|
+
`Only ${maxDays} days can be displayed at time`
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
isAlphaNumeric: (str: string) => /^(\w+,?)*$/.test(str),
|
|
94
|
+
|
|
95
|
+
isMatchingRegex: (regexp: RegExp, entity: string) => (str: string) =>
|
|
96
|
+
!regexp.test(str) && `Invalid format for ${entity}`,
|
|
97
|
+
|
|
98
|
+
minLength: (minLength: number) => (value: string | any[] | undefined) =>
|
|
99
|
+
value && value.length < minLength ? `Value's min length is ${minLength}` : false,
|
|
100
|
+
|
|
101
|
+
maxLength: (maxLength: number) => (value: string | any[] | undefined) =>
|
|
102
|
+
value && value.length > maxLength ? `Value's max length is ${maxLength}` : false,
|
|
103
|
+
|
|
104
|
+
exactLength: (exactLength: number) => (value: string | any[] | undefined) =>
|
|
105
|
+
value && value.length !== exactLength ? `Value's length must be ${exactLength}` : false,
|
|
106
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { DomainStorage } from '../domain-storage';
|
|
2
|
+
|
|
3
|
+
const newData = {
|
|
4
|
+
string: 'value',
|
|
5
|
+
number: 1,
|
|
6
|
+
simpleObject: {
|
|
7
|
+
name: 'name value',
|
|
8
|
+
deepObject: {
|
|
9
|
+
name2: 'name2 value',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('domain-storage', () => {
|
|
15
|
+
test('getItem returns null if the key does not exist', function () {
|
|
16
|
+
const storage = new DomainStorage({ cacheKey: 'test', version: 1 });
|
|
17
|
+
|
|
18
|
+
const data = storage.getItem();
|
|
19
|
+
|
|
20
|
+
expect(data).toBe(null);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('getItem returns value from storage', function () {
|
|
24
|
+
const existingValue = JSON.stringify({ value: newData, version: 1 });
|
|
25
|
+
localStorage.setItem('test', existingValue);
|
|
26
|
+
const storage = new DomainStorage<any>({ cacheKey: 'test', version: 1 });
|
|
27
|
+
|
|
28
|
+
const data = storage.getItem();
|
|
29
|
+
|
|
30
|
+
expect(data).toEqual(newData);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('getItem returns null when the version does not match', function () {
|
|
34
|
+
const storage = new DomainStorage<any>({ cacheKey: 'test', version: 1 });
|
|
35
|
+
|
|
36
|
+
storage.setItem('', newData);
|
|
37
|
+
const data = storage.getItem();
|
|
38
|
+
expect(data).toEqual(newData);
|
|
39
|
+
|
|
40
|
+
const newStorage = new DomainStorage({ cacheKey: 'test', version: 2 });
|
|
41
|
+
const newStorageData = newStorage.getItem();
|
|
42
|
+
expect(newStorageData).toEqual(null);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('getItem returns null when there is invalid formStateCache in storage', function () {
|
|
46
|
+
localStorage.setItem('test', '{}');
|
|
47
|
+
const storage = new DomainStorage<any>({ cacheKey: 'test', version: 1 });
|
|
48
|
+
|
|
49
|
+
const data = storage.getItem();
|
|
50
|
+
|
|
51
|
+
expect(data).toEqual(null);
|
|
52
|
+
expect(localStorage.removeItem).toHaveBeenCalledWith('test');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('getItem returns null when there is invalid json in storage', function () {
|
|
56
|
+
localStorage.setItem('test', '{{}');
|
|
57
|
+
const storage = new DomainStorage<any>({ cacheKey: 'test', version: 1 });
|
|
58
|
+
|
|
59
|
+
const data = storage.getItem();
|
|
60
|
+
|
|
61
|
+
expect(data).toEqual(null);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('setItem adds value to storage', function () {
|
|
65
|
+
const storage = new DomainStorage<any>({ cacheKey: 'test', version: 1 });
|
|
66
|
+
|
|
67
|
+
storage.setItem('', newData);
|
|
68
|
+
|
|
69
|
+
const expectedValue = JSON.stringify({ value: newData, version: 1 });
|
|
70
|
+
expect(localStorage.getItem('test')).toEqual(expectedValue);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('removeItem removes value from storage', function () {
|
|
74
|
+
localStorage.setItem('test', '{}');
|
|
75
|
+
const storage = new DomainStorage<any>({ cacheKey: 'test', version: 1 });
|
|
76
|
+
|
|
77
|
+
storage.removeItem();
|
|
78
|
+
|
|
79
|
+
expect(localStorage.length).toEqual(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { FormStateShape } from '../form-helpers';
|
|
2
|
+
import { ValidatableMapOrArray } from 'formstate';
|
|
3
|
+
|
|
4
|
+
interface FormStateCache<T> {
|
|
5
|
+
version: number;
|
|
6
|
+
value: FormStateShape<T>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class DomainStorage<T extends ValidatableMapOrArray> {
|
|
10
|
+
private readonly version: number;
|
|
11
|
+
private readonly cacheKey: string;
|
|
12
|
+
|
|
13
|
+
constructor({ cacheKey, version }: { cacheKey: string; version: number }) {
|
|
14
|
+
this.version = version;
|
|
15
|
+
this.cacheKey = cacheKey;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getItem(): FormStateShape<T> | null {
|
|
19
|
+
try {
|
|
20
|
+
const cachedData = localStorage.getItem(this.cacheKey);
|
|
21
|
+
if (cachedData) {
|
|
22
|
+
const formData: FormStateCache<T> = JSON.parse(cachedData);
|
|
23
|
+
if (formData.version === this.version) {
|
|
24
|
+
return formData.value;
|
|
25
|
+
}
|
|
26
|
+
this.removeItem();
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
this.removeItem();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
removeItem(): void {
|
|
36
|
+
localStorage.removeItem(this.cacheKey);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setItem(_: string, value: FormStateShape<T>): void {
|
|
40
|
+
const formData = { value, version: this.version };
|
|
41
|
+
localStorage.setItem(this.cacheKey, JSON.stringify(formData));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FormStateShape } from '../form-helpers';
|
|
2
|
+
import { ValidatableMapOrArray } from 'formstate';
|
|
3
|
+
|
|
4
|
+
export class InMemoryStorage<T extends ValidatableMapOrArray> {
|
|
5
|
+
private storage = new Map<string, FormStateShape<T>>();
|
|
6
|
+
|
|
7
|
+
get length() {
|
|
8
|
+
return this.storage.size;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
clear(): void {
|
|
12
|
+
this.storage.clear();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getItem(key: string): FormStateShape<T> | null {
|
|
16
|
+
return this.storage.get(key) !== undefined ? this.storage.get(key)! : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
key(index: number): string | null {
|
|
20
|
+
return this.length < index ? null : Array.from(this.storage.keys())[index];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
removeItem(key: string): void {
|
|
24
|
+
this.storage.delete(key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
setItem(key: string, value: FormStateShape<T>): void {
|
|
28
|
+
this.storage.set(key, value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const MemoryStorage = new InMemoryStorage<any>();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './persistent-form-state';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { autorun } from 'mobx';
|
|
2
|
+
import { FormState, ValidatableMapOrArray } from 'formstate';
|
|
3
|
+
import { InMemoryStorage, MemoryStorage } from './in-memory-storage';
|
|
4
|
+
import {
|
|
5
|
+
RecursivePartial,
|
|
6
|
+
FormStateShape,
|
|
7
|
+
setFormStateValues,
|
|
8
|
+
formStateToJS,
|
|
9
|
+
} from '../form-helpers';
|
|
10
|
+
import { DomainStorage } from './domain-storage';
|
|
11
|
+
|
|
12
|
+
export enum PersistenceMode {
|
|
13
|
+
Session,
|
|
14
|
+
Domain,
|
|
15
|
+
InMemory,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class PersistentFormState<T extends ValidatableMapOrArray> extends FormState<T> {
|
|
19
|
+
private storageSystem!: InMemoryStorage<T> | DomainStorage<T>;
|
|
20
|
+
private resetFormSuper = this.reset;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
$: T,
|
|
24
|
+
private cacheKey: string,
|
|
25
|
+
private persistenceMode: PersistenceMode,
|
|
26
|
+
autoSave?: boolean,
|
|
27
|
+
version?: number
|
|
28
|
+
) {
|
|
29
|
+
super($);
|
|
30
|
+
|
|
31
|
+
if (PersistenceMode.Domain === this.persistenceMode) {
|
|
32
|
+
if (!version) {
|
|
33
|
+
throw 'Set a data structure version.';
|
|
34
|
+
}
|
|
35
|
+
this.storageSystem = new DomainStorage<T>({ cacheKey, version });
|
|
36
|
+
} else if (PersistenceMode.Session === this.persistenceMode) {
|
|
37
|
+
throw 'Use of Session storage is currently unsafe';
|
|
38
|
+
} else {
|
|
39
|
+
this.storageSystem = MemoryStorage;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.getCached();
|
|
43
|
+
if (autoSave) {
|
|
44
|
+
this.trackChange();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
save = () => {
|
|
49
|
+
this.storageSystem.setItem(this.cacheKey, formStateToJS(this));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
resetForm = () => {
|
|
53
|
+
this.resetFormSuper();
|
|
54
|
+
this.storageSystem.removeItem(this.cacheKey);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
private trackChange = () => {
|
|
58
|
+
autorun(() => this.save(), { delay: 500 });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
private getCached = () => {
|
|
62
|
+
const data = this.storageSystem.getItem(this.cacheKey);
|
|
63
|
+
|
|
64
|
+
if (data) {
|
|
65
|
+
setFormStateValues(this, data as unknown as RecursivePartial<FormStateShape<T>>);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|