@aerogel/core 0.0.0-next.bde642c4a8096c5fc3d5e676c2115da23f4bf1d8 → 0.0.0-next.c2e6acc000e97a1020c2e232678563c53884dd0e

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 (166) hide show
  1. package/dist/aerogel-core.d.ts +1396 -1645
  2. package/dist/aerogel-core.js +2968 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +27 -37
  5. package/src/bootstrap/bootstrap.test.ts +4 -7
  6. package/src/bootstrap/index.ts +25 -16
  7. package/src/bootstrap/options.ts +1 -1
  8. package/src/components/{AGAppLayout.vue → AppLayout.vue} +4 -4
  9. package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
  10. package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -5
  11. package/src/components/composition.ts +1 -1
  12. package/src/components/contracts/AlertModal.ts +4 -0
  13. package/src/components/contracts/Button.ts +15 -0
  14. package/src/components/contracts/ConfirmModal.ts +41 -0
  15. package/src/components/contracts/ErrorReportModal.ts +29 -0
  16. package/src/components/contracts/Input.ts +26 -0
  17. package/src/components/contracts/LoadingModal.ts +18 -0
  18. package/src/components/contracts/Modal.ts +9 -0
  19. package/src/components/contracts/PromptModal.ts +28 -0
  20. package/src/components/contracts/index.ts +7 -0
  21. package/src/components/contracts/shared.ts +9 -0
  22. package/src/components/forms/AGSelect.vue +11 -17
  23. package/src/components/forms/index.ts +0 -4
  24. package/src/components/headless/HeadlessButton.vue +45 -0
  25. package/src/components/headless/HeadlessInput.vue +59 -0
  26. package/src/components/headless/{forms/AGHeadlessInputDescription.vue → HeadlessInputDescription.vue} +6 -7
  27. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
  28. package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +11 -19
  29. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  30. package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +7 -9
  31. package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +16 -18
  32. package/src/components/headless/HeadlessModalContent.vue +24 -0
  33. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  34. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  35. package/src/components/headless/forms/AGHeadlessSelect.ts +3 -3
  36. package/src/components/headless/forms/AGHeadlessSelect.vue +16 -16
  37. package/src/components/headless/forms/AGHeadlessSelectError.vue +2 -2
  38. package/src/components/headless/forms/AGHeadlessSelectOption.vue +10 -18
  39. package/src/components/headless/forms/AGHeadlessSelectOptions.vue +19 -0
  40. package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +25 -0
  41. package/src/components/headless/forms/composition.ts +2 -2
  42. package/src/components/headless/forms/index.ts +2 -12
  43. package/src/components/headless/index.ts +12 -1
  44. package/src/components/headless/snackbars/index.ts +3 -3
  45. package/src/components/index.ts +5 -5
  46. package/src/components/lib/AGErrorMessage.vue +5 -5
  47. package/src/components/lib/AGMeasured.vue +3 -2
  48. package/src/components/lib/AGStartupCrash.vue +8 -8
  49. package/src/components/lib/index.ts +0 -2
  50. package/src/components/snackbars/AGSnackbar.vue +10 -8
  51. package/src/components/ui/AlertModal.vue +13 -0
  52. package/src/components/ui/Button.vue +58 -0
  53. package/src/components/ui/Checkbox.vue +49 -0
  54. package/src/components/ui/ConfirmModal.vue +42 -0
  55. package/src/components/ui/ErrorReportModal.vue +62 -0
  56. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +29 -20
  57. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  58. package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
  59. package/src/components/ui/Input.vue +52 -0
  60. package/src/components/ui/Link.vue +12 -0
  61. package/src/components/ui/LoadingModal.vue +32 -0
  62. package/src/components/ui/Markdown.vue +62 -0
  63. package/src/components/ui/Modal.vue +55 -0
  64. package/src/components/ui/ModalContext.vue +30 -0
  65. package/src/components/ui/ProgressBar.vue +50 -0
  66. package/src/components/ui/PromptModal.vue +35 -0
  67. package/src/components/ui/index.ts +16 -0
  68. package/src/components/utils.ts +106 -9
  69. package/src/directives/index.ts +9 -5
  70. package/src/directives/measure.ts +25 -6
  71. package/src/errors/Errors.state.ts +1 -1
  72. package/src/errors/Errors.ts +14 -14
  73. package/src/errors/JobCancelledError.ts +3 -0
  74. package/src/errors/index.ts +9 -6
  75. package/src/errors/utils.ts +17 -1
  76. package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
  77. package/src/forms/{Form.ts → FormController.ts} +46 -25
  78. package/src/forms/composition.ts +4 -4
  79. package/src/forms/index.ts +3 -2
  80. package/src/forms/utils.ts +22 -6
  81. package/src/forms/validation.ts +19 -0
  82. package/src/index.css +8 -0
  83. package/src/jobs/Job.ts +144 -2
  84. package/src/jobs/index.ts +4 -1
  85. package/src/jobs/listeners.ts +3 -0
  86. package/src/jobs/status.ts +4 -0
  87. package/src/lang/DefaultLangProvider.ts +7 -4
  88. package/src/lang/Lang.state.ts +1 -1
  89. package/src/lang/Lang.ts +5 -1
  90. package/src/lang/index.ts +8 -6
  91. package/src/plugins/Plugin.ts +1 -1
  92. package/src/plugins/index.ts +10 -7
  93. package/src/services/App.state.ts +13 -4
  94. package/src/services/App.ts +8 -3
  95. package/src/services/Cache.ts +1 -1
  96. package/src/services/Events.ts +15 -5
  97. package/src/services/Service.ts +116 -53
  98. package/src/services/Storage.ts +20 -0
  99. package/src/services/index.ts +11 -5
  100. package/src/services/utils.ts +18 -0
  101. package/src/testing/index.ts +4 -3
  102. package/src/testing/setup.ts +5 -13
  103. package/src/ui/UI.state.ts +17 -5
  104. package/src/ui/UI.ts +168 -62
  105. package/src/ui/index.ts +17 -16
  106. package/src/ui/utils.ts +16 -0
  107. package/src/utils/composition/events.ts +2 -2
  108. package/src/utils/composition/forms.ts +4 -3
  109. package/src/utils/composition/persistent.test.ts +33 -0
  110. package/src/utils/composition/persistent.ts +11 -0
  111. package/src/utils/composition/state.test.ts +47 -0
  112. package/src/utils/composition/state.ts +24 -0
  113. package/src/utils/index.ts +2 -0
  114. package/src/utils/markdown.test.ts +50 -0
  115. package/src/utils/markdown.ts +19 -6
  116. package/src/utils/vdom.ts +31 -0
  117. package/src/utils/vue.ts +18 -13
  118. package/dist/aerogel-core.cjs.js +0 -2
  119. package/dist/aerogel-core.cjs.js.map +0 -1
  120. package/dist/aerogel-core.esm.js +0 -2
  121. package/dist/aerogel-core.esm.js.map +0 -1
  122. package/histoire.config.ts +0 -7
  123. package/noeldemartin.config.js +0 -5
  124. package/postcss.config.js +0 -6
  125. package/src/assets/histoire.css +0 -3
  126. package/src/components/forms/AGButton.vue +0 -44
  127. package/src/components/forms/AGCheckbox.vue +0 -41
  128. package/src/components/forms/AGInput.vue +0 -40
  129. package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
  130. package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
  131. package/src/components/headless/forms/AGHeadlessInput.ts +0 -33
  132. package/src/components/headless/forms/AGHeadlessInput.vue +0 -63
  133. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  134. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  135. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  136. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  137. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  138. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  139. package/src/components/headless/modals/index.ts +0 -4
  140. package/src/components/interfaces.ts +0 -24
  141. package/src/components/lib/AGLink.vue +0 -9
  142. package/src/components/lib/AGMarkdown.vue +0 -41
  143. package/src/components/modals/AGAlertModal.ts +0 -15
  144. package/src/components/modals/AGAlertModal.vue +0 -14
  145. package/src/components/modals/AGConfirmModal.ts +0 -33
  146. package/src/components/modals/AGConfirmModal.vue +0 -26
  147. package/src/components/modals/AGErrorReportModal.ts +0 -46
  148. package/src/components/modals/AGErrorReportModal.vue +0 -54
  149. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  150. package/src/components/modals/AGLoadingModal.ts +0 -23
  151. package/src/components/modals/AGLoadingModal.vue +0 -15
  152. package/src/components/modals/AGModal.ts +0 -10
  153. package/src/components/modals/AGModal.vue +0 -39
  154. package/src/components/modals/AGModalContext.ts +0 -8
  155. package/src/components/modals/AGModalContext.vue +0 -22
  156. package/src/components/modals/AGModalTitle.vue +0 -9
  157. package/src/components/modals/AGPromptModal.ts +0 -36
  158. package/src/components/modals/AGPromptModal.vue +0 -34
  159. package/src/components/modals/index.ts +0 -17
  160. package/src/directives/initial-focus.ts +0 -11
  161. package/src/main.histoire.ts +0 -1
  162. package/tailwind.config.js +0 -4
  163. package/tsconfig.json +0 -11
  164. package/vite.config.ts +0 -17
  165. /package/src/components/{AGAppSnackbars.vue → AppSnackbars.vue} +0 -0
  166. /package/src/{main.ts → index.ts} +0 -0
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <div class="mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-200">
3
+ <div :class="filledClasses" :style="`transform:translateX(-${(1 - renderedProgress) * 100}%)`" />
4
+ <span class="sr-only">
5
+ {{
6
+ $td('ui.progress', '{progress}% complete', {
7
+ progress: renderedProgress * 100,
8
+ })
9
+ }}
10
+ </span>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { computed, onUnmounted, ref, watch } from 'vue';
16
+
17
+ import { classes } from '@aerogel/core/components/utils';
18
+ import type Job from '@aerogel/core/jobs/Job';
19
+
20
+ const { filledClass, progress, job } = defineProps<{
21
+ filledClass?: string;
22
+ progress?: number;
23
+ job?: Job;
24
+ }>();
25
+
26
+ let cleanup: Function | undefined;
27
+ const jobProgress = ref(0);
28
+ const filledClasses = computed(() =>
29
+ classes('size-full transition-transform duration-500 rounded-r-full ease-linear bg-primary', filledClass));
30
+ const renderedProgress = computed(() => {
31
+ if (typeof progress === 'number') {
32
+ return progress;
33
+ }
34
+
35
+ return jobProgress.value;
36
+ });
37
+
38
+ watch(
39
+ () => job,
40
+ () => {
41
+ cleanup?.();
42
+
43
+ jobProgress.value = job?.progress ?? 0;
44
+ cleanup = job?.listeners.add({ onUpdated: (value) => (jobProgress.value = value) });
45
+ },
46
+ { immediate: true },
47
+ );
48
+
49
+ onUnmounted(() => cleanup?.());
50
+ </script>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <Modal v-slot="{ close }" :title="title" persistent>
3
+ <Form :form="form" @submit="close(form.draft)">
4
+ <Markdown :text="message" />
5
+ <Input
6
+ name="draft"
7
+ class="mt-2"
8
+ :placeholder="placeholder"
9
+ :label="label"
10
+ />
11
+
12
+ <div class="mt-4 flex flex-row-reverse gap-2">
13
+ <Button :variant="acceptVariant" submit>
14
+ {{ renderedAcceptText }}
15
+ </Button>
16
+ <Button :variant="cancelVariant" @click="close(false)">
17
+ {{ renderedCancelText }}
18
+ </Button>
19
+ </div>
20
+ </Form>
21
+ </Modal>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import Markdown from '@aerogel/core/components/ui/Markdown.vue';
26
+ import Button from '@aerogel/core/components/ui/Button.vue';
27
+ import Form from '@aerogel/core/components/ui/Form.vue';
28
+ import Input from '@aerogel/core/components/ui/Input.vue';
29
+ import Modal from '@aerogel/core/components/ui/Modal.vue';
30
+ import { usePromptModal } from '@aerogel/core/components/contracts/PromptModal';
31
+ import type { PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
32
+
33
+ const { cancelVariant = 'secondary', ...props } = defineProps<PromptModalProps>();
34
+ const { form, renderedAcceptText, renderedCancelText } = usePromptModal(props);
35
+ </script>
@@ -0,0 +1,16 @@
1
+ export { default as AlertModal } from './AlertModal.vue';
2
+ export { default as Button } from './Button.vue';
3
+ export { default as Checkbox } from './Checkbox.vue';
4
+ export { default as ConfirmModal } from './ConfirmModal.vue';
5
+ export { default as ErrorReportModal } from './ErrorReportModal.vue';
6
+ export { default as ErrorReportModalButtons } from './ErrorReportModalButtons.vue';
7
+ export { default as ErrorReportModalTitle } from './ErrorReportModalTitle.vue';
8
+ export { default as Form } from './Form.vue';
9
+ export { default as Input } from './Input.vue';
10
+ export { default as Link } from './Link.vue';
11
+ export { default as LoadingModal } from './LoadingModal.vue';
12
+ export { default as Markdown } from './Markdown.vue';
13
+ export { default as Modal } from './Modal.vue';
14
+ export { default as ModalContext } from './ModalContext.vue';
15
+ export { default as ProgressBar } from './ProgressBar.vue';
16
+ export { default as PromptModal } from './PromptModal.vue';
@@ -1,10 +1,107 @@
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;
1
+ import clsx from 'clsx';
2
+ import { computed, customRef, inject, onUnmounted, unref } from 'vue';
3
+ import { cva } from 'class-variance-authority';
4
+ import { isObject } from '@noeldemartin/utils';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import type { ClassValue } from 'clsx';
7
+ import type { ComputedRef, ExtractPropTypes, PropType, Ref, UnwrapNestedRefs } from 'vue';
8
+ import type { GetClosureArgs, GetClosureResult, Nullable } from '@noeldemartin/utils';
9
+
10
+ import type { HasElement } from '@aerogel/core/components/contracts/shared';
11
+ import type { FormController } from '@aerogel/core/forms';
12
+
13
+ export type CVAConfig<T> = NonNullable<GetClosureArgs<typeof cva<T>>[1]>;
14
+ export type CVAProps<T> = NonNullable<GetClosureArgs<GetClosureResult<typeof cva<T>>>[0]>;
15
+ export type RefsObject<T> = { [K in keyof T]: Ref<T[K]> | T[K] };
16
+ export type Variants<T extends Record<string, string>> = Required<{
17
+ [K in keyof T]: {
18
+ [key in T[K]]: string;
19
+ };
20
+ }>;
21
+
22
+ export type ComponentPropDefinitions<T> = {
23
+ [K in keyof T]: {
24
+ type?: PropType<T[K]>;
25
+ default: T[K] | (() => T[K]) | null;
26
+ };
27
+ };
28
+
29
+ export type PickComponentProps<TValues, TDefinitions> = {
30
+ [K in keyof TValues]: K extends keyof TDefinitions ? TValues[K] : never;
31
+ };
32
+
33
+ export function computedVariantClasses<T>(
34
+ value: RefsObject<{ baseClasses?: string } & CVAProps<T>>,
35
+ config: { baseClasses?: string } & CVAConfig<T>,
36
+ ): ComputedRef<string> {
37
+ return computed(() => {
38
+ const { baseClasses: valueBaseClasses, ...valueRefs } = value;
39
+ const { baseClasses: configBaseClasses, ...configs } = config;
40
+ const variants = cva(configBaseClasses, configs as CVAConfig<T>);
41
+ const values = Object.entries(valueRefs).reduce((extractedValues, [name, valueRef]) => {
42
+ extractedValues[name as keyof CVAProps<T>] = unref(valueRef);
43
+
44
+ return extractedValues;
45
+ }, {} as CVAProps<T>);
46
+
47
+ return classes(variants(values), unref(valueBaseClasses));
48
+ });
49
+ }
50
+
51
+ export function classes(...inputs: ClassValue[]): string {
52
+ return twMerge(clsx(inputs));
53
+ }
54
+
55
+ export function elementRef(): Ref<HTMLElement | undefined> {
56
+ return customRef((track, trigger) => {
57
+ let value: HTMLElement | undefined = undefined;
58
+
59
+ return {
60
+ get() {
61
+ track();
62
+
63
+ return value;
64
+ },
65
+ set(newValue) {
66
+ value = getElement(newValue);
67
+
68
+ trigger();
69
+ },
70
+ };
71
+ });
72
+ }
73
+
74
+ export function extractComponentProps<TDefinitions extends {}, TValues extends ExtractPropTypes<TDefinitions>>(
75
+ values: TValues,
76
+ definitions: TDefinitions,
77
+ ): PickComponentProps<TValues, TDefinitions> {
78
+ return Object.keys(definitions).reduce(
79
+ (extracted, prop) => {
80
+ extracted[prop] = values[prop as keyof TValues];
81
+
82
+ return extracted;
83
+ },
84
+ {} as Record<string, unknown>,
85
+ ) as PickComponentProps<TValues, TDefinitions>;
86
+ }
87
+
88
+ export function getElement(value: unknown): HTMLElement | undefined {
89
+ if (value instanceof HTMLElement) {
90
+ return value;
91
+ }
92
+
93
+ if (hasElement(value)) {
94
+ return value.$el;
95
+ }
96
+ }
97
+
98
+ export function hasElement(value: unknown): value is UnwrapNestedRefs<HasElement> {
99
+ return isObject(value) && '$el' in value;
100
+ }
101
+
102
+ export function onFormFocus(input: { name: Nullable<string> }, listener: () => unknown): void {
103
+ const form = inject<FormController | null>('form', null);
104
+ const stop = form?.on('focus', (name) => input.name === name && listener());
105
+
106
+ onUnmounted(() => stop?.());
10
107
  }
@@ -1,13 +1,11 @@
1
1
  import type { Directive } from 'vue';
2
2
 
3
- import { definePlugin } from '@/plugins';
3
+ import { definePlugin } from '@aerogel/core/plugins';
4
4
 
5
- import initialFocus from './initial-focus';
6
5
  import measure from './measure';
7
6
 
8
7
  const builtInDirectives: Record<string, Directive> = {
9
- 'initial-focus': initialFocus,
10
- 'measure': measure,
8
+ measure: measure,
11
9
  };
12
10
 
13
11
  export * from './measure';
@@ -25,8 +23,14 @@ export default definePlugin({
25
23
  },
26
24
  });
27
25
 
28
- declare module '@/bootstrap/options' {
26
+ declare module '@aerogel/core/bootstrap/options' {
29
27
  export interface AerogelOptions {
30
28
  directives?: Record<string, Directive>;
31
29
  }
32
30
  }
31
+
32
+ declare module 'vue' {
33
+ interface ComponentCustomDirectives {
34
+ measure: Directive<string, string>;
35
+ }
36
+ }
@@ -1,4 +1,7 @@
1
- import { defineDirective } from '@/utils/vue';
1
+ import { defineDirective } from '@aerogel/core/utils/vue';
2
+ import { tap } from '@noeldemartin/utils';
3
+
4
+ const resizeObservers: WeakMap<HTMLElement, ResizeObserver> = new WeakMap();
2
5
 
3
6
  export interface ElementSize {
4
7
  width: number;
@@ -9,13 +12,29 @@ export type MeasureDirectiveListener = (size: ElementSize) => unknown;
9
12
 
10
13
  export default defineDirective({
11
14
  mounted(element: HTMLElement, { value }) {
15
+ // TODO replace with argument when typed properly
16
+ const modifiers = { css: true, watch: true };
17
+
12
18
  const listener = typeof value === 'function' ? (value as MeasureDirectiveListener) : null;
13
- const sizes = element.getBoundingClientRect();
19
+ const update = () => {
20
+ const sizes = element.getBoundingClientRect();
21
+
22
+ if (modifiers.css) {
23
+ element.style.setProperty('--width', `${sizes.width}px`);
24
+ element.style.setProperty('--height', `${sizes.height}px`);
25
+ }
14
26
 
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`);
27
+ listener?.({ width: sizes.width, height: sizes.height });
28
+ };
18
29
 
19
- listener?.({ width: sizes.width, height: sizes.height });
30
+ if (modifiers.watch) {
31
+ resizeObservers.set(element, tap(new ResizeObserver(update)).observe(element));
32
+ }
33
+
34
+ update();
35
+ },
36
+ unmounted(element) {
37
+ resizeObservers.get(element)?.unobserve(element);
38
+ resizeObservers.delete(element);
20
39
  },
21
40
  });
@@ -1,6 +1,6 @@
1
1
  import type { JSError } from '@noeldemartin/utils';
2
2
 
3
- import { defineServiceState } from '@/services';
3
+ import { defineServiceState } from '@aerogel/core/services';
4
4
 
5
5
  export type ErrorSource = string | Error | JSError | unknown;
6
6
 
@@ -1,16 +1,16 @@
1
- import { JSError, facade, isObject, objectWithoutEmpty, toString } from '@noeldemartin/utils';
1
+ import { JSError, facade, isDevelopment, isObject, isTesting, objectWithoutEmpty, toString } from '@noeldemartin/utils';
2
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';
3
+ import App from '@aerogel/core/services/App';
4
+ import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
5
+ import UI, { UIComponents } from '@aerogel/core/ui/UI';
6
+ import { translateWithDefault } from '@aerogel/core/lang/utils';
7
+ import { Colors } from '@aerogel/core/components/constants';
8
+ import { Events } from '@aerogel/core/services';
9
+ import type { ErrorReportModalProps } from '@aerogel/core/components/contracts/ErrorReportModal';
10
+ import type { ModalComponent } from '@aerogel/core/ui/UI.state';
7
11
 
8
12
  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
13
  import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
13
- import type { ModalComponent } from '@/ui/UI.state';
14
14
 
15
15
  export class ErrorsService extends Service {
16
16
 
@@ -34,7 +34,7 @@ export class ErrorsService extends Service {
34
34
  return;
35
35
  }
36
36
 
37
- UI.openModal<ModalComponent<AGErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
37
+ UI.openModal<ModalComponent<ErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
38
38
  reports,
39
39
  });
40
40
  }
@@ -42,11 +42,11 @@ export class ErrorsService extends Service {
42
42
  public async report(error: ErrorSource, message?: string): Promise<void> {
43
43
  await Events.emit('error', { error, message });
44
44
 
45
- if (App.testing) {
45
+ if (isTesting('unit')) {
46
46
  throw error;
47
47
  }
48
48
 
49
- if (App.development) {
49
+ if (isDevelopment()) {
50
50
  this.logError(error);
51
51
  }
52
52
 
@@ -81,7 +81,7 @@ export class ErrorsService extends Service {
81
81
  text: translateWithDefault('errors.viewDetails', 'View details'),
82
82
  dismiss: true,
83
83
  handler: () =>
84
- UI.openModal<ModalComponent<AGErrorReportModalProps>>(
84
+ UI.openModal<ModalComponent<ErrorReportModalProps>>(
85
85
  UI.requireComponent(UIComponents.ErrorReportModal),
86
86
  { reports: [report] },
87
87
  ),
@@ -178,7 +178,7 @@ export class ErrorsService extends Service {
178
178
 
179
179
  export default facade(ErrorsService);
180
180
 
181
- declare module '@/services/Events' {
181
+ declare module '@aerogel/core/services/Events' {
182
182
  export interface EventsPayload {
183
183
  error: { error: ErrorSource; message?: string };
184
184
  }
@@ -0,0 +1,3 @@
1
+ import { JSError } from '@noeldemartin/utils';
2
+
3
+ export default class JobCancelledError extends JSError {}
@@ -1,13 +1,16 @@
1
1
  import type { App } from 'vue';
2
2
 
3
- import { bootServices } from '@/services';
4
- import { definePlugin } from '@/plugins';
3
+ import { bootServices } from '@aerogel/core/services';
4
+ import { definePlugin } from '@aerogel/core/plugins';
5
5
 
6
6
  import Errors from './Errors';
7
- import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
7
+ import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
8
8
 
9
9
  export * from './utils';
10
- export { Errors, ErrorSource, ErrorReport, ErrorReportLog };
10
+ export { Errors };
11
+ export { default as JobCancelledError } from './JobCancelledError';
12
+ export { default as ServiceBootError } from './ServiceBootError';
13
+ export type { ErrorSource, ErrorReport, ErrorReportLog };
11
14
 
12
15
  const services = { $errors: Errors };
13
16
  const frameworkHandler: ErrorHandler = (error) => {
@@ -35,12 +38,12 @@ export default definePlugin({
35
38
  },
36
39
  });
37
40
 
38
- declare module '@/bootstrap/options' {
41
+ declare module '@aerogel/core/bootstrap/options' {
39
42
  export interface AerogelOptions {
40
43
  handleError?(error: ErrorSource): boolean;
41
44
  }
42
45
  }
43
46
 
44
- declare module '@/services' {
47
+ declare module '@aerogel/core/services' {
45
48
  export interface Services extends ErrorsServices {}
46
49
  }
@@ -1,8 +1,24 @@
1
1
  import { JSError, isObject, toString } from '@noeldemartin/utils';
2
- import { translateWithDefault } from '@/lang/utils';
2
+ import { translateWithDefault } from '@aerogel/core/lang/utils';
3
3
  import type { ErrorSource } from './Errors.state';
4
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
+
5
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
+
6
22
  if (typeof error === 'string') {
7
23
  return error;
8
24
  }
@@ -1,10 +1,11 @@
1
1
  import { describe, expect, expectTypeOf, it } from 'vitest';
2
2
 
3
- import { useForm } from './composition';
4
- import { FormFieldTypes } from '@/main';
5
- import { numberInput, requiredStringInput } from '@/forms/utils';
3
+ import { useForm } from '@aerogel/core/forms/composition';
4
+ import { numberInput, requiredStringInput } from '@aerogel/core/forms/utils';
6
5
 
7
- describe('Form', () => {
6
+ import { FormFieldTypes } from './FormController';
7
+
8
+ describe('FormController', () => {
8
9
 
9
10
  it('defines magic fields', () => {
10
11
  const form = useForm({
@@ -55,4 +56,32 @@ describe('Form', () => {
55
56
  expect(form.name).toBeNull();
56
57
  });
57
58
 
59
+ it('trims values', () => {
60
+ // Arrange
61
+ const form = useForm({
62
+ trimmed: {
63
+ type: FormFieldTypes.String,
64
+ rules: 'required',
65
+ },
66
+ untrimmed: {
67
+ type: FormFieldTypes.String,
68
+ rules: 'required',
69
+ trim: false,
70
+ },
71
+ });
72
+
73
+ // Act
74
+ form.trimmed = ' ';
75
+ form.untrimmed = ' ';
76
+
77
+ form.submit();
78
+
79
+ // Assert
80
+ expect(form.valid).toBe(false);
81
+ expect(form.submitted).toBe(true);
82
+ expect(form.trimmed).toEqual('');
83
+ expect(form.untrimmed).toEqual(' ');
84
+ expect(form.errors).toEqual({ trimmed: ['required'], untrimmed: null });
85
+ });
86
+
58
87
  });
@@ -1,5 +1,6 @@
1
- import { MagicObject, arrayRemove } from '@noeldemartin/utils';
2
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
 
@@ -13,6 +14,7 @@ export const FormFieldTypes = {
13
14
 
14
15
  export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
15
16
  type: TType;
17
+ trim?: boolean;
16
18
  default?: GetFormFieldValue<TType>;
17
19
  rules?: TRules;
18
20
  }
@@ -36,21 +38,21 @@ export type FormErrors<T> = {
36
38
  export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.String
37
39
  ? string
38
40
  : TType extends typeof FormFieldTypes.Number
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
46
- : never;
41
+ ? number
42
+ : TType extends typeof FormFieldTypes.Boolean
43
+ ? boolean
44
+ : TType extends typeof FormFieldTypes.Object
45
+ ? object
46
+ : TType extends typeof FormFieldTypes.Date
47
+ ? Date
48
+ : never;
47
49
 
48
- const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
50
+ const validForms: WeakMap<FormController, ComputedRef<boolean>> = new WeakMap();
49
51
 
50
52
  export type SubmitFormListener = () => unknown;
51
53
  export type FocusFormListener = (input: string) => unknown;
52
54
 
53
- export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
55
+ export default class FormController<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
54
56
 
55
57
  public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
56
58
 
@@ -85,7 +87,13 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
85
87
  }
86
88
 
87
89
  public setFieldValue<T extends keyof Fields>(field: T, value: FormData<Fields>[T]): void {
88
- this._data[field] = value;
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;
89
97
 
90
98
  if (this._submitted.value) {
91
99
  this.validate();
@@ -96,16 +104,23 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
96
104
  return this._data[field] as unknown as GetFormFieldValue<Fields[T]['type']>;
97
105
  }
98
106
 
107
+ public getFieldRules<T extends keyof Fields>(field: T): string[] {
108
+ return this._fields[field]?.rules?.split('|') ?? [];
109
+ }
110
+
99
111
  public data(): FormData<Fields> {
100
112
  return { ...this._data };
101
113
  }
102
114
 
103
115
  public validate(): boolean {
104
- const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
105
- formErrors[name] = this.getFieldErrors(name, definition);
116
+ const errors = Object.entries(this._fields).reduce(
117
+ (formErrors, [name, definition]) => {
118
+ formErrors[name] = this.getFieldErrors(name, definition);
106
119
 
107
- return formErrors;
108
- }, {} as Record<string, string[] | null>);
120
+ return formErrors;
121
+ },
122
+ {} as Record<string, string[] | null>,
123
+ );
109
124
 
110
125
  this.resetErrors(errors);
111
126
 
@@ -153,29 +168,35 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
153
168
  this._listeners['focus']?.forEach((listener) => listener(input));
154
169
  }
155
170
 
156
- protected __get(property: string): unknown {
171
+ protected override __get(property: string): unknown {
157
172
  if (!(property in this._fields)) {
158
173
  return super.__get(property);
159
174
  }
160
175
 
161
- return this._data[property];
176
+ return this.getFieldValue(property);
162
177
  }
163
178
 
164
- protected __set(property: string, value: unknown): void {
179
+ protected override __set(property: string, value: unknown): void {
165
180
  if (!(property in this._fields)) {
166
181
  super.__set(property, value);
167
182
 
168
183
  return;
169
184
  }
170
185
 
171
- Object.assign(this._data, { [property]: value });
186
+ this.setFieldValue(property, value as FormData<Fields>[string]);
172
187
  }
173
188
 
174
189
  private getFieldErrors(name: keyof Fields, definition: FormFieldDefinition): string[] | null {
175
190
  const errors = [];
191
+ const value = this._data[name];
192
+ const rules = definition.rules?.split('|') ?? [];
193
+
194
+ for (const rule of rules) {
195
+ if (rule !== 'required' && (value === null || value === undefined)) {
196
+ continue;
197
+ }
176
198
 
177
- if (definition.rules?.includes('required') && !this._data[name]) {
178
- errors.push('required');
199
+ errors.push(...validate(value, rule));
179
200
  }
180
201
 
181
202
  return errors.length > 0 ? errors : null;
@@ -186,10 +207,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
186
207
  return {} as FormData<Fields>;
187
208
  }
188
209
 
189
- const data = Object.entries(fields).reduce((formData, [name, definition]) => {
190
- formData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
210
+ const data = Object.entries(fields).reduce((initialData, [name, definition]) => {
211
+ initialData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
191
212
 
192
- return formData;
213
+ return initialData;
193
214
  }, {} as FormData<Fields>);
194
215
 
195
216
  return reactive(data) as FormData<Fields>;
@@ -1,6 +1,6 @@
1
- import Form from '@/forms/Form';
2
- import type { FormData, FormFieldDefinitions } from '@/forms/Form';
1
+ import FormController from '@aerogel/core/forms/FormController';
2
+ import type { FormData, FormFieldDefinitions } from '@aerogel/core/forms/FormController';
3
3
 
4
- export function useForm<const T extends FormFieldDefinitions>(fields: T): Form<T> & FormData<T> {
5
- return new Form(fields) as Form<T> & FormData<T>;
4
+ export function useForm<const T extends FormFieldDefinitions>(fields: T): FormController<T> & FormData<T> {
5
+ return new FormController(fields) as FormController<T> & FormData<T>;
6
6
  }
@@ -1,4 +1,5 @@
1
- export * from './Form';
2
1
  export * from './composition';
2
+ export * from './FormController';
3
3
  export * from './utils';
4
- export { default as Form } from './Form';
4
+ export * from './validation';
5
+ export { default as FormController } from './FormController';