@idem.agency/form-builder 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/CHANGELOG.md +0 -16
- package/eslint.config.js +0 -23
- package/public/index.html +0 -13
- package/public/main.tsx +0 -97
- package/src/app/index.test.tsx +0 -137
- package/src/app/index.tsx +0 -34
- package/src/app/test.css +0 -1
- package/src/entity/dynamicBuilder/element.tsx +0 -71
- package/src/entity/dynamicBuilder/index.tsx +0 -21
- package/src/entity/dynamicBuilder/model/createBuilderContext.tsx +0 -52
- package/src/entity/dynamicBuilder/model/index.ts +0 -7
- package/src/entity/inputs/index.ts +0 -2
- package/src/entity/inputs/ui/group/index.test.tsx +0 -46
- package/src/entity/inputs/ui/group/index.tsx +0 -26
- package/src/entity/inputs/ui/input/index.test.tsx +0 -73
- package/src/entity/inputs/ui/input/index.tsx +0 -31
- package/src/plugins/validation/index.ts +0 -39
- package/src/plugins/validation/rules/confirm.test.ts +0 -23
- package/src/plugins/validation/rules/confirm.ts +0 -11
- package/src/plugins/validation/rules/email.test.ts +0 -20
- package/src/plugins/validation/rules/email.ts +0 -10
- package/src/plugins/validation/rules/index.ts +0 -5
- package/src/plugins/validation/rules/required.test.ts +0 -24
- package/src/plugins/validation/rules/required.ts +0 -9
- package/src/plugins/validation/types/index.ts +0 -7
- package/src/plugins/validation/validation.test.ts +0 -69
- package/src/plugins/validation/validation.ts +0 -57
- package/src/plugins/visibility/core.ts +0 -56
- package/src/plugins/visibility/index.ts +0 -25
- package/src/plugins/visibility/types.ts +0 -20
- package/src/shared/hook/useUpdateEffect.tsx +0 -23
- package/src/shared/model/index.ts +0 -12
- package/src/shared/model/plugins/EventBus.ts +0 -15
- package/src/shared/model/plugins/FieldRegistry.ts +0 -15
- package/src/shared/model/plugins/HookRegistry.ts +0 -21
- package/src/shared/model/plugins/MiddlewareRegistry.ts +0 -17
- package/src/shared/model/plugins/PipelineRegistry.ts +0 -73
- package/src/shared/model/plugins/PluginManager.ts +0 -84
- package/src/shared/model/plugins/context.tsx +0 -10
- package/src/shared/model/plugins/hooks.ts +0 -22
- package/src/shared/model/plugins/index.ts +0 -23
- package/src/shared/model/plugins/types.ts +0 -99
- package/src/shared/model/store/createStoreContext.tsx +0 -70
- package/src/shared/model/store/index.test.tsx +0 -113
- package/src/shared/model/store/index.tsx +0 -96
- package/src/shared/model/store/store.test.ts +0 -64
- package/src/shared/model/store/store.ts +0 -38
- package/src/shared/test-utils.tsx +0 -33
- package/src/shared/types/common.ts +0 -45
- package/src/shared/utils.test.ts +0 -55
- package/src/shared/utils.ts +0 -36
- package/src/widgets/form/form.tsx +0 -59
- package/tsconfig.json +0 -24
- package/tsdown.config.ts +0 -10
- package/vite.config.ts +0 -12
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { FormElementProps, FormFieldConfig, FormFieldBase, RC } from '../../../../shared/types/common';
|
|
2
|
-
import { useId } from "react";
|
|
3
|
-
|
|
4
|
-
export type TextFieldConfig = FormFieldBase & { type: 'text' | 'email' | 'password'; placeholder?: string; };
|
|
5
|
-
|
|
6
|
-
function isTextFieldConfig(field: FormFieldConfig): field is TextFieldConfig {
|
|
7
|
-
return field.type === 'text' || field.type === 'email' || field.type === 'password';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const TextField: RC<FormElementProps> = ({ field, value, errors, onChange }) => {
|
|
11
|
-
if (!isTextFieldConfig(field)) {
|
|
12
|
-
console.warn(`TextField received an invalid field config for type: ${field.type}`);
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
const id = useId();
|
|
16
|
-
return (
|
|
17
|
-
<div style={{ marginBottom: '15px' }}>
|
|
18
|
-
<label htmlFor={id}>{field.label}:</label>
|
|
19
|
-
<input
|
|
20
|
-
type={field.type}
|
|
21
|
-
id={id}
|
|
22
|
-
name={field.name}
|
|
23
|
-
placeholder={field.placeholder}
|
|
24
|
-
value={value || ''}
|
|
25
|
-
onChange={(e) => onChange(e.target.value)}
|
|
26
|
-
style={{ borderColor: errors ? 'red' : '#ccc' }}
|
|
27
|
-
/>
|
|
28
|
-
{errors && <p style={{ color: 'red', fontSize: '0.8em' }}>{Object.values(errors).join(', ')}</p>}
|
|
29
|
-
</div>
|
|
30
|
-
);
|
|
31
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Validation } from './validation';
|
|
2
|
-
import type { IPlugin, IPluginContext } from '@/shared/model/plugins/types';
|
|
3
|
-
import type { IUserRule } from './types';
|
|
4
|
-
|
|
5
|
-
export type TValidator = {
|
|
6
|
-
rules?: IUserRule[];
|
|
7
|
-
onSubmit?: boolean;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export function createValidationPlugin(config?: TValidator): IPlugin {
|
|
11
|
-
const engine = new Validation(
|
|
12
|
-
config?.onSubmit ?? false,
|
|
13
|
-
config?.rules ?? []
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
name: 'validation',
|
|
18
|
-
install(ctx: IPluginContext) {
|
|
19
|
-
ctx.events.tap<{ path: string; field: { validation?: string[] } }>(
|
|
20
|
-
'field:register',
|
|
21
|
-
({ path, field }) => {
|
|
22
|
-
engine.registerField(path, field.validation ?? []);
|
|
23
|
-
}
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
ctx.pipeline.use('field:change', (data, next) => {
|
|
27
|
-
const errors = engine.validate(data.field.validation ?? [], data.value, data.formData);
|
|
28
|
-
return next({ ...data, errors });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
ctx.pipeline.use('form:validate', (data, next) => {
|
|
32
|
-
const errors = engine.validateAll(data.formData);
|
|
33
|
-
return next({ ...data, errors });
|
|
34
|
-
});
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type { IUserRule };
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { confirm } from './confirm'
|
|
3
|
-
|
|
4
|
-
describe('confirm rule', () => {
|
|
5
|
-
it('returns true for matching values', () => {
|
|
6
|
-
const data = { password: 'secret' }
|
|
7
|
-
expect(confirm.fn('secret', data, ['password'])).toBe(true)
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('returns false for non-matching values', () => {
|
|
11
|
-
const data = { password: 'secret' }
|
|
12
|
-
expect(confirm.fn('wrong', data, ['password'])).toBe(false)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('returns false when no args provided (attr === false)', () => {
|
|
16
|
-
expect(confirm.fn('value', {}, [])).toBe(false)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('returns true for nested field path', () => {
|
|
20
|
-
const data = { user: { password: 'abc' } }
|
|
21
|
-
expect(confirm.fn('abc', data, ['user.password'])).toBe(true)
|
|
22
|
-
})
|
|
23
|
-
})
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type {IUserRule} from "@/plugins/validation/types";
|
|
2
|
-
import {getNestedValue} from "@/shared/utils";
|
|
3
|
-
|
|
4
|
-
export const confirm: IUserRule = {
|
|
5
|
-
code: 'confirm',
|
|
6
|
-
fn: (value, data, args) => {
|
|
7
|
-
const attr = args[0] ?? false;
|
|
8
|
-
return attr ? value == getNestedValue(data, attr) : false;
|
|
9
|
-
},
|
|
10
|
-
message: 'Поле не совпадает с ::attr(1)'
|
|
11
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { email } from './email'
|
|
3
|
-
|
|
4
|
-
describe('email rule', () => {
|
|
5
|
-
it('returns true for valid email', () => {
|
|
6
|
-
expect(email.fn('user@example.com', {}, [])).toBe(true)
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
it('returns false without @', () => {
|
|
10
|
-
expect(email.fn('userexample.com', {}, [])).toBe(false)
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('returns false without domain', () => {
|
|
14
|
-
expect(email.fn('user@', {}, [])).toBe(false)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('returns false for empty string', () => {
|
|
18
|
-
expect(email.fn('', {}, [])).toBe(false)
|
|
19
|
-
})
|
|
20
|
-
})
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type {IUserRule} from "@/plugins/validation/types";
|
|
2
|
-
|
|
3
|
-
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
4
|
-
export const email: IUserRule = {
|
|
5
|
-
code: 'email',
|
|
6
|
-
fn: (value) => {
|
|
7
|
-
return emailRegex.test(value);
|
|
8
|
-
},
|
|
9
|
-
message: 'Поле не является email'
|
|
10
|
-
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { required } from './required'
|
|
3
|
-
|
|
4
|
-
describe('required rule', () => {
|
|
5
|
-
it('returns false for empty string', () => {
|
|
6
|
-
expect(required.fn('', {}, [])).toBe(false)
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
it('returns false for undefined', () => {
|
|
10
|
-
expect(required.fn(undefined, {}, [])).toBe(false)
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('returns false for null', () => {
|
|
14
|
-
expect(required.fn(null, {}, [])).toBe(false)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('returns true for non-empty string', () => {
|
|
18
|
-
expect(required.fn('hello', {}, [])).toBe(true)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('returns true for string with spaces (length > 0)', () => {
|
|
22
|
-
expect(required.fn(' ', {}, [])).toBe(true)
|
|
23
|
-
})
|
|
24
|
-
})
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { Validation } from './validation'
|
|
3
|
-
import type { IUserRule } from './types'
|
|
4
|
-
|
|
5
|
-
describe('Validation', () => {
|
|
6
|
-
describe('validate()', () => {
|
|
7
|
-
it('returns [] when onSubmit=true', () => {
|
|
8
|
-
const v = new Validation(true)
|
|
9
|
-
expect(v.validate(['required'], '', {})).toEqual([])
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('returns errors when onSubmit=false', () => {
|
|
13
|
-
const v = new Validation(false)
|
|
14
|
-
const errors = v.validate(['required'], '', {})
|
|
15
|
-
expect(errors.length).toBeGreaterThan(0)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('returns [] when value is valid', () => {
|
|
19
|
-
const v = new Validation(false)
|
|
20
|
-
expect(v.validate(['required'], 'hello', {})).toEqual([])
|
|
21
|
-
})
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('validateAll()', () => {
|
|
25
|
-
it('validates all registered fields', () => {
|
|
26
|
-
const v = new Validation(false)
|
|
27
|
-
v.registerField('name', ['required'])
|
|
28
|
-
v.registerField('email', ['email'])
|
|
29
|
-
const errors = v.validateAll({ name: '', email: 'notanemail' })
|
|
30
|
-
expect(errors.name).toBeDefined()
|
|
31
|
-
expect(errors.email).toBeDefined()
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('returns empty object when all fields are valid', () => {
|
|
35
|
-
const v = new Validation(false)
|
|
36
|
-
v.registerField('name', ['required'])
|
|
37
|
-
const errors = v.validateAll({ name: 'John' })
|
|
38
|
-
expect(errors).toEqual({})
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('unknown rule code returns null (no error added)', () => {
|
|
43
|
-
const v = new Validation(false)
|
|
44
|
-
const errors = v.validate(['unknownRule'], 'value', {})
|
|
45
|
-
expect(errors).toEqual([])
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('replaceMessageArgs substitutes ::attr(N) in message', () => {
|
|
49
|
-
const customRule: IUserRule = {
|
|
50
|
-
code: 'minLen',
|
|
51
|
-
fn: (value: any, _data: any, args: string[]) => String(value).length >= Number(args[0]),
|
|
52
|
-
message: 'Min length is ::attr(0)',
|
|
53
|
-
}
|
|
54
|
-
const v = new Validation(false, [customRule])
|
|
55
|
-
const errors = v.validate(['minLen:5'], 'hi', {})
|
|
56
|
-
expect(errors[0]).toBe('Min length is 5')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('custom rules merge with built-in rules', () => {
|
|
60
|
-
const customRule: IUserRule = {
|
|
61
|
-
code: 'custom',
|
|
62
|
-
fn: () => false,
|
|
63
|
-
message: 'Custom error',
|
|
64
|
-
}
|
|
65
|
-
const v = new Validation(false, [customRule])
|
|
66
|
-
expect(v.validate(['required'], '', {})).toHaveLength(1)
|
|
67
|
-
expect(v.validate(['custom'], 'anything', {})).toHaveLength(1)
|
|
68
|
-
})
|
|
69
|
-
})
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type {FormData} from "@/shared/types/common";
|
|
2
|
-
import base from './rules';
|
|
3
|
-
import type {IUserRule} from "@/plugins/validation/types";
|
|
4
|
-
import {getNestedValue} from "@/shared/utils";
|
|
5
|
-
|
|
6
|
-
export class Validation {
|
|
7
|
-
private readonly registry: IUserRule[] = [];
|
|
8
|
-
private registerFields: Record<string, string[]> = {};
|
|
9
|
-
constructor(public readonly onSubmit: boolean, rules: IUserRule[] = []) {
|
|
10
|
-
this.registry = [...rules, ...base];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
private validateRule(rule: string, data: unknown, formData: FormData): string|null {
|
|
15
|
-
const [code, rawArgs] = rule.split(':');
|
|
16
|
-
const args = rawArgs ? rawArgs.split(',') : [];
|
|
17
|
-
const userRule = this.registry.find(i => i.code == code);
|
|
18
|
-
if (!userRule) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
const validateStatus = userRule.fn(data, formData, args);
|
|
22
|
-
return !validateStatus ? this.replaceMessageArgs(userRule.message, args) : null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private replaceMessageArgs(message: string, args: string[] = []): string {
|
|
26
|
-
const replaceArgs = args.reduce<Record<string, string>>((acc, arg, index) => {
|
|
27
|
-
acc[`::attr(${index})`] = arg;
|
|
28
|
-
return acc;
|
|
29
|
-
}, {});
|
|
30
|
-
|
|
31
|
-
return message.replace(/::attr\(\d\)/g, i => replaceArgs[i]);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
public registerField(path: string, validators: string[]): void {
|
|
35
|
-
this.registerFields[path] = validators;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
private _validate(rules: string[], data: unknown, formData: FormData): string[] {
|
|
39
|
-
return rules.map((rule) => {
|
|
40
|
-
return this.validateRule(rule, data, formData);
|
|
41
|
-
}).filter(i => i !== null)
|
|
42
|
-
}
|
|
43
|
-
validate(rules: string[], data: unknown, formData: FormData): string[] {
|
|
44
|
-
return !this.onSubmit ? this._validate(rules, data, formData) : [];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
public validateAll(formData: FormData): Record<string, string[]> {
|
|
48
|
-
return Object.entries(this.registerFields).reduce<Record<string, string[]>>((acc, [path, rules]) => {
|
|
49
|
-
const value = getNestedValue(formData, path);
|
|
50
|
-
const validationMessage = this._validate(rules, value, formData);
|
|
51
|
-
if (validationMessage.length) {
|
|
52
|
-
acc[path] = validationMessage;
|
|
53
|
-
}
|
|
54
|
-
return acc;
|
|
55
|
-
} , {});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type {TEqualRule, TGroupRules, TInRule, TCommonRule} from "@/plugins/visibility/types";
|
|
2
|
-
import type {FormData} from "@/shared/types/common";
|
|
3
|
-
import {getNestedValue} from "@/shared/utils";
|
|
4
|
-
export class VisibilityCore {
|
|
5
|
-
private registerFields: Record<string, TGroupRules> = {};
|
|
6
|
-
|
|
7
|
-
registerField(path: string, rule: TGroupRules): void {
|
|
8
|
-
this.registerFields[path] = rule;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
private static isGroupRule(i: TGroupRules | TCommonRule): i is TGroupRules {
|
|
12
|
-
return 'rules' in i;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
private static isInOperator(i: TCommonRule): i is TInRule {
|
|
16
|
-
return i.operator == 'in' ;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
private static isEqualOperand(i: TCommonRule): i is TEqualRule {
|
|
20
|
-
return i.operator == '=' ;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
static checkGroup(groupRule: TGroupRules, formData: FormData): boolean {
|
|
24
|
-
const logic = groupRule.logic;
|
|
25
|
-
const items: boolean[] = [];
|
|
26
|
-
groupRule.rules.forEach((rule: TGroupRules | TCommonRule) => {
|
|
27
|
-
if (this.isGroupRule(rule)) {
|
|
28
|
-
items.push(this.checkGroup(rule, formData));
|
|
29
|
-
} else {
|
|
30
|
-
const value = getNestedValue(formData, rule.field)
|
|
31
|
-
if (this.isInOperator(rule)) {
|
|
32
|
-
items.push(rule.value.indexOf(value) !== -1);
|
|
33
|
-
} else if (this.isEqualOperand(rule)) {
|
|
34
|
-
items.push(rule.value == value);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
if (logic == 'or') {
|
|
39
|
-
return items.indexOf(true) != -1
|
|
40
|
-
} else {
|
|
41
|
-
return items.indexOf(false) == -1;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
isView(path: string, data: FormData): boolean {
|
|
46
|
-
const rule = this.checkInRule(path);
|
|
47
|
-
if (!this.checkInRule(path)) {
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
return VisibilityCore.checkGroup(rule, data);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private checkInRule(path: string) {
|
|
54
|
-
return this.registerFields[path];
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type {IPlugin, IPluginContext} from "~/src";
|
|
2
|
-
import {VisibilityCore} from "@/plugins/visibility/core";
|
|
3
|
-
import type {TGroupRules} from "@/plugins/visibility/types";
|
|
4
|
-
|
|
5
|
-
export function createVisibilityPlugin(): IPlugin {
|
|
6
|
-
const engine = new VisibilityCore();
|
|
7
|
-
return {
|
|
8
|
-
name: 'visibility',
|
|
9
|
-
install(ctx: IPluginContext) {
|
|
10
|
-
ctx.events.tap<{ path: string; field: { visibility?: TGroupRules } }>(
|
|
11
|
-
'field:register',
|
|
12
|
-
({ path, field }) => {
|
|
13
|
-
if (field.visibility) {
|
|
14
|
-
engine.registerField(path, field.visibility);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
ctx.pipeline.useSync('field:visible', (data, next) => {
|
|
20
|
-
const visible = engine.isView(data.path, data.formData);
|
|
21
|
-
return next({ ...data, visible});
|
|
22
|
-
});
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export type TInRule = {
|
|
2
|
-
operator: 'in',
|
|
3
|
-
value: (string | number | boolean)[]
|
|
4
|
-
} & TCommonRule
|
|
5
|
-
|
|
6
|
-
export type TEqualRule = {
|
|
7
|
-
operator: '=',
|
|
8
|
-
value: string | number | boolean
|
|
9
|
-
} & TCommonRule
|
|
10
|
-
|
|
11
|
-
export type TCommonRule = {
|
|
12
|
-
operator: string,
|
|
13
|
-
field: string,
|
|
14
|
-
value: any,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export type TGroupRules = {
|
|
18
|
-
logic: 'and' | 'or',
|
|
19
|
-
rules: (TGroupRules | TCommonRule)[]
|
|
20
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useEffect,
|
|
3
|
-
useRef,
|
|
4
|
-
type DependencyList,
|
|
5
|
-
type EffectCallback,
|
|
6
|
-
} from 'react';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Хук как useEffect, но не выполняется на первом рендере.
|
|
10
|
-
* Выполняется только на обновлениях, когда меняются зависимости.
|
|
11
|
-
*/
|
|
12
|
-
export const useUpdateEffect = (effect: EffectCallback, deps: DependencyList): void => {
|
|
13
|
-
const isFirstRenderRef = useRef(true);
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
if (isFirstRenderRef.current) {
|
|
17
|
-
isFirstRenderRef.current = false;
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return effect();
|
|
22
|
-
}, deps);
|
|
23
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { IEventBus } from './types';
|
|
2
|
-
|
|
3
|
-
export class EventBus implements IEventBus {
|
|
4
|
-
private listeners = new Map<string, Set<(data: any) => void>>();
|
|
5
|
-
|
|
6
|
-
on<T = unknown>(event: string, handler: (data: T) => void): () => void {
|
|
7
|
-
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
|
|
8
|
-
this.listeners.get(event)!.add(handler);
|
|
9
|
-
return () => this.listeners.get(event)?.delete(handler);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
emit<T = unknown>(event: string, data: T): void {
|
|
13
|
-
this.listeners.get(event)?.forEach((h) => h(data));
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { IFieldRegistry } from './types';
|
|
2
|
-
import type { FormElementComponent } from '@/shared/types/common';
|
|
3
|
-
|
|
4
|
-
export class FieldRegistry implements IFieldRegistry {
|
|
5
|
-
private components = new Map<string, FormElementComponent<any>>();
|
|
6
|
-
|
|
7
|
-
register(type: string, component: FormElementComponent<any>): () => void {
|
|
8
|
-
this.components.set(type, component);
|
|
9
|
-
return () => this.components.delete(type);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
getAll(): Record<string, FormElementComponent<any>> {
|
|
13
|
-
return Object.fromEntries(this.components);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { FormHookName, HookHandler, IEventRegistry } from './types';
|
|
2
|
-
|
|
3
|
-
export class HookRegistry implements IEventRegistry {
|
|
4
|
-
private handlers = new Map<string, Set<HookHandler<any>>>();
|
|
5
|
-
|
|
6
|
-
tap<T>(hook: FormHookName, handler: HookHandler<T>): () => void {
|
|
7
|
-
if (!this.handlers.has(hook)) {
|
|
8
|
-
this.handlers.set(hook, new Set());
|
|
9
|
-
}
|
|
10
|
-
this.handlers.get(hook)!.add(handler as HookHandler<any>);
|
|
11
|
-
return () => {
|
|
12
|
-
this.handlers.get(hook)?.delete(handler as HookHandler<any>);
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async call<T>(hook: FormHookName, data: T): Promise<void> {
|
|
17
|
-
const set = this.handlers.get(hook);
|
|
18
|
-
if (!set) return;
|
|
19
|
-
await Promise.all([...set].map((h) => h(data)));
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { IMiddlewareRegistry, StoreMiddleware } from './types';
|
|
2
|
-
|
|
3
|
-
export class MiddlewareRegistry implements IMiddlewareRegistry {
|
|
4
|
-
private middlewares: StoreMiddleware[] = [];
|
|
5
|
-
|
|
6
|
-
use(middleware: StoreMiddleware): () => void {
|
|
7
|
-
this.middlewares.push(middleware);
|
|
8
|
-
return () => {
|
|
9
|
-
const i = this.middlewares.indexOf(middleware);
|
|
10
|
-
if (i >= 0) this.middlewares.splice(i, 1);
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
getAll(): StoreMiddleware[] {
|
|
15
|
-
return [...this.middlewares];
|
|
16
|
-
}
|
|
17
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AsyncPipelineName,
|
|
3
|
-
SyncPipelineName,
|
|
4
|
-
PipelineStage,
|
|
5
|
-
SyncPipelineStage,
|
|
6
|
-
IPipelineRegistry,
|
|
7
|
-
AsyncPipelineDataMap,
|
|
8
|
-
SyncPipelineDataMap,
|
|
9
|
-
} from './types';
|
|
10
|
-
|
|
11
|
-
type SortedAsyncStage<T> = { stage: PipelineStage<T>; priority: number };
|
|
12
|
-
type SortedSyncStage<T> = { stage: SyncPipelineStage<T>; priority: number };
|
|
13
|
-
|
|
14
|
-
export class PipelineRegistry implements IPipelineRegistry {
|
|
15
|
-
private stages = new Map<string, SortedAsyncStage<any>[]>();
|
|
16
|
-
private syncStages = new Map<string, SortedSyncStage<any>[]>();
|
|
17
|
-
|
|
18
|
-
use<K extends AsyncPipelineName>(
|
|
19
|
-
pipeline: K,
|
|
20
|
-
stage: PipelineStage<AsyncPipelineDataMap[K]>,
|
|
21
|
-
priority = 10
|
|
22
|
-
): () => void {
|
|
23
|
-
if (!this.stages.has(pipeline)) this.stages.set(pipeline, []);
|
|
24
|
-
const arr = this.stages.get(pipeline)!;
|
|
25
|
-
const entry = { stage, priority };
|
|
26
|
-
arr.push(entry);
|
|
27
|
-
arr.sort((a, b) => b.priority - a.priority);
|
|
28
|
-
return () => {
|
|
29
|
-
const i = arr.indexOf(entry);
|
|
30
|
-
if (i >= 0) arr.splice(i, 1);
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
useSync<K extends SyncPipelineName>(
|
|
35
|
-
pipeline: K,
|
|
36
|
-
stage: SyncPipelineStage<SyncPipelineDataMap[K]>,
|
|
37
|
-
priority = 10
|
|
38
|
-
): () => void {
|
|
39
|
-
if (!this.syncStages.has(pipeline)) this.syncStages.set(pipeline, []);
|
|
40
|
-
const arr = this.syncStages.get(pipeline)!;
|
|
41
|
-
const entry = { stage, priority };
|
|
42
|
-
arr.push(entry);
|
|
43
|
-
arr.sort((a, b) => b.priority - a.priority);
|
|
44
|
-
return () => {
|
|
45
|
-
const i = arr.indexOf(entry);
|
|
46
|
-
if (i >= 0) arr.splice(i, 1);
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async run<K extends AsyncPipelineName>(
|
|
51
|
-
name: K,
|
|
52
|
-
initialData: AsyncPipelineDataMap[K]
|
|
53
|
-
): Promise<AsyncPipelineDataMap[K]> {
|
|
54
|
-
const arr = this.stages.get(name) ?? [];
|
|
55
|
-
const compose = (index: number) => (data: AsyncPipelineDataMap[K]): Promise<AsyncPipelineDataMap[K]> => {
|
|
56
|
-
if (index >= arr.length) return Promise.resolve(data);
|
|
57
|
-
return arr[index].stage(data, compose(index + 1));
|
|
58
|
-
};
|
|
59
|
-
return compose(0)(initialData);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
runSync<K extends SyncPipelineName>(
|
|
63
|
-
name: K,
|
|
64
|
-
initialData: SyncPipelineDataMap[K]
|
|
65
|
-
): SyncPipelineDataMap[K] {
|
|
66
|
-
const arr = this.syncStages.get(name) ?? [];
|
|
67
|
-
const compose = (index: number) => (data: SyncPipelineDataMap[K]): SyncPipelineDataMap[K] => {
|
|
68
|
-
if (index >= arr.length) return data;
|
|
69
|
-
return arr[index].stage(data, compose(index + 1));
|
|
70
|
-
};
|
|
71
|
-
return compose(0)(initialData);
|
|
72
|
-
}
|
|
73
|
-
}
|