@aerogel/core 0.0.0-next.f8cdd39997c56dcd46e07c26af8a84d04d610fce → 0.0.0-next.f9394854509d71d644498ac087706a2f8f8eea1c
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/dist/aerogel-core.cjs.js +1 -1
- package/dist/aerogel-core.cjs.js.map +1 -1
- package/dist/aerogel-core.d.ts +1345 -231
- package/dist/aerogel-core.esm.js +1 -1
- package/dist/aerogel-core.esm.js.map +1 -1
- package/histoire.config.ts +7 -0
- package/package.json +14 -5
- package/postcss.config.js +6 -0
- package/src/assets/histoire.css +3 -0
- package/src/bootstrap/bootstrap.test.ts +3 -3
- package/src/bootstrap/index.ts +32 -5
- package/src/bootstrap/options.ts +3 -0
- package/src/components/AGAppLayout.vue +7 -2
- package/src/components/AGAppOverlays.vue +5 -1
- package/src/components/AGAppSnackbars.vue +2 -2
- package/src/components/composition.ts +23 -0
- package/src/components/forms/AGCheckbox.vue +7 -1
- package/src/components/forms/AGForm.vue +9 -10
- package/src/components/forms/AGInput.vue +10 -6
- package/src/components/forms/AGSelect.story.vue +46 -0
- package/src/components/forms/AGSelect.vue +60 -0
- package/src/components/forms/index.ts +5 -6
- package/src/components/headless/forms/AGHeadlessButton.ts +3 -0
- package/src/components/headless/forms/AGHeadlessButton.vue +23 -12
- package/src/components/headless/forms/AGHeadlessInput.ts +30 -4
- package/src/components/headless/forms/AGHeadlessInput.vue +23 -7
- package/src/components/headless/forms/AGHeadlessInputDescription.vue +28 -0
- package/src/components/headless/forms/AGHeadlessInputInput.vue +44 -5
- package/src/components/headless/forms/AGHeadlessInputLabel.vue +8 -2
- package/src/components/headless/forms/AGHeadlessInputTextArea.vue +43 -0
- package/src/components/headless/forms/AGHeadlessSelect.ts +42 -0
- package/src/components/headless/forms/AGHeadlessSelect.vue +77 -0
- package/src/components/headless/forms/AGHeadlessSelectButton.vue +24 -0
- package/src/components/headless/forms/AGHeadlessSelectError.vue +26 -0
- package/src/components/headless/forms/AGHeadlessSelectLabel.vue +24 -0
- package/src/components/headless/forms/AGHeadlessSelectOption.ts +4 -0
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +39 -0
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +3 -0
- package/src/components/headless/forms/composition.ts +10 -0
- package/src/components/headless/forms/index.ts +13 -1
- package/src/components/headless/modals/AGHeadlessModal.ts +27 -0
- package/src/components/headless/modals/AGHeadlessModal.vue +3 -5
- package/src/components/headless/modals/index.ts +4 -6
- package/src/components/headless/snackbars/index.ts +23 -8
- package/src/components/index.ts +3 -1
- package/src/components/interfaces.ts +24 -0
- package/src/components/{basic → lib}/AGErrorMessage.vue +2 -2
- package/src/components/{basic → lib}/AGMarkdown.vue +9 -4
- package/src/components/lib/AGMeasured.vue +16 -0
- package/src/components/lib/AGStartupCrash.vue +31 -0
- package/src/components/lib/index.ts +5 -0
- package/src/components/modals/AGAlertModal.ts +15 -0
- package/src/components/modals/AGAlertModal.vue +4 -15
- package/src/components/modals/AGConfirmModal.ts +33 -0
- package/src/components/modals/AGConfirmModal.vue +9 -13
- package/src/components/modals/AGErrorReportModal.ts +27 -1
- package/src/components/modals/AGErrorReportModal.vue +8 -16
- package/src/components/modals/AGErrorReportModalButtons.vue +4 -2
- package/src/components/modals/AGErrorReportModalTitle.vue +1 -1
- package/src/components/modals/AGLoadingModal.ts +23 -0
- package/src/components/modals/AGLoadingModal.vue +4 -8
- package/src/components/modals/AGModal.ts +1 -1
- package/src/components/modals/AGModal.vue +15 -12
- package/src/components/modals/AGModalTitle.vue +9 -0
- package/src/components/modals/AGPromptModal.ts +36 -0
- package/src/components/modals/AGPromptModal.vue +34 -0
- package/src/components/modals/index.ts +13 -17
- package/src/components/snackbars/AGSnackbar.vue +3 -9
- package/src/components/utils.ts +10 -0
- package/src/directives/index.ts +5 -1
- package/src/directives/measure.ts +40 -0
- package/src/errors/Errors.ts +26 -24
- package/src/errors/index.ts +10 -23
- package/src/errors/utils.ts +35 -0
- package/src/forms/Form.test.ts +28 -0
- package/src/forms/Form.ts +80 -14
- package/src/forms/index.ts +3 -1
- package/src/forms/utils.ts +34 -3
- package/src/forms/validation.ts +19 -0
- package/src/jobs/Job.ts +5 -0
- package/src/jobs/index.ts +7 -0
- package/src/lang/DefaultLangProvider.ts +43 -0
- package/src/lang/Lang.state.ts +11 -0
- package/src/lang/Lang.ts +44 -29
- package/src/main.histoire.ts +1 -0
- package/src/main.ts +3 -0
- package/src/plugins/Plugin.ts +1 -0
- package/src/plugins/index.ts +19 -0
- package/src/services/App.state.ts +20 -5
- package/src/services/App.ts +38 -3
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +100 -30
- package/src/services/Service.ts +59 -17
- package/src/services/index.ts +5 -2
- package/src/services/store.ts +8 -5
- package/src/testing/index.ts +25 -0
- package/src/testing/setup.ts +19 -0
- package/src/ui/UI.state.ts +7 -0
- package/src/ui/UI.ts +155 -22
- package/src/ui/index.ts +9 -3
- package/src/ui/utils.ts +16 -0
- package/src/utils/composition/events.ts +1 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/tailwindcss.test.ts +26 -0
- package/src/utils/tailwindcss.ts +7 -0
- package/src/utils/vue.ts +23 -5
- package/tailwind.config.js +4 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +4 -1
- package/.eslintrc.js +0 -3
- package/dist/virtual.d.ts +0 -11
- package/src/components/basic/index.ts +0 -5
- package/src/types/virtual.d.ts +0 -11
- /package/src/components/{basic → lib}/AGLink.vue +0 -0
package/src/errors/Errors.ts
CHANGED
|
@@ -7,7 +7,10 @@ import { translateWithDefault } from '@/lang/utils';
|
|
|
7
7
|
|
|
8
8
|
import Service from './Errors.state';
|
|
9
9
|
import { Colors } from '@/components/constants';
|
|
10
|
+
import { Events } from '@/services';
|
|
11
|
+
import type { AGErrorReportModalProps } from '@/components/modals/AGErrorReportModal';
|
|
10
12
|
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
13
|
+
import type { ModalComponent } from '@/ui/UI.state';
|
|
11
14
|
|
|
12
15
|
export class ErrorsService extends Service {
|
|
13
16
|
|
|
@@ -23,7 +26,7 @@ export class ErrorsService extends Service {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
public async inspect(error: ErrorSource | ErrorReport[]): Promise<void> {
|
|
26
|
-
const reports = Array.isArray(error) ? error : [await this.createErrorReport(error)];
|
|
29
|
+
const reports = Array.isArray(error) ? (error as ErrorReport[]) : [await this.createErrorReport(error)];
|
|
27
30
|
|
|
28
31
|
if (reports.length === 0) {
|
|
29
32
|
UI.alert(translateWithDefault('errors.inspectEmpty', 'Nothing to inspect!'));
|
|
@@ -31,11 +34,19 @@ export class ErrorsService extends Service {
|
|
|
31
34
|
return;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
UI.openModal(UI.requireComponent(UIComponents.ErrorReportModal), {
|
|
37
|
+
UI.openModal<ModalComponent<AGErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
|
|
38
|
+
reports,
|
|
39
|
+
});
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
public async report(error: ErrorSource, message?: string): Promise<void> {
|
|
38
|
-
|
|
43
|
+
await Events.emit('error', { error, message });
|
|
44
|
+
|
|
45
|
+
if (App.testing) {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (App.development) {
|
|
39
50
|
this.logError(error);
|
|
40
51
|
}
|
|
41
52
|
|
|
@@ -43,7 +54,7 @@ export class ErrorsService extends Service {
|
|
|
43
54
|
throw error;
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
if (!App.isMounted) {
|
|
57
|
+
if (!App.isMounted()) {
|
|
47
58
|
const startupError = await this.createStartupErrorReport(error);
|
|
48
59
|
|
|
49
60
|
if (startupError) {
|
|
@@ -70,9 +81,10 @@ export class ErrorsService extends Service {
|
|
|
70
81
|
text: translateWithDefault('errors.viewDetails', 'View details'),
|
|
71
82
|
dismiss: true,
|
|
72
83
|
handler: () =>
|
|
73
|
-
UI.openModal(
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
UI.openModal<ModalComponent<AGErrorReportModalProps>>(
|
|
85
|
+
UI.requireComponent(UIComponents.ErrorReportModal),
|
|
86
|
+
{ reports: [report] },
|
|
87
|
+
),
|
|
76
88
|
},
|
|
77
89
|
],
|
|
78
90
|
},
|
|
@@ -105,22 +117,6 @@ export class ErrorsService extends Service {
|
|
|
105
117
|
});
|
|
106
118
|
}
|
|
107
119
|
|
|
108
|
-
public getErrorMessage(error: ErrorSource): string {
|
|
109
|
-
if (typeof error === 'string') {
|
|
110
|
-
return error;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (error instanceof Error || error instanceof JSError) {
|
|
114
|
-
return error.message;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (isObject(error)) {
|
|
118
|
-
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
120
|
private logError(error: unknown): void {
|
|
125
121
|
// eslint-disable-next-line no-console
|
|
126
122
|
console.error(error);
|
|
@@ -180,4 +176,10 @@ export class ErrorsService extends Service {
|
|
|
180
176
|
|
|
181
177
|
}
|
|
182
178
|
|
|
183
|
-
export default facade(
|
|
179
|
+
export default facade(ErrorsService);
|
|
180
|
+
|
|
181
|
+
declare module '@/services/Events' {
|
|
182
|
+
export interface EventsPayload {
|
|
183
|
+
error: { error: ErrorSource; message?: string };
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/errors/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { App } from 'vue';
|
|
2
2
|
|
|
3
3
|
import { bootServices } from '@/services';
|
|
4
4
|
import { definePlugin } from '@/plugins';
|
|
@@ -6,33 +6,22 @@ import { definePlugin } from '@/plugins';
|
|
|
6
6
|
import Errors from './Errors';
|
|
7
7
|
import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
8
8
|
|
|
9
|
+
export * from './utils';
|
|
9
10
|
export { Errors, ErrorSource, ErrorReport, ErrorReportLog };
|
|
10
11
|
|
|
11
12
|
const services = { $errors: Errors };
|
|
12
13
|
const frameworkHandler: ErrorHandler = (error) => {
|
|
13
|
-
if (!Errors.instance) {
|
|
14
|
-
// eslint-disable-next-line no-console
|
|
15
|
-
console.warn('Errors service hasn\'t been initialized properly!');
|
|
16
|
-
|
|
17
|
-
// eslint-disable-next-line no-console
|
|
18
|
-
console.error(error);
|
|
19
|
-
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
14
|
Errors.report(error);
|
|
24
15
|
|
|
25
16
|
return true;
|
|
26
17
|
};
|
|
27
18
|
|
|
28
|
-
function setUpErrorHandler(baseHandler: ErrorHandler = () => false):
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
);
|
|
19
|
+
function setUpErrorHandler(app: App, baseHandler: ErrorHandler = () => false): void {
|
|
20
|
+
const errorHandler: ErrorHandler = (error) => baseHandler(error) || frameworkHandler(error);
|
|
21
|
+
|
|
22
|
+
app.config.errorHandler = errorHandler;
|
|
23
|
+
globalThis.onerror = (event, _, __, ___, error) => errorHandler(error ?? event);
|
|
24
|
+
globalThis.onunhandledrejection = (event) => errorHandler(event.reason);
|
|
36
25
|
}
|
|
37
26
|
|
|
38
27
|
export type ErrorHandler = (error: ErrorSource) => boolean;
|
|
@@ -40,16 +29,14 @@ export type ErrorsServices = typeof services;
|
|
|
40
29
|
|
|
41
30
|
export default definePlugin({
|
|
42
31
|
async install(app, options) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
app.config.errorHandler = errorHandler;
|
|
32
|
+
setUpErrorHandler(app, options.handleError);
|
|
46
33
|
|
|
47
34
|
await bootServices(app, services);
|
|
48
35
|
},
|
|
49
36
|
});
|
|
50
37
|
|
|
51
38
|
declare module '@/bootstrap/options' {
|
|
52
|
-
interface AerogelOptions {
|
|
39
|
+
export interface AerogelOptions {
|
|
53
40
|
handleError?(error: ErrorSource): boolean;
|
|
54
41
|
}
|
|
55
42
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { JSError, isObject, toString } from '@noeldemartin/utils';
|
|
2
|
+
import { translateWithDefault } from '@/lang/utils';
|
|
3
|
+
import type { ErrorSource } from './Errors.state';
|
|
4
|
+
|
|
5
|
+
const handlers: ErrorHandler[] = [];
|
|
6
|
+
|
|
7
|
+
export type ErrorHandler = (error: ErrorSource) => string | undefined;
|
|
8
|
+
|
|
9
|
+
export function registerErrorHandler(handler: ErrorHandler): void {
|
|
10
|
+
handlers.push(handler);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getErrorMessage(error: ErrorSource): string {
|
|
14
|
+
for (const handler of handlers) {
|
|
15
|
+
const result = handler(error);
|
|
16
|
+
|
|
17
|
+
if (result) {
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof error === 'string') {
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
27
|
+
return error.message;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isObject(error)) {
|
|
31
|
+
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
35
|
+
}
|
package/src/forms/Form.test.ts
CHANGED
|
@@ -55,4 +55,32 @@ describe('Form', () => {
|
|
|
55
55
|
expect(form.name).toBeNull();
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
it('trims values', () => {
|
|
59
|
+
// Arrange
|
|
60
|
+
const form = useForm({
|
|
61
|
+
trimmed: {
|
|
62
|
+
type: FormFieldTypes.String,
|
|
63
|
+
rules: 'required',
|
|
64
|
+
},
|
|
65
|
+
untrimmed: {
|
|
66
|
+
type: FormFieldTypes.String,
|
|
67
|
+
rules: 'required',
|
|
68
|
+
trim: false,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
form.trimmed = ' ';
|
|
74
|
+
form.untrimmed = ' ';
|
|
75
|
+
|
|
76
|
+
form.submit();
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
expect(form.valid).toBe(false);
|
|
80
|
+
expect(form.submitted).toBe(true);
|
|
81
|
+
expect(form.trimmed).toEqual('');
|
|
82
|
+
expect(form.untrimmed).toEqual(' ');
|
|
83
|
+
expect(form.errors).toEqual({ trimmed: ['required'], untrimmed: null });
|
|
84
|
+
});
|
|
85
|
+
|
|
58
86
|
});
|
package/src/forms/Form.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { computed, nextTick, reactive, readonly, ref } from 'vue';
|
|
2
|
+
import { MagicObject, arrayRemove, fail, toString } from '@noeldemartin/utils';
|
|
3
|
+
import { validate } from './validation';
|
|
3
4
|
import type { ObjectValues } from '@noeldemartin/utils';
|
|
4
5
|
import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
|
|
5
6
|
|
|
@@ -7,16 +8,20 @@ export const FormFieldTypes = {
|
|
|
7
8
|
String: 'string',
|
|
8
9
|
Number: 'number',
|
|
9
10
|
Boolean: 'boolean',
|
|
11
|
+
Object: 'object',
|
|
12
|
+
Date: 'date',
|
|
10
13
|
} as const;
|
|
11
14
|
|
|
12
15
|
export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
|
|
13
16
|
type: TType;
|
|
17
|
+
trim?: boolean;
|
|
14
18
|
default?: GetFormFieldValue<TType>;
|
|
15
19
|
rules?: TRules;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
|
|
19
23
|
export type FormFieldType = ObjectValues<typeof FormFieldTypes>;
|
|
24
|
+
export type FormFieldValue = GetFormFieldValue<FormFieldType>;
|
|
20
25
|
|
|
21
26
|
export type FormData<T> = {
|
|
22
27
|
-readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
|
|
@@ -36,17 +41,26 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
|
|
|
36
41
|
? number
|
|
37
42
|
: TType extends typeof FormFieldTypes.Boolean
|
|
38
43
|
? boolean
|
|
44
|
+
: TType extends typeof FormFieldTypes.Object
|
|
45
|
+
? object
|
|
46
|
+
: TType extends typeof FormFieldTypes.Date
|
|
47
|
+
? Date
|
|
39
48
|
: never;
|
|
40
49
|
|
|
50
|
+
const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
|
|
51
|
+
|
|
52
|
+
export type SubmitFormListener = () => unknown;
|
|
53
|
+
export type FocusFormListener = (input: string) => unknown;
|
|
54
|
+
|
|
41
55
|
export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
|
|
42
56
|
|
|
43
57
|
public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
|
|
44
58
|
|
|
45
59
|
private _fields: Fields;
|
|
46
60
|
private _data: FormData<Fields>;
|
|
47
|
-
private _valid: ComputedRef<boolean>;
|
|
48
61
|
private _submitted: Ref<boolean>;
|
|
49
62
|
private _errors: FormErrors<Fields>;
|
|
63
|
+
private _listeners: { focus?: FocusFormListener[]; submit?: SubmitFormListener[] } = {};
|
|
50
64
|
|
|
51
65
|
constructor(fields: Fields) {
|
|
52
66
|
super();
|
|
@@ -55,13 +69,17 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
55
69
|
this._submitted = ref(false);
|
|
56
70
|
this._data = this.getInitialData(fields);
|
|
57
71
|
this._errors = this.getInitialErrors(fields);
|
|
58
|
-
|
|
72
|
+
|
|
73
|
+
validForms.set(
|
|
74
|
+
this,
|
|
75
|
+
computed(() => !Object.values(this._errors).some((error) => error !== null)),
|
|
76
|
+
);
|
|
59
77
|
|
|
60
78
|
this.errors = readonly(this._errors);
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
public get valid(): boolean {
|
|
64
|
-
return this
|
|
82
|
+
return !!validForms.get(this)?.value;
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
public get submitted(): boolean {
|
|
@@ -69,7 +87,13 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
public setFieldValue<T extends keyof Fields>(field: T, value: FormData<Fields>[T]): void {
|
|
72
|
-
|
|
90
|
+
const definition =
|
|
91
|
+
this._fields[field] ?? fail<FormFieldDefinition>(`Trying to set undefined '${toString(field)}' field`);
|
|
92
|
+
|
|
93
|
+
this._data[field] =
|
|
94
|
+
definition.type === FormFieldTypes.String && (definition.trim ?? true)
|
|
95
|
+
? (toString(value).trim() as FormData<Fields>[T])
|
|
96
|
+
: value;
|
|
73
97
|
|
|
74
98
|
if (this._submitted.value) {
|
|
75
99
|
this.validate();
|
|
@@ -80,6 +104,14 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
80
104
|
return this._data[field] as unknown as GetFormFieldValue<Fields[T]['type']>;
|
|
81
105
|
}
|
|
82
106
|
|
|
107
|
+
public getFieldRules<T extends keyof Fields>(field: T): string[] {
|
|
108
|
+
return this._fields[field]?.rules?.split('|') ?? [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public data(): FormData<Fields> {
|
|
112
|
+
return { ...this._data };
|
|
113
|
+
}
|
|
114
|
+
|
|
83
115
|
public validate(): boolean {
|
|
84
116
|
const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
|
|
85
117
|
formErrors[name] = this.getFieldErrors(name, definition);
|
|
@@ -92,17 +124,45 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
92
124
|
return this.valid;
|
|
93
125
|
}
|
|
94
126
|
|
|
95
|
-
public reset(): void {
|
|
127
|
+
public reset(options: { keepData?: boolean; keepErrors?: boolean } = {}): void {
|
|
96
128
|
this._submitted.value = false;
|
|
97
129
|
|
|
98
|
-
this.resetData();
|
|
99
|
-
this.resetErrors();
|
|
130
|
+
options.keepData || this.resetData();
|
|
131
|
+
options.keepErrors || this.resetErrors();
|
|
100
132
|
}
|
|
101
133
|
|
|
102
134
|
public submit(): boolean {
|
|
103
135
|
this._submitted.value = true;
|
|
104
136
|
|
|
105
|
-
|
|
137
|
+
const valid = this.validate();
|
|
138
|
+
|
|
139
|
+
valid && this._listeners['submit']?.forEach((listener) => listener());
|
|
140
|
+
|
|
141
|
+
return valid;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public on(event: 'focus', listener: FocusFormListener): () => void;
|
|
145
|
+
public on(event: 'submit', listener: SubmitFormListener): () => void;
|
|
146
|
+
public on(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): () => void {
|
|
147
|
+
this._listeners[event] ??= [];
|
|
148
|
+
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
+
this._listeners[event]?.push(listener as any);
|
|
151
|
+
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
+
return () => this.off(event as any, listener);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public off(event: 'focus', listener: FocusFormListener): void;
|
|
157
|
+
public off(event: 'submit', listener: SubmitFormListener): void;
|
|
158
|
+
public off(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): void {
|
|
159
|
+
arrayRemove(this._listeners[event] ?? [], listener);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public async focus(input: string): Promise<void> {
|
|
163
|
+
await nextTick();
|
|
164
|
+
|
|
165
|
+
this._listeners['focus']?.forEach((listener) => listener(input));
|
|
106
166
|
}
|
|
107
167
|
|
|
108
168
|
protected __get(property: string): unknown {
|
|
@@ -110,7 +170,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
110
170
|
return super.__get(property);
|
|
111
171
|
}
|
|
112
172
|
|
|
113
|
-
return this.
|
|
173
|
+
return this.getFieldValue(property);
|
|
114
174
|
}
|
|
115
175
|
|
|
116
176
|
protected __set(property: string, value: unknown): void {
|
|
@@ -120,14 +180,20 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
120
180
|
return;
|
|
121
181
|
}
|
|
122
182
|
|
|
123
|
-
|
|
183
|
+
this.setFieldValue(property, value as FormData<Fields>[string]);
|
|
124
184
|
}
|
|
125
185
|
|
|
126
186
|
private getFieldErrors(name: keyof Fields, definition: FormFieldDefinition): string[] | null {
|
|
127
187
|
const errors = [];
|
|
188
|
+
const value = this._data[name];
|
|
189
|
+
const rules = definition.rules?.split('|') ?? [];
|
|
190
|
+
|
|
191
|
+
for (const rule of rules) {
|
|
192
|
+
if (rule !== 'required' && (value === null || value === undefined)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
128
195
|
|
|
129
|
-
|
|
130
|
-
errors.push('required');
|
|
196
|
+
errors.push(...validate(value, rule));
|
|
131
197
|
}
|
|
132
198
|
|
|
133
199
|
return errors.length > 0 ? errors : null;
|
package/src/forms/index.ts
CHANGED
package/src/forms/utils.ts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { FormFieldTypes } from './Form';
|
|
2
2
|
import type { FormFieldDefinition } from './Form';
|
|
3
3
|
|
|
4
|
-
export function booleanInput(
|
|
4
|
+
export function booleanInput(
|
|
5
|
+
defaultValue?: boolean,
|
|
6
|
+
options: { rules?: string } = {},
|
|
7
|
+
): FormFieldDefinition<typeof FormFieldTypes.Boolean> {
|
|
5
8
|
return {
|
|
6
9
|
default: defaultValue,
|
|
7
10
|
type: FormFieldTypes.Boolean,
|
|
11
|
+
rules: options.rules,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function dateInput(
|
|
16
|
+
defaultValue?: Date,
|
|
17
|
+
options: { rules?: string } = {},
|
|
18
|
+
): FormFieldDefinition<typeof FormFieldTypes.Date> {
|
|
19
|
+
return {
|
|
20
|
+
default: defaultValue,
|
|
21
|
+
type: FormFieldTypes.Date,
|
|
22
|
+
rules: options.rules,
|
|
8
23
|
};
|
|
9
24
|
}
|
|
10
25
|
|
|
@@ -18,6 +33,14 @@ export function requiredBooleanInput(
|
|
|
18
33
|
};
|
|
19
34
|
}
|
|
20
35
|
|
|
36
|
+
export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
|
|
37
|
+
return {
|
|
38
|
+
default: defaultValue,
|
|
39
|
+
type: FormFieldTypes.Date,
|
|
40
|
+
rules: 'required',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
export function requiredNumberInput(
|
|
22
45
|
defaultValue?: number,
|
|
23
46
|
): FormFieldDefinition<typeof FormFieldTypes.Number, 'required'> {
|
|
@@ -38,16 +61,24 @@ export function requiredStringInput(
|
|
|
38
61
|
};
|
|
39
62
|
}
|
|
40
63
|
|
|
41
|
-
export function numberInput(
|
|
64
|
+
export function numberInput(
|
|
65
|
+
defaultValue?: number,
|
|
66
|
+
options: { rules?: string } = {},
|
|
67
|
+
): FormFieldDefinition<typeof FormFieldTypes.Number> {
|
|
42
68
|
return {
|
|
43
69
|
default: defaultValue,
|
|
44
70
|
type: FormFieldTypes.Number,
|
|
71
|
+
rules: options.rules,
|
|
45
72
|
};
|
|
46
73
|
}
|
|
47
74
|
|
|
48
|
-
export function stringInput(
|
|
75
|
+
export function stringInput(
|
|
76
|
+
defaultValue?: string,
|
|
77
|
+
options: { rules?: string } = {},
|
|
78
|
+
): FormFieldDefinition<typeof FormFieldTypes.String> {
|
|
49
79
|
return {
|
|
50
80
|
default: defaultValue,
|
|
51
81
|
type: FormFieldTypes.String,
|
|
82
|
+
rules: options.rules,
|
|
52
83
|
};
|
|
53
84
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { arrayFrom } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
const builtInRules: Record<string, FormFieldValidator> = {
|
|
4
|
+
required: (value) => (value ? undefined : 'required'),
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type FormFieldValidator<T = unknown> = (value: T) => string | string[] | undefined;
|
|
8
|
+
|
|
9
|
+
export const validators: Record<string, FormFieldValidator> = { ...builtInRules };
|
|
10
|
+
|
|
11
|
+
export function defineFormValidationRule<T>(rule: string, validator: FormFieldValidator<T>): void {
|
|
12
|
+
validators[rule] = validator as FormFieldValidator;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function validate(value: unknown, rule: string): string[] {
|
|
16
|
+
const errors = validators[rule]?.(value);
|
|
17
|
+
|
|
18
|
+
return errors ? arrayFrom(errors) : [];
|
|
19
|
+
}
|
package/src/jobs/Job.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import App from '@/services/App';
|
|
2
|
+
|
|
3
|
+
import type { LangProvider } from './Lang';
|
|
4
|
+
|
|
5
|
+
export default class DefaultLangProvider implements LangProvider {
|
|
6
|
+
|
|
7
|
+
constructor(private locale: string, private fallbackLocale: string) {}
|
|
8
|
+
|
|
9
|
+
public getLocale(): string {
|
|
10
|
+
return this.locale;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public async setLocale(locale: string): Promise<void> {
|
|
14
|
+
this.locale = locale;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public getFallbackLocale(): string {
|
|
18
|
+
return this.fallbackLocale;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async setFallbackLocale(fallbackLocale: string): Promise<void> {
|
|
22
|
+
this.fallbackLocale = fallbackLocale;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public getLocales(): string[] {
|
|
26
|
+
return ['en'];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public translate(key: string): string {
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
App.development && console.warn('Lang provider is missing');
|
|
32
|
+
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public translateWithDefault(_: string, defaultMessage: string): string {
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
App.development && console.warn('Lang provider is missing');
|
|
39
|
+
|
|
40
|
+
return defaultMessage;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineServiceState } from '@/services/Service';
|
|
2
|
+
|
|
3
|
+
export default defineServiceState({
|
|
4
|
+
name: 'lang',
|
|
5
|
+
persist: ['locale', 'fallbackLocale'],
|
|
6
|
+
initialState: {
|
|
7
|
+
locale: null as string | null,
|
|
8
|
+
locales: ['en'],
|
|
9
|
+
fallbackLocale: 'en',
|
|
10
|
+
},
|
|
11
|
+
});
|