@aerogel/core 0.0.0-next.f16bd1d894543c5303039c49f6f33488a1ffe931 → 0.0.0-next.f86b4b09f066c4aef21796a37dbc8417b7dce3cd
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 +1396 -235
- 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 +35 -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 +24 -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/lib/AGErrorMessage.vue +16 -0
- package/src/components/lib/AGLink.vue +9 -0
- package/src/components/{basic → lib}/AGMarkdown.vue +12 -11
- 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 -16
- package/src/components/modals/AGConfirmModal.ts +35 -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 +6 -1
- package/src/components/modals/AGErrorReportModalTitle.vue +2 -2
- 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 +4 -10
- 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 +36 -12
- 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 +24 -6
- package/src/services/App.ts +43 -5
- 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 +64 -17
- package/src/services/index.ts +11 -5
- 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 +194 -27
- 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/composition/state.test.ts +47 -0
- package/src/utils/composition/state.ts +24 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/markdown.ts +11 -2
- 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 -3
- package/src/types/virtual.d.ts +0 -11
package/src/errors/Errors.ts
CHANGED
|
@@ -3,11 +3,14 @@ import { JSError, facade, isObject, objectWithoutEmpty, toString } from '@noelde
|
|
|
3
3
|
import App from '@/services/App';
|
|
4
4
|
import ServiceBootError from '@/errors/ServiceBootError';
|
|
5
5
|
import UI, { UIComponents } from '@/ui/UI';
|
|
6
|
-
import {
|
|
6
|
+
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
|
},
|
|
@@ -125,14 +137,20 @@ export class ErrorsService extends Service {
|
|
|
125
137
|
|
|
126
138
|
if (isObject(error)) {
|
|
127
139
|
return objectWithoutEmpty({
|
|
128
|
-
title: toString(
|
|
129
|
-
|
|
140
|
+
title: toString(
|
|
141
|
+
error['name'] ?? error['title'] ?? translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
142
|
+
),
|
|
143
|
+
description: toString(
|
|
144
|
+
error['message'] ??
|
|
145
|
+
error['description'] ??
|
|
146
|
+
translateWithDefault('errors.unknownDescription', 'Unknown error object'),
|
|
147
|
+
),
|
|
130
148
|
error,
|
|
131
149
|
});
|
|
132
150
|
}
|
|
133
151
|
|
|
134
152
|
return {
|
|
135
|
-
title:
|
|
153
|
+
title: translateWithDefault('errors.unknown', 'Unknown Error'),
|
|
136
154
|
error,
|
|
137
155
|
};
|
|
138
156
|
}
|
|
@@ -158,4 +176,10 @@ export class ErrorsService extends Service {
|
|
|
158
176
|
|
|
159
177
|
}
|
|
160
178
|
|
|
161
|
-
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
|
+
});
|