@aerogel/core 0.0.0-next.59bf5f7cc06e728d0cf6c00de28f1da48d7d6b8e → 0.0.0-next.7f6ed5a1f91688a86bf5ede2adc465e4fd6cfdea

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 (61) hide show
  1. package/dist/aerogel-core.cjs.js +1 -1
  2. package/dist/aerogel-core.d.ts +68 -414
  3. package/dist/aerogel-core.esm.js +1 -1
  4. package/package.json +8 -6
  5. package/src/bootstrap/bootstrap.test.ts +56 -0
  6. package/src/bootstrap/hooks.ts +19 -0
  7. package/src/bootstrap/index.ts +25 -9
  8. package/src/bootstrap/options.ts +1 -5
  9. package/src/components/basic/AGMarkdown.vue +5 -20
  10. package/src/components/forms/AGButton.vue +3 -26
  11. package/src/components/forms/AGInput.vue +4 -8
  12. package/src/components/forms/index.ts +1 -2
  13. package/src/components/headless/forms/AGHeadlessInput.ts +2 -2
  14. package/src/components/headless/forms/AGHeadlessInput.vue +5 -5
  15. package/src/components/headless/forms/AGHeadlessInputError.vue +5 -9
  16. package/src/components/headless/forms/AGHeadlessInputInput.vue +4 -20
  17. package/src/components/headless/forms/index.ts +4 -6
  18. package/src/components/headless/modals/AGHeadlessModal.vue +1 -5
  19. package/src/components/headless/modals/AGHeadlessModalPanel.vue +2 -10
  20. package/src/components/index.ts +1 -2
  21. package/src/components/modals/AGAlertModal.vue +2 -13
  22. package/src/components/modals/AGModal.ts +0 -4
  23. package/src/components/modals/AGModal.vue +2 -20
  24. package/src/components/modals/index.ts +1 -4
  25. package/src/directives/index.ts +3 -5
  26. package/src/forms/Form.test.ts +0 -21
  27. package/src/forms/Form.ts +16 -38
  28. package/src/forms/utils.ts +0 -17
  29. package/src/lang/Lang.ts +8 -47
  30. package/src/lang/helpers.ts +5 -0
  31. package/src/lang/index.ts +76 -17
  32. package/src/main.ts +0 -4
  33. package/src/models/index.ts +18 -0
  34. package/src/routing/index.ts +33 -0
  35. package/src/services/Service.ts +28 -151
  36. package/src/services/index.ts +7 -29
  37. package/src/testing/stubs/lang/en.yaml +1 -0
  38. package/src/testing/stubs/models/User.ts +3 -0
  39. package/src/types/vite.d.ts +2 -0
  40. package/src/ui/UI.state.ts +6 -3
  41. package/src/ui/UI.ts +2 -35
  42. package/src/ui/index.ts +13 -19
  43. package/src/utils/index.ts +0 -3
  44. package/tsconfig.json +10 -1
  45. package/vite.config.ts +6 -2
  46. package/src/components/forms/AGCheckbox.vue +0 -35
  47. package/src/components/headless/forms/AGHeadlessInputLabel.vue +0 -16
  48. package/src/components/modals/AGConfirmModal.vue +0 -30
  49. package/src/components/modals/AGLoadingModal.vue +0 -19
  50. package/src/errors/Errors.state.ts +0 -31
  51. package/src/errors/Errors.ts +0 -132
  52. package/src/errors/index.ts +0 -21
  53. package/src/globals.ts +0 -6
  54. package/src/lang/utils.ts +0 -4
  55. package/src/plugins/Plugin.ts +0 -7
  56. package/src/plugins/index.ts +0 -7
  57. package/src/services/App.state.ts +0 -13
  58. package/src/services/App.ts +0 -17
  59. package/src/services/store.ts +0 -27
  60. package/src/utils/composition/forms.ts +0 -11
  61. package/src/utils/composition/hooks.ts +0 -9
@@ -1,10 +1,5 @@
1
1
  <template>
2
- <AGHeadlessModal
3
- ref="$headlessModal"
4
- v-slot="{ close }: IAGHeadlessModalDefaultSlotProps"
5
- :cancellable="cancellable"
6
- class="relative z-50"
7
- >
2
+ <AGHeadlessModal v-slot="{ close }: IAGHeadlessModalDefaultSlotProps" class="relative z-50">
8
3
  <div class="fixed inset-0 flex items-center justify-center">
9
4
  <AGHeadlessModalPanel class="flex max-h-full max-w-full flex-col overflow-hidden bg-white">
10
5
  <div class="flex max-h-full flex-col overflow-auto p-4">
@@ -16,21 +11,8 @@
16
11
  </template>
17
12
 
18
13
  <script setup lang="ts">
19
- import { computed, ref } from 'vue';
20
-
21
- import { booleanProp } from '@/utils';
22
- import type { IAGHeadlessModal, IAGHeadlessModalDefaultSlotProps } from '@/components/headless/modals/AGHeadlessModal';
23
-
24
- import type { IAGModal } from './AGModal';
14
+ import type { IAGHeadlessModalDefaultSlotProps } from '@/components/headless/modals/AGHeadlessModal';
25
15
 
26
16
  import AGHeadlessModal from '../headless/modals/AGHeadlessModal.vue';
27
17
  import AGHeadlessModalPanel from '../headless/modals/AGHeadlessModalPanel.vue';
28
-
29
- const $headlessModal = ref<IAGHeadlessModal>();
30
-
31
- defineProps({ cancellable: booleanProp(true) });
32
- defineExpose<IAGModal>({
33
- close: async () => $headlessModal.value?.close(),
34
- cancellable: computed(() => !!$headlessModal.value?.cancellable),
35
- });
36
18
  </script>
@@ -1,8 +1,5 @@
1
- import AGAlertModal from './AGAlertModal.vue';
2
- import AGConfirmModal from './AGConfirmModal.vue';
3
- import AGLoadingModal from './AGLoadingModal.vue';
4
1
  import AGModal from './AGModal.vue';
5
2
  import AGModalContext from './AGModalContext.vue';
6
3
  import { IAGModal } from './AGModal';
7
4
 
8
- export { AGAlertModal, AGConfirmModal, AGModal, AGModalContext, AGLoadingModal, IAGModal };
5
+ export { AGModal, AGModalContext, IAGModal };
@@ -1,6 +1,6 @@
1
1
  import type { Directive } from 'vue';
2
2
 
3
- import { definePlugin } from '@/plugins';
3
+ import { defineBootstrapHook } from '@/bootstrap/hooks';
4
4
 
5
5
  import initialFocus from './initial-focus';
6
6
 
@@ -8,8 +8,6 @@ const directives: Record<string, Directive> = {
8
8
  'initial-focus': initialFocus,
9
9
  };
10
10
 
11
- export default definePlugin({
12
- install(app) {
13
- Object.entries(directives).forEach(([name, directive]) => app.directive(name, directive));
14
- },
11
+ export default defineBootstrapHook(async (app) => {
12
+ Object.entries(directives).forEach(([name, directive]) => app.directive(name, directive));
15
13
  });
@@ -34,25 +34,4 @@ 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
-
58
37
  });
package/src/forms/Form.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  import { MagicObject } from '@noeldemartin/utils';
2
- import { computed, reactive, readonly, ref } from 'vue';
2
+ import { reactive, readonly, ref } from 'vue';
3
3
  import type { ObjectValues } from '@noeldemartin/utils';
4
- import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
4
+ import type { DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
5
5
 
6
6
  export const FormFieldTypes = {
7
7
  String: 'string',
8
8
  Number: 'number',
9
- Boolean: 'boolean',
10
9
  } as const;
11
10
 
12
11
  export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
@@ -19,7 +18,7 @@ export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
19
18
  export type FormFieldType = ObjectValues<typeof FormFieldTypes>;
20
19
 
21
20
  export type FormData<T> = {
22
- -readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
21
+ [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
23
22
  ? TRules extends 'required'
24
23
  ? GetFormFieldValue<TType>
25
24
  : GetFormFieldValue<TType> | null
@@ -34,8 +33,6 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
34
33
  ? string
35
34
  : TType extends typeof FormFieldTypes.Number
36
35
  ? number
37
- : TType extends typeof FormFieldTypes.Boolean
38
- ? boolean
39
36
  : never;
40
37
 
41
38
  export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
@@ -44,7 +41,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
44
41
 
45
42
  private _fields: Fields;
46
43
  private _data: FormData<Fields>;
47
- private _valid: ComputedRef<boolean>;
44
+ private _valid: Ref<boolean>;
48
45
  private _submitted: Ref<boolean>;
49
46
  private _errors: FormErrors<Fields>;
50
47
 
@@ -53,9 +50,9 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
53
50
 
54
51
  this._fields = fields;
55
52
  this._submitted = ref(false);
53
+ this._valid = ref(true);
56
54
  this._data = this.getInitialData(fields);
57
55
  this._errors = this.getInitialErrors(fields);
58
- this._valid = computed(() => !Object.values(this._errors).some((error) => error !== null));
59
56
 
60
57
  this.errors = readonly(this._errors);
61
58
  }
@@ -81,22 +78,15 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
81
78
  }
82
79
 
83
80
  public validate(): boolean {
84
- const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
85
- formErrors[name] = this.getFieldErrors(name, definition);
81
+ const errors = Object.entries(this._fields).reduce((errors, [name, definition]) => {
82
+ errors[name] = this.getFieldErrors(name, definition);
86
83
 
87
- return formErrors;
84
+ return errors;
88
85
  }, {} as Record<string, string[] | null>);
89
86
 
90
- this.resetErrors(errors);
87
+ Object.assign(this._errors, errors);
91
88
 
92
- return this.valid;
93
- }
94
-
95
- public reset(): void {
96
- this._submitted.value = false;
97
-
98
- this.resetData();
99
- this.resetErrors();
89
+ return (this._valid.value = !Object.values(errors).some((error) => error !== null));
100
90
  }
101
91
 
102
92
  public submit(): boolean {
@@ -138,10 +128,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
138
128
  return {} as FormData<Fields>;
139
129
  }
140
130
 
141
- const data = Object.entries(fields).reduce((formData, [name, definition]) => {
142
- formData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
131
+ const data = Object.entries(fields).reduce((data, [name, definition]) => {
132
+ data[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
143
133
 
144
- return formData;
134
+ return data;
145
135
  }, {} as FormData<Fields>);
146
136
 
147
137
  return reactive(data) as FormData<Fields>;
@@ -152,25 +142,13 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
152
142
  return {} as FormErrors<Fields>;
153
143
  }
154
144
 
155
- const errors = Object.keys(fields).reduce((formErrors, name) => {
156
- formErrors[name as keyof Fields] = null;
145
+ const errors = Object.keys(fields).reduce((errors, name) => {
146
+ errors[name as keyof Fields] = null;
157
147
 
158
- return formErrors;
148
+ return errors;
159
149
  }, {} as FormErrors<Fields>);
160
150
 
161
151
  return reactive(errors) as FormErrors<Fields>;
162
152
  }
163
153
 
164
- private resetData(): void {
165
- for (const [name, field] of Object.entries(this._fields)) {
166
- this._data[name as keyof Fields] = (field.default ?? null) as FormData<Fields>[keyof Fields];
167
- }
168
- }
169
-
170
- private resetErrors(errors?: Record<string, string[] | null>): void {
171
- Object.keys(this._errors).forEach((key) => delete this._errors[key as keyof Fields]);
172
-
173
- errors && Object.assign(this._errors, errors);
174
- }
175
-
176
154
  }
@@ -1,23 +1,6 @@
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 requiredBooleanInput(
12
- defaultValue?: boolean,
13
- ): FormFieldDefinition<typeof FormFieldTypes.Boolean, 'required'> {
14
- return {
15
- default: defaultValue,
16
- type: FormFieldTypes.Boolean,
17
- rules: 'required',
18
- };
19
- }
20
-
21
4
  export function requiredNumberInput(
22
5
  defaultValue?: number,
23
6
  ): FormFieldDefinition<typeof FormFieldTypes.Number, 'required'> {
package/src/lang/Lang.ts CHANGED
@@ -1,58 +1,19 @@
1
- import { facade, toString } from '@noeldemartin/utils';
1
+ import { useI18n } from 'vue-i18n';
2
+ import { facade } from '@noeldemartin/utils';
3
+ import type { Composer } from 'vue-i18n';
2
4
 
3
- import App from '@/services/App';
4
5
  import Service from '@/services/Service';
5
6
 
6
- export interface LangProvider {
7
- translate(key: string, parameters?: Record<string, unknown>): string;
8
- }
9
-
10
7
  export class LangService extends Service {
11
8
 
12
- private provider: LangProvider;
13
-
14
- constructor() {
15
- super();
16
-
17
- this.provider = {
18
- translate: (key) => {
19
- // eslint-disable-next-line no-console
20
- App.isDevelopment && console.warn('Lang provider is missing');
21
-
22
- return key;
23
- },
24
- };
25
- }
9
+ private i18n?: Composer;
26
10
 
27
- public setProvider(provider: LangProvider): void {
28
- this.provider = provider;
11
+ public setup(): void {
12
+ this.i18n = useI18n();
29
13
  }
30
14
 
31
- public translate(key: string, parameters?: Record<string, unknown>): string {
32
- return this.provider.translate(key, parameters) ?? key;
33
- }
34
-
35
- public translateWithDefault(key: string, defaultMessage: string): string;
36
- public translateWithDefault(key: string, parameters: Record<string, unknown>, defaultMessage: string): string;
37
- public translateWithDefault(
38
- key: string,
39
- defaultMessageOrParameters?: string | Record<string, unknown>,
40
- defaultMessage?: string,
41
- ): 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
- if (message === key) {
48
- return Object.entries(parameters).reduce(
49
- (renderedMessage, [name, value]) =>
50
- renderedMessage.replace(new RegExp(`\\{\\s*${name}\\s*\\}`, 'g'), toString(value)),
51
- defaultMessage,
52
- );
53
- }
54
-
55
- return message;
15
+ public translate(key: string, parameters: Record<string, unknown> = {}): string {
16
+ return this.i18n?.t(key, parameters) ?? key;
56
17
  }
57
18
 
58
19
  }
@@ -0,0 +1,5 @@
1
+ import Lang from '@/lang/Lang';
2
+
3
+ export function lang(key: string, parameters: Record<string, unknown> = {}): string {
4
+ return Lang.translate(key, parameters);
5
+ }
package/src/lang/index.ts CHANGED
@@ -1,30 +1,89 @@
1
- import { bootServices } from '@/services';
2
- import { definePlugin } from '@/plugins';
1
+ import { createI18n } from 'vue-i18n';
2
+ import { fail, stringMatch } from '@noeldemartin/utils';
3
+ import type { I18nOptions } from 'vue-i18n';
4
+ import type { Plugin } from 'vue';
3
5
 
4
- import Lang, { LangProvider } from './Lang';
5
- import { translate, translateWithDefault } from './utils';
6
+ import { defineBootstrapHook, onAppMounted } from '@/bootstrap/hooks';
7
+ import { bootServices } from '@/services';
8
+ import type { BootstrapOptions } from '@/bootstrap/options';
6
9
 
7
- export { Lang, LangProvider, translate, translateWithDefault };
10
+ import Lang from './Lang';
8
11
 
9
12
  const services = { $lang: Lang };
10
13
 
14
+ function getLangOptions(options: BootstrapOptions): LangOptions | null {
15
+ if (options.lang) {
16
+ return options.lang;
17
+ }
18
+
19
+ if (options.langMessages) {
20
+ return { messages: options.langMessages };
21
+ }
22
+
23
+ return null;
24
+ }
25
+
26
+ function getMessageLoaders(messageLoaders: Record<string, unknown>): Record<string, LazyMessages> {
27
+ return Object.entries(messageLoaders).reduce((loaders, [fileName, loader]) => {
28
+ const locale = stringMatch<2>(fileName, /.*\/lang\/(.+)\.yaml/)?.[1];
29
+
30
+ if (locale) {
31
+ loaders[locale] = () =>
32
+ (loader as () => Promise<{ default: Record<string, unknown> }>)().then(
33
+ ({ default: messages }) => messages,
34
+ );
35
+ }
36
+
37
+ return loaders;
38
+ }, {} as Record<string, LazyMessages>);
39
+ }
40
+
41
+ async function createAppI18n(options: LangOptions): Promise<Plugin> {
42
+ const locale = options.defaultLocale ?? 'en';
43
+ const fallbackLocale = options.fallbackLocale ?? 'en';
44
+ const messageLoaders = getMessageLoaders(options.messages);
45
+ const lazyMessages = messageLoaders[locale] ?? fail<LazyMessages>(`Missing messages for '${locale}' locale`);
46
+ const messages = { [locale]: await lazyMessages() } as I18nOptions['messages'];
47
+
48
+ return createI18n({ locale, fallbackLocale, messages });
49
+ }
50
+
11
51
  export type LangServices = typeof services;
12
52
 
13
- export default definePlugin({
14
- async install(app) {
15
- app.config.globalProperties.$t ??= translate;
16
- app.config.globalProperties.$td = translateWithDefault;
53
+ export type LazyMessages = () => Promise<Record<string, unknown>>;
17
54
 
18
- await bootServices(app, services);
19
- },
55
+ export interface LangOptions {
56
+ messages: Record<string, unknown>;
57
+ defaultLocale?: string;
58
+ fallbackLocale?: string;
59
+ }
60
+
61
+ export * from './helpers';
62
+ export { Lang };
63
+
64
+ export default defineBootstrapHook(async (app, options) => {
65
+ const langOptions = getLangOptions(options);
66
+
67
+ if (!langOptions) {
68
+ return;
69
+ }
70
+
71
+ onAppMounted(() => Lang.setup());
72
+
73
+ const plugin = await createAppI18n(langOptions);
74
+
75
+ app.use(plugin);
76
+
77
+ await bootServices(app, services);
20
78
  });
21
79
 
22
- declare module '@/services' {
23
- interface Services extends LangServices {}
80
+ declare module '@/bootstrap/options' {
81
+ interface BootstrapOptions {
82
+ lang?: LangOptions;
83
+ langMessages?: Record<string, unknown>;
84
+ }
24
85
  }
25
86
 
26
- declare module '@vue/runtime-core' {
27
- interface ComponentCustomProperties {
28
- $td: typeof translateWithDefault;
29
- }
87
+ declare module '@/services' {
88
+ interface Services extends LangServices {}
30
89
  }
package/src/main.ts CHANGED
@@ -1,11 +1,7 @@
1
- import './globals';
2
-
3
1
  export * from './bootstrap';
4
2
  export * from './components';
5
- export * from './errors';
6
3
  export * from './forms';
7
4
  export * from './lang';
8
- export * from './plugins';
9
5
  export * from './services';
10
6
  export * from './ui';
11
7
  export * from './utils';
@@ -0,0 +1,18 @@
1
+ import { IndexedDBEngine, bootModelsFromViteGlob, setEngine } from 'soukai';
2
+
3
+ import { defineBootstrapHook } from '@/bootstrap/hooks';
4
+
5
+ export default defineBootstrapHook(async (_, options) => {
6
+ if (!options.models) {
7
+ return;
8
+ }
9
+
10
+ setEngine(new IndexedDBEngine());
11
+ bootModelsFromViteGlob(options.models);
12
+ });
13
+
14
+ declare module '@/bootstrap/options' {
15
+ interface BootstrapOptions {
16
+ models?: Record<string, Record<string, unknown>>;
17
+ }
18
+ }
@@ -0,0 +1,33 @@
1
+ import { createRouter, createWebHistory } from 'vue-router';
2
+ import type { Plugin } from 'vue';
3
+
4
+ import { defineBootstrapHook } from '@/bootstrap/hooks';
5
+
6
+ function createAppRouter(options: { routes: RouteRecordRaw[]; basePath?: string }): Plugin {
7
+ return createRouter({
8
+ history: createWebHistory(options.basePath),
9
+ routes: options.routes,
10
+ });
11
+ }
12
+
13
+ export default defineBootstrapHook(async (app, options) => {
14
+ if (!options.routes) {
15
+ return;
16
+ }
17
+
18
+ const plugin = createAppRouter({
19
+ routes: options.routes,
20
+ basePath: options.basePath ?? __AG_BASE_PATH,
21
+ });
22
+
23
+ app.use(plugin);
24
+ });
25
+
26
+ declare module '@/bootstrap/options' {
27
+ interface BootstrapOptions {
28
+ routes?: RouteRecordRaw[];
29
+ basePath?: string;
30
+ }
31
+ }
32
+
33
+ import type { RouteRecordRaw } from 'vue-router';