@aerogel/core 0.0.0-next.c8f032a868370824898e171969aec1bb6827688e → 0.0.0-next.d34923f3b144e8f6720e6a9cdadb2cd4fb4ab289

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.
Files changed (122) hide show
  1. package/dist/aerogel-core.cjs.js +1 -1
  2. package/dist/aerogel-core.cjs.js.map +1 -1
  3. package/dist/aerogel-core.d.ts +1720 -243
  4. package/dist/aerogel-core.esm.js +1 -1
  5. package/dist/aerogel-core.esm.js.map +1 -1
  6. package/histoire.config.ts +7 -0
  7. package/noeldemartin.config.js +4 -1
  8. package/package.json +14 -4
  9. package/postcss.config.js +6 -0
  10. package/src/assets/histoire.css +3 -0
  11. package/src/bootstrap/bootstrap.test.ts +4 -3
  12. package/src/bootstrap/index.ts +27 -4
  13. package/src/bootstrap/options.ts +3 -0
  14. package/src/components/AGAppLayout.vue +7 -2
  15. package/src/components/AGAppModals.vue +15 -0
  16. package/src/components/AGAppOverlays.vue +10 -8
  17. package/src/components/AGAppSnackbars.vue +13 -0
  18. package/src/components/constants.ts +8 -0
  19. package/src/components/forms/AGButton.vue +33 -10
  20. package/src/components/forms/AGCheckbox.vue +41 -0
  21. package/src/components/forms/AGForm.vue +9 -10
  22. package/src/components/forms/AGInput.vue +17 -9
  23. package/src/components/forms/AGSelect.story.vue +46 -0
  24. package/src/components/forms/AGSelect.vue +60 -0
  25. package/src/components/forms/index.ts +5 -5
  26. package/src/components/headless/forms/AGHeadlessButton.vue +21 -16
  27. package/src/components/headless/forms/AGHeadlessInput.ts +29 -4
  28. package/src/components/headless/forms/AGHeadlessInput.vue +17 -7
  29. package/src/components/headless/forms/AGHeadlessInputDescription.vue +28 -0
  30. package/src/components/headless/forms/AGHeadlessInputError.vue +1 -1
  31. package/src/components/headless/forms/AGHeadlessInputInput.vue +56 -6
  32. package/src/components/headless/forms/AGHeadlessInputLabel.vue +8 -2
  33. package/src/components/headless/forms/AGHeadlessInputTextArea.vue +42 -0
  34. package/src/components/headless/forms/AGHeadlessSelect.ts +42 -0
  35. package/src/components/headless/forms/AGHeadlessSelect.vue +77 -0
  36. package/src/components/headless/forms/AGHeadlessSelectButton.vue +24 -0
  37. package/src/components/headless/forms/AGHeadlessSelectError.vue +26 -0
  38. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +24 -0
  39. package/src/components/headless/forms/AGHeadlessSelectOption.ts +4 -0
  40. package/src/components/headless/forms/AGHeadlessSelectOption.vue +39 -0
  41. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +3 -0
  42. package/src/components/headless/forms/composition.ts +10 -0
  43. package/src/components/headless/forms/index.ts +12 -1
  44. package/src/components/headless/index.ts +1 -0
  45. package/src/components/headless/modals/AGHeadlessModal.ts +27 -0
  46. package/src/components/headless/modals/AGHeadlessModal.vue +3 -5
  47. package/src/components/headless/modals/AGHeadlessModalPanel.vue +5 -1
  48. package/src/components/headless/modals/index.ts +4 -6
  49. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +10 -0
  50. package/src/components/headless/snackbars/index.ts +40 -0
  51. package/src/components/index.ts +4 -1
  52. package/src/components/interfaces.ts +9 -0
  53. package/src/components/lib/AGErrorMessage.vue +16 -0
  54. package/src/components/lib/AGLink.vue +9 -0
  55. package/src/components/lib/AGMarkdown.vue +41 -0
  56. package/src/components/lib/AGMeasured.vue +15 -0
  57. package/src/components/lib/AGStartupCrash.vue +31 -0
  58. package/src/components/lib/index.ts +5 -0
  59. package/src/components/modals/AGAlertModal.ts +15 -0
  60. package/src/components/modals/AGAlertModal.vue +4 -16
  61. package/src/components/modals/AGConfirmModal.ts +33 -0
  62. package/src/components/modals/AGConfirmModal.vue +9 -13
  63. package/src/components/modals/AGErrorReportModal.ts +46 -0
  64. package/src/components/modals/AGErrorReportModal.vue +54 -0
  65. package/src/components/modals/AGErrorReportModalButtons.vue +111 -0
  66. package/src/components/modals/AGErrorReportModalTitle.vue +25 -0
  67. package/src/components/modals/AGLoadingModal.ts +23 -0
  68. package/src/components/modals/AGLoadingModal.vue +15 -0
  69. package/src/components/modals/AGModal.ts +1 -1
  70. package/src/components/modals/AGModal.vue +26 -5
  71. package/src/components/modals/AGModalTitle.vue +9 -0
  72. package/src/components/modals/AGPromptModal.ts +36 -0
  73. package/src/components/modals/AGPromptModal.vue +34 -0
  74. package/src/components/modals/index.ts +16 -6
  75. package/src/components/snackbars/AGSnackbar.vue +36 -0
  76. package/src/components/snackbars/index.ts +3 -0
  77. package/src/components/utils.ts +10 -0
  78. package/src/directives/index.ts +20 -3
  79. package/src/directives/measure.ts +21 -0
  80. package/src/errors/Errors.state.ts +31 -0
  81. package/src/errors/Errors.ts +185 -0
  82. package/src/errors/index.ts +46 -0
  83. package/src/errors/utils.ts +19 -0
  84. package/src/forms/Form.test.ts +21 -0
  85. package/src/forms/Form.ts +76 -18
  86. package/src/forms/index.ts +1 -0
  87. package/src/forms/utils.ts +32 -0
  88. package/src/jobs/Job.ts +5 -0
  89. package/src/jobs/index.ts +7 -0
  90. package/src/lang/Lang.ts +14 -14
  91. package/src/lang/index.ts +3 -5
  92. package/src/lang/utils.ts +4 -0
  93. package/src/main.histoire.ts +1 -0
  94. package/src/main.ts +4 -2
  95. package/src/plugins/Plugin.ts +1 -0
  96. package/src/plugins/index.ts +19 -0
  97. package/src/services/App.state.ts +23 -2
  98. package/src/services/App.ts +46 -3
  99. package/src/services/Cache.ts +43 -0
  100. package/src/services/Events.test.ts +39 -0
  101. package/src/services/Events.ts +100 -30
  102. package/src/services/Service.ts +174 -53
  103. package/src/services/index.ts +25 -5
  104. package/src/services/store.ts +30 -0
  105. package/src/testing/index.ts +25 -0
  106. package/src/ui/UI.state.ts +11 -1
  107. package/src/ui/UI.ts +177 -20
  108. package/src/ui/index.ts +15 -4
  109. package/src/utils/composition/events.ts +1 -0
  110. package/src/utils/composition/forms.ts +11 -0
  111. package/src/utils/index.ts +2 -0
  112. package/src/utils/markdown.ts +11 -2
  113. package/src/utils/tailwindcss.test.ts +26 -0
  114. package/src/utils/tailwindcss.ts +7 -0
  115. package/src/utils/vue.ts +15 -4
  116. package/tailwind.config.js +4 -0
  117. package/tsconfig.json +1 -0
  118. package/vite.config.ts +2 -1
  119. package/.eslintrc.js +0 -3
  120. package/src/components/basic/AGMarkdown.vue +0 -35
  121. package/src/components/basic/index.ts +0 -3
  122. package/src/globals.ts +0 -6
@@ -0,0 +1,10 @@
1
+ export function extractComponentProps<T extends Record<string, unknown>>(
2
+ values: Record<string, unknown>,
3
+ definitions: Record<string, unknown>,
4
+ ): T {
5
+ return Object.keys(definitions).reduce((extracted, prop) => {
6
+ extracted[prop] = values[prop];
7
+
8
+ return extracted;
9
+ }, {} as Record<string, unknown>) as T;
10
+ }
@@ -3,13 +3,30 @@ import type { Directive } from 'vue';
3
3
  import { definePlugin } from '@/plugins';
4
4
 
5
5
  import initialFocus from './initial-focus';
6
+ import measure from './measure';
6
7
 
7
- const directives: Record<string, Directive> = {
8
+ const builtInDirectives: Record<string, Directive> = {
8
9
  'initial-focus': initialFocus,
10
+ 'measure': measure,
9
11
  };
10
12
 
13
+ export * from './measure';
14
+
11
15
  export default definePlugin({
12
- install(app) {
13
- Object.entries(directives).forEach(([name, directive]) => app.directive(name, directive));
16
+ install(app, options) {
17
+ const directives = {
18
+ ...builtInDirectives,
19
+ ...options.directives,
20
+ };
21
+
22
+ for (const [name, directive] of Object.entries(directives)) {
23
+ app.directive(name, directive);
24
+ }
14
25
  },
15
26
  });
27
+
28
+ declare module '@/bootstrap/options' {
29
+ export interface AerogelOptions {
30
+ directives?: Record<string, Directive>;
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ import { defineDirective } from '@/utils/vue';
2
+
3
+ export interface ElementSize {
4
+ width: number;
5
+ height: number;
6
+ }
7
+
8
+ export type MeasureDirectiveListener = (size: ElementSize) => unknown;
9
+
10
+ export default defineDirective({
11
+ mounted(element: HTMLElement, { value }) {
12
+ const listener = typeof value === 'function' ? (value as MeasureDirectiveListener) : null;
13
+ const sizes = element.getBoundingClientRect();
14
+
15
+ // TODO guard with modifiers.css once typed properly
16
+ element.style.setProperty('--width', `${sizes.width}px`);
17
+ element.style.setProperty('--height', `${sizes.height}px`);
18
+
19
+ listener?.({ width: sizes.width, height: sizes.height });
20
+ },
21
+ });
@@ -0,0 +1,31 @@
1
+ import type { JSError } from '@noeldemartin/utils';
2
+
3
+ import { defineServiceState } from '@/services';
4
+
5
+ export type ErrorSource = string | Error | JSError | unknown;
6
+
7
+ export interface ErrorReport {
8
+ title: string;
9
+ description?: string;
10
+ details?: string;
11
+ error?: Error | JSError | unknown;
12
+ }
13
+
14
+ export interface ErrorReportLog {
15
+ report: ErrorReport;
16
+ seen: boolean;
17
+ date: Date;
18
+ }
19
+
20
+ export default defineServiceState({
21
+ name: 'errors',
22
+ initialState: {
23
+ logs: [] as ErrorReportLog[],
24
+ startupErrors: [] as ErrorReport[],
25
+ },
26
+ computed: {
27
+ hasErrors: ({ logs }) => logs.length > 0,
28
+ hasNewErrors: ({ logs }) => logs.some((error) => !error.seen),
29
+ hasStartupErrors: ({ startupErrors }) => startupErrors.length > 0,
30
+ },
31
+ });
@@ -0,0 +1,185 @@
1
+ import { JSError, facade, isObject, objectWithoutEmpty, toString } from '@noeldemartin/utils';
2
+
3
+ import App from '@/services/App';
4
+ import ServiceBootError from '@/errors/ServiceBootError';
5
+ import UI, { UIComponents } from '@/ui/UI';
6
+ import { translateWithDefault } from '@/lang/utils';
7
+
8
+ import Service from './Errors.state';
9
+ import { Colors } from '@/components/constants';
10
+ import { Events } from '@/services';
11
+ import type { AGErrorReportModalProps } from '@/components/modals/AGErrorReportModal';
12
+ import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
13
+ import type { ModalComponent } from '@/ui/UI.state';
14
+
15
+ export class ErrorsService extends Service {
16
+
17
+ public forceReporting: boolean = false;
18
+ private enabled: boolean = true;
19
+
20
+ public enable(): void {
21
+ this.enabled = true;
22
+ }
23
+
24
+ public disable(): void {
25
+ this.enabled = false;
26
+ }
27
+
28
+ public async inspect(error: ErrorSource | ErrorReport[]): Promise<void> {
29
+ const reports = Array.isArray(error) ? (error as ErrorReport[]) : [await this.createErrorReport(error)];
30
+
31
+ if (reports.length === 0) {
32
+ UI.alert(translateWithDefault('errors.inspectEmpty', 'Nothing to inspect!'));
33
+
34
+ return;
35
+ }
36
+
37
+ UI.openModal<ModalComponent<AGErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
38
+ reports,
39
+ });
40
+ }
41
+
42
+ public async report(error: ErrorSource, message?: string): Promise<void> {
43
+ await Events.emit('error', { error, message });
44
+
45
+ if (App.testing) {
46
+ throw error;
47
+ }
48
+
49
+ if (App.development) {
50
+ this.logError(error);
51
+ }
52
+
53
+ if (!this.enabled) {
54
+ throw error;
55
+ }
56
+
57
+ if (!App.isMounted()) {
58
+ const startupError = await this.createStartupErrorReport(error);
59
+
60
+ if (startupError) {
61
+ this.setState({ startupErrors: this.startupErrors.concat(startupError) });
62
+ }
63
+
64
+ return;
65
+ }
66
+
67
+ const report = await this.createErrorReport(error);
68
+ const log: ErrorReportLog = {
69
+ report,
70
+ seen: false,
71
+ date: new Date(),
72
+ };
73
+
74
+ UI.showSnackbar(
75
+ message ??
76
+ translateWithDefault('errors.notice', 'Something went wrong, but it\'s not your fault. Try again!'),
77
+ {
78
+ color: Colors.Danger,
79
+ actions: [
80
+ {
81
+ text: translateWithDefault('errors.viewDetails', 'View details'),
82
+ dismiss: true,
83
+ handler: () =>
84
+ UI.openModal<ModalComponent<AGErrorReportModalProps>>(
85
+ UI.requireComponent(UIComponents.ErrorReportModal),
86
+ { reports: [report] },
87
+ ),
88
+ },
89
+ ],
90
+ },
91
+ );
92
+
93
+ this.setState({ logs: [log].concat(this.logs) });
94
+ }
95
+
96
+ public see(report: ErrorReport): void {
97
+ this.setState({
98
+ logs: this.logs.map((log) => {
99
+ if (log.report !== report) {
100
+ return log;
101
+ }
102
+
103
+ return {
104
+ ...log,
105
+ seen: true,
106
+ };
107
+ }),
108
+ });
109
+ }
110
+
111
+ public seeAll(): void {
112
+ this.setState({
113
+ logs: this.logs.map((log) => ({
114
+ ...log,
115
+ seen: true,
116
+ })),
117
+ });
118
+ }
119
+
120
+ private logError(error: unknown): void {
121
+ // eslint-disable-next-line no-console
122
+ console.error(error);
123
+
124
+ if (isObject(error) && error.cause) {
125
+ this.logError(error.cause);
126
+ }
127
+ }
128
+
129
+ private async createErrorReport(error: ErrorSource): Promise<ErrorReport> {
130
+ if (typeof error === 'string') {
131
+ return { title: error };
132
+ }
133
+
134
+ if (error instanceof Error || error instanceof JSError) {
135
+ return this.createErrorReportFromError(error);
136
+ }
137
+
138
+ if (isObject(error)) {
139
+ return objectWithoutEmpty({
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
+ ),
148
+ error,
149
+ });
150
+ }
151
+
152
+ return {
153
+ title: translateWithDefault('errors.unknown', 'Unknown Error'),
154
+ error,
155
+ };
156
+ }
157
+
158
+ private async createStartupErrorReport(error: ErrorSource): Promise<ErrorReport | null> {
159
+ if (error instanceof ServiceBootError) {
160
+ // Ignore second-order boot errors in order to have a cleaner startup crash screen.
161
+ return error.cause instanceof ServiceBootError ? null : this.createErrorReport(error.cause);
162
+ }
163
+
164
+ return this.createErrorReport(error);
165
+ }
166
+
167
+ private createErrorReportFromError(error: Error | JSError, defaults: Partial<ErrorReport> = {}): ErrorReport {
168
+ return {
169
+ title: error.name,
170
+ description: error.message,
171
+ details: error.stack,
172
+ error,
173
+ ...defaults,
174
+ };
175
+ }
176
+
177
+ }
178
+
179
+ export default facade(ErrorsService);
180
+
181
+ declare module '@/services/Events' {
182
+ export interface EventsPayload {
183
+ error: { error: ErrorSource; message?: string };
184
+ }
185
+ }
@@ -0,0 +1,46 @@
1
+ import type { App } from 'vue';
2
+
3
+ import { bootServices } from '@/services';
4
+ import { definePlugin } from '@/plugins';
5
+
6
+ import Errors from './Errors';
7
+ import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
8
+
9
+ export * from './utils';
10
+ export { Errors, ErrorSource, ErrorReport, ErrorReportLog };
11
+
12
+ const services = { $errors: Errors };
13
+ const frameworkHandler: ErrorHandler = (error) => {
14
+ Errors.report(error);
15
+
16
+ return true;
17
+ };
18
+
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);
25
+ }
26
+
27
+ export type ErrorHandler = (error: ErrorSource) => boolean;
28
+ export type ErrorsServices = typeof services;
29
+
30
+ export default definePlugin({
31
+ async install(app, options) {
32
+ setUpErrorHandler(app, options.handleError);
33
+
34
+ await bootServices(app, services);
35
+ },
36
+ });
37
+
38
+ declare module '@/bootstrap/options' {
39
+ export interface AerogelOptions {
40
+ handleError?(error: ErrorSource): boolean;
41
+ }
42
+ }
43
+
44
+ declare module '@/services' {
45
+ export interface Services extends ErrorsServices {}
46
+ }
@@ -0,0 +1,19 @@
1
+ import { JSError, isObject, toString } from '@noeldemartin/utils';
2
+ import { translateWithDefault } from '@/lang/utils';
3
+ import type { ErrorSource } from './Errors.state';
4
+
5
+ export function getErrorMessage(error: ErrorSource): string {
6
+ if (typeof error === 'string') {
7
+ return error;
8
+ }
9
+
10
+ if (error instanceof Error || error instanceof JSError) {
11
+ return error.message;
12
+ }
13
+
14
+ if (isObject(error)) {
15
+ return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
16
+ }
17
+
18
+ return translateWithDefault('errors.unknown', 'Unknown Error');
19
+ }
@@ -34,4 +34,25 @@ describe('Form', () => {
34
34
  expect(form.errors.name).toEqual(['required']);
35
35
  });
36
36
 
37
+ it('resets form', () => {
38
+ // Arrange
39
+ const form = useForm({
40
+ name: {
41
+ type: FormFieldTypes.String,
42
+ rules: 'required',
43
+ },
44
+ });
45
+
46
+ form.name = 'Foo bar';
47
+ form.submit();
48
+
49
+ // Act
50
+ form.reset();
51
+
52
+ // Assert
53
+ expect(form.valid).toBe(true);
54
+ expect(form.submitted).toBe(false);
55
+ expect(form.name).toBeNull();
56
+ });
57
+
37
58
  });
package/src/forms/Form.ts CHANGED
@@ -1,11 +1,14 @@
1
- import { MagicObject } from '@noeldemartin/utils';
2
- import { computed, reactive, readonly, ref } from 'vue';
1
+ import { MagicObject, arrayRemove } from '@noeldemartin/utils';
2
+ import { computed, nextTick, reactive, readonly, ref } from 'vue';
3
3
  import type { ObjectValues } from '@noeldemartin/utils';
4
4
  import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
5
5
 
6
6
  export const FormFieldTypes = {
7
7
  String: 'string',
8
8
  Number: 'number',
9
+ Boolean: 'boolean',
10
+ Object: 'object',
11
+ Date: 'date',
9
12
  } as const;
10
13
 
11
14
  export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
@@ -16,9 +19,10 @@ export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType
16
19
 
17
20
  export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
18
21
  export type FormFieldType = ObjectValues<typeof FormFieldTypes>;
22
+ export type FormFieldValue = GetFormFieldValue<FormFieldType>;
19
23
 
20
24
  export type FormData<T> = {
21
- [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
25
+ -readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
22
26
  ? TRules extends 'required'
23
27
  ? GetFormFieldValue<TType>
24
28
  : GetFormFieldValue<TType> | null
@@ -33,17 +37,28 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
33
37
  ? string
34
38
  : TType extends typeof FormFieldTypes.Number
35
39
  ? number
40
+ : TType extends typeof FormFieldTypes.Boolean
41
+ ? boolean
42
+ : TType extends typeof FormFieldTypes.Object
43
+ ? object
44
+ : TType extends typeof FormFieldTypes.Date
45
+ ? Date
36
46
  : never;
37
47
 
48
+ const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
49
+
50
+ export type SubmitFormListener = () => unknown;
51
+ export type FocusFormListener = (input: string) => unknown;
52
+
38
53
  export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
39
54
 
40
55
  public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
41
56
 
42
57
  private _fields: Fields;
43
58
  private _data: FormData<Fields>;
44
- private _valid: ComputedRef<boolean>;
45
59
  private _submitted: Ref<boolean>;
46
60
  private _errors: FormErrors<Fields>;
61
+ private _listeners: { focus?: FocusFormListener[]; submit?: SubmitFormListener[] } = {};
47
62
 
48
63
  constructor(fields: Fields) {
49
64
  super();
@@ -52,13 +67,17 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
52
67
  this._submitted = ref(false);
53
68
  this._data = this.getInitialData(fields);
54
69
  this._errors = this.getInitialErrors(fields);
55
- this._valid = computed(() => !Object.values(this._errors).some((error) => error !== null));
70
+
71
+ validForms.set(
72
+ this,
73
+ computed(() => !Object.values(this._errors).some((error) => error !== null)),
74
+ );
56
75
 
57
76
  this.errors = readonly(this._errors);
58
77
  }
59
78
 
60
79
  public get valid(): boolean {
61
- return this._valid.value;
80
+ return !!validForms.get(this)?.value;
62
81
  }
63
82
 
64
83
  public get submitted(): boolean {
@@ -77,11 +96,15 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
77
96
  return this._data[field] as unknown as GetFormFieldValue<Fields[T]['type']>;
78
97
  }
79
98
 
99
+ public data(): FormData<Fields> {
100
+ return { ...this._data };
101
+ }
102
+
80
103
  public validate(): boolean {
81
- const errors = Object.entries(this._fields).reduce((errors, [name, definition]) => {
82
- errors[name] = this.getFieldErrors(name, definition);
104
+ const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
105
+ formErrors[name] = this.getFieldErrors(name, definition);
83
106
 
84
- return errors;
107
+ return formErrors;
85
108
  }, {} as Record<string, string[] | null>);
86
109
 
87
110
  this.resetErrors(errors);
@@ -89,16 +112,45 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
89
112
  return this.valid;
90
113
  }
91
114
 
92
- public reset(): void {
115
+ public reset(options: { keepData?: boolean; keepErrors?: boolean } = {}): void {
93
116
  this._submitted.value = false;
94
117
 
95
- this.resetErrors();
118
+ options.keepData || this.resetData();
119
+ options.keepErrors || this.resetErrors();
96
120
  }
97
121
 
98
122
  public submit(): boolean {
99
123
  this._submitted.value = true;
100
124
 
101
- return this.validate();
125
+ const valid = this.validate();
126
+
127
+ valid && this._listeners['submit']?.forEach((listener) => listener());
128
+
129
+ return valid;
130
+ }
131
+
132
+ public on(event: 'focus', listener: FocusFormListener): () => void;
133
+ public on(event: 'submit', listener: SubmitFormListener): () => void;
134
+ public on(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): () => void {
135
+ this._listeners[event] ??= [];
136
+
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ this._listeners[event]?.push(listener as any);
139
+
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ return () => this.off(event as any, listener);
142
+ }
143
+
144
+ public off(event: 'focus', listener: FocusFormListener): void;
145
+ public off(event: 'submit', listener: SubmitFormListener): void;
146
+ public off(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): void {
147
+ arrayRemove(this._listeners[event] ?? [], listener);
148
+ }
149
+
150
+ public async focus(input: string): Promise<void> {
151
+ await nextTick();
152
+
153
+ this._listeners['focus']?.forEach((listener) => listener(input));
102
154
  }
103
155
 
104
156
  protected __get(property: string): unknown {
@@ -134,10 +186,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
134
186
  return {} as FormData<Fields>;
135
187
  }
136
188
 
137
- const data = Object.entries(fields).reduce((data, [name, definition]) => {
138
- data[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
189
+ const data = Object.entries(fields).reduce((formData, [name, definition]) => {
190
+ formData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
139
191
 
140
- return data;
192
+ return formData;
141
193
  }, {} as FormData<Fields>);
142
194
 
143
195
  return reactive(data) as FormData<Fields>;
@@ -148,15 +200,21 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
148
200
  return {} as FormErrors<Fields>;
149
201
  }
150
202
 
151
- const errors = Object.keys(fields).reduce((errors, name) => {
152
- errors[name as keyof Fields] = null;
203
+ const errors = Object.keys(fields).reduce((formErrors, name) => {
204
+ formErrors[name as keyof Fields] = null;
153
205
 
154
- return errors;
206
+ return formErrors;
155
207
  }, {} as FormErrors<Fields>);
156
208
 
157
209
  return reactive(errors) as FormErrors<Fields>;
158
210
  }
159
211
 
212
+ private resetData(): void {
213
+ for (const [name, field] of Object.entries(this._fields)) {
214
+ this._data[name as keyof Fields] = (field.default ?? null) as FormData<Fields>[keyof Fields];
215
+ }
216
+ }
217
+
160
218
  private resetErrors(errors?: Record<string, string[] | null>): void {
161
219
  Object.keys(this._errors).forEach((key) => delete this._errors[key as keyof Fields]);
162
220
 
@@ -1,3 +1,4 @@
1
1
  export * from './Form';
2
2
  export * from './composition';
3
3
  export * from './utils';
4
+ export { default as Form } from './Form';
@@ -1,6 +1,38 @@
1
1
  import { FormFieldTypes } from './Form';
2
2
  import type { FormFieldDefinition } from './Form';
3
3
 
4
+ export function booleanInput(defaultValue?: boolean): FormFieldDefinition<typeof FormFieldTypes.Boolean> {
5
+ return {
6
+ default: defaultValue,
7
+ type: FormFieldTypes.Boolean,
8
+ };
9
+ }
10
+
11
+ export function dateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
12
+ return {
13
+ default: defaultValue,
14
+ type: FormFieldTypes.Date,
15
+ };
16
+ }
17
+
18
+ export function requiredBooleanInput(
19
+ defaultValue?: boolean,
20
+ ): FormFieldDefinition<typeof FormFieldTypes.Boolean, 'required'> {
21
+ return {
22
+ default: defaultValue,
23
+ type: FormFieldTypes.Boolean,
24
+ rules: 'required',
25
+ };
26
+ }
27
+
28
+ export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
29
+ return {
30
+ default: defaultValue,
31
+ type: FormFieldTypes.Date,
32
+ rules: 'required',
33
+ };
34
+ }
35
+
4
36
  export function requiredNumberInput(
5
37
  defaultValue?: number,
6
38
  ): FormFieldDefinition<typeof FormFieldTypes.Number, 'required'> {
@@ -0,0 +1,5 @@
1
+ export default abstract class Job {
2
+
3
+ public abstract run(): Promise<void>;
4
+
5
+ }
@@ -0,0 +1,7 @@
1
+ import Job from './Job';
2
+
3
+ export { Job };
4
+
5
+ export async function dispatch(job: Job): Promise<void> {
6
+ await job.run();
7
+ }
package/src/lang/Lang.ts CHANGED
@@ -4,7 +4,8 @@ import App from '@/services/App';
4
4
  import Service from '@/services/Service';
5
5
 
6
6
  export interface LangProvider {
7
- translate(key: string, parameters?: Record<string, unknown>): string;
7
+ translate(key: string, parameters?: Record<string, unknown> | number): string;
8
+ translateWithDefault(key: string, defaultMessage: string, parameters?: Record<string, unknown> | number): string;
8
9
  }
9
10
 
10
11
  export class LangService extends Service {
@@ -17,10 +18,16 @@ export class LangService extends Service {
17
18
  this.provider = {
18
19
  translate: (key) => {
19
20
  // eslint-disable-next-line no-console
20
- App.isDevelopment && console.warn('Lang provider is missing');
21
+ App.development && console.warn('Lang provider is missing');
21
22
 
22
23
  return key;
23
24
  },
25
+ translateWithDefault: (_, defaultMessage) => {
26
+ // eslint-disable-next-line no-console
27
+ App.development && console.warn('Lang provider is missing');
28
+
29
+ return defaultMessage;
30
+ },
24
31
  };
25
32
  }
26
33
 
@@ -28,25 +35,18 @@ export class LangService extends Service {
28
35
  this.provider = provider;
29
36
  }
30
37
 
31
- public translate(key: string, parameters?: Record<string, unknown>): string {
38
+ public translate(key: string, parameters?: Record<string, unknown> | number): string {
32
39
  return this.provider.translate(key, parameters) ?? key;
33
40
  }
34
41
 
35
- public translateWithDefault(key: string, defaultMessage: string): string;
36
- public translateWithDefault(key: string, parameters: Record<string, unknown>, defaultMessage: string): string;
37
42
  public translateWithDefault(
38
43
  key: string,
39
- defaultMessageOrParameters?: string | Record<string, unknown>,
40
- defaultMessage?: string,
44
+ defaultMessage: string,
45
+ parameters: Record<string, unknown> | number = {},
41
46
  ): string {
42
- defaultMessage ??= defaultMessageOrParameters as string;
43
-
44
- const parameters = typeof defaultMessageOrParameters === 'string' ? {} : defaultMessageOrParameters;
45
- const message = this.provider.translate(key, parameters) ?? key;
46
-
47
- return message === key ? defaultMessage : message;
47
+ return this.provider.translateWithDefault(key, defaultMessage, parameters);
48
48
  }
49
49
 
50
50
  }
51
51
 
52
- export default facade(new LangService());
52
+ export default facade(LangService);