@byyuurin/ui 0.0.9 → 0.0.10

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 (91) hide show
  1. package/README.md +0 -3
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +1 -1
  4. package/dist/runtime/app/injections.d.ts +9299 -3
  5. package/dist/runtime/app/injections.js +35 -0
  6. package/dist/runtime/components/Accordion.vue +16 -20
  7. package/dist/runtime/components/Alert.vue +1 -1
  8. package/dist/runtime/components/Badge.vue +1 -1
  9. package/dist/runtime/components/Breadcrumb.vue +17 -21
  10. package/dist/runtime/components/Calendar.vue +15 -6
  11. package/dist/runtime/components/Carousel.vue +5 -3
  12. package/dist/runtime/components/Checkbox.vue +12 -7
  13. package/dist/runtime/components/Drawer.vue +12 -12
  14. package/dist/runtime/components/DropdownMenu.vue +143 -0
  15. package/dist/runtime/components/DropdownMenuContent.vue +188 -0
  16. package/dist/runtime/components/Form.vue +311 -0
  17. package/dist/runtime/components/FormItem.vue +129 -0
  18. package/dist/runtime/components/Input.vue +27 -13
  19. package/dist/runtime/components/InputNumber.vue +22 -14
  20. package/dist/runtime/components/Link.vue +17 -2
  21. package/dist/runtime/components/Modal.vue +11 -11
  22. package/dist/runtime/components/PinInput.vue +22 -13
  23. package/dist/runtime/components/Popover.vue +3 -3
  24. package/dist/runtime/components/RadioGroup.vue +50 -46
  25. package/dist/runtime/components/Select.vue +90 -80
  26. package/dist/runtime/components/Slider.vue +12 -7
  27. package/dist/runtime/components/Switch.vue +12 -6
  28. package/dist/runtime/components/Table.vue +21 -8
  29. package/dist/runtime/components/Tabs.vue +12 -11
  30. package/dist/runtime/components/Textarea.vue +19 -13
  31. package/dist/runtime/components/Toast.vue +6 -3
  32. package/dist/runtime/components/Tooltip.vue +3 -3
  33. package/dist/runtime/composables/useFormItem.d.ts +27 -0
  34. package/dist/runtime/composables/useFormItem.js +64 -0
  35. package/dist/runtime/composables/useTheme.js +2 -1
  36. package/dist/runtime/index.d.ts +3 -0
  37. package/dist/runtime/index.js +3 -0
  38. package/dist/runtime/theme/app.d.ts +1 -0
  39. package/dist/runtime/theme/app.js +2 -1
  40. package/dist/runtime/theme/badge.d.ts +21 -45
  41. package/dist/runtime/theme/breadcrumb.d.ts +3 -3
  42. package/dist/runtime/theme/button.d.ts +111 -57
  43. package/dist/runtime/theme/calendar.d.ts +2 -2
  44. package/dist/runtime/theme/chip.d.ts +11 -44
  45. package/dist/runtime/theme/drawer.d.ts +68 -33
  46. package/dist/runtime/theme/dropdown-menu.d.ts +71 -0
  47. package/dist/runtime/theme/dropdown-menu.js +83 -0
  48. package/dist/runtime/theme/form-item.d.ts +76 -0
  49. package/dist/runtime/theme/form-item.js +34 -0
  50. package/dist/runtime/theme/form.d.ts +8 -0
  51. package/dist/runtime/theme/form.js +7 -0
  52. package/dist/runtime/theme/index.d.ts +3 -0
  53. package/dist/runtime/theme/index.js +3 -0
  54. package/dist/runtime/theme/input-number.d.ts +41 -61
  55. package/dist/runtime/theme/input.d.ts +99 -71
  56. package/dist/runtime/theme/input.js +2 -2
  57. package/dist/runtime/theme/modal.d.ts +5 -33
  58. package/dist/runtime/theme/pinInput.d.ts +42 -42
  59. package/dist/runtime/theme/pinInput.js +1 -1
  60. package/dist/runtime/theme/progress.d.ts +117 -53
  61. package/dist/runtime/theme/select.d.ts +100 -84
  62. package/dist/runtime/theme/select.js +2 -1
  63. package/dist/runtime/theme/separator.d.ts +13 -28
  64. package/dist/runtime/theme/table.d.ts +3 -0
  65. package/dist/runtime/theme/table.js +2 -1
  66. package/dist/runtime/theme/tabs.d.ts +51 -68
  67. package/dist/runtime/theme/textarea.d.ts +37 -43
  68. package/dist/runtime/theme/textarea.js +1 -1
  69. package/dist/runtime/theme/toast-provider.d.ts +26 -41
  70. package/dist/runtime/types/components.d.ts +3 -0
  71. package/dist/runtime/types/form.d.ts +45 -0
  72. package/dist/runtime/types/form.js +0 -0
  73. package/dist/runtime/types/index.d.ts +5 -2
  74. package/dist/runtime/types/index.js +1 -0
  75. package/dist/runtime/types/utils.d.ts +32 -11
  76. package/dist/runtime/utils/extend-theme.js +15 -4
  77. package/dist/runtime/utils/form.d.ts +5 -0
  78. package/dist/runtime/utils/form.js +24 -0
  79. package/dist/runtime/utils/index.d.ts +2 -0
  80. package/dist/runtime/utils/index.js +4 -0
  81. package/dist/runtime/utils/link.d.ts +4 -26
  82. package/dist/runtime/utils/link.js +10 -3
  83. package/dist/shared/ui.3e7fad19.mjs +5 -0
  84. package/dist/shared/ui.3e7fad19.mjs.map +1 -0
  85. package/dist/unocss.mjs +2 -2
  86. package/dist/unocss.mjs.map +1 -1
  87. package/dist/unplugin.mjs +1 -1
  88. package/dist/vite.mjs +1 -1
  89. package/package.json +16 -14
  90. package/dist/shared/ui.1a1f119c.mjs +0 -5
  91. package/dist/shared/ui.1a1f119c.mjs.map +0 -1
@@ -0,0 +1,188 @@
1
+ <script lang="ts">
2
+ import type { VariantProps } from '@byyuurin/ui-kit'
3
+ import type { DropdownMenuContentEmits as RekaDropdownMenuContentEmits, DropdownMenuContentProps as RekaDropdownMenuContentProps } from 'reka-ui'
4
+ import type { dropdownMenu } from '../theme'
5
+ import type { ArrayOrNested, ComponentAttrs, DropdownMenuItem, DropdownMenuSlots, NestedItem } from '../types'
6
+
7
+ type ExtractItem<T extends ArrayOrNested<any>> = Extract<NestedItem<T>, { slot: string }>
8
+
9
+ export type DropdownMenuContentSlots<T extends ArrayOrNested<DropdownMenuItem>> = Omit<DropdownMenuSlots<T>, 'default'> & {
10
+ default?: any
11
+ }
12
+
13
+ export interface DropdownMenuContentEmits extends RekaDropdownMenuContentEmits {}
14
+
15
+ type DropdownMenuVariants = VariantProps<typeof dropdownMenu>
16
+
17
+ export interface DropdownMenuContentProps<T extends ArrayOrNested<DropdownMenuItem>> extends ComponentAttrs<typeof dropdownMenu>, Omit<RekaDropdownMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
18
+ size?: DropdownMenuVariants['size']
19
+ items?: T
20
+ portal?: boolean
21
+ sub?: boolean
22
+ labelKey: keyof NestedItem<T>
23
+ checkedIcon?: string
24
+ loadingIcon?: string
25
+ externalIcon?: boolean | string
26
+ }
27
+ </script>
28
+
29
+ <script setup lang="ts" generic="T extends ArrayOrNested<DropdownMenuItem>">
30
+ import { createReusableTemplate, reactiveOmit } from '@vueuse/core'
31
+ import { useForwardPropsEmits } from 'reka-ui'
32
+ import { DropdownMenu } from 'reka-ui/namespaced'
33
+ import { computed } from 'vue'
34
+ import { useTheme } from '../composables/useTheme'
35
+ import { get, isArrayOfArray, omit } from '../utils'
36
+ import { pickLinkProps } from '../utils/link'
37
+ import Avatar from './Avatar.vue'
38
+ // eslint-disable-next-line import/no-self-import
39
+ import DropdownMenuContent from './DropdownMenuContent.vue'
40
+ import Kbd from './Kbd.vue'
41
+ import Link from './Link.vue'
42
+ import LinkBase from './LinkBase.vue'
43
+
44
+ const props = defineProps<DropdownMenuContentProps<T>>()
45
+ const emit = defineEmits<DropdownMenuContentEmits>()
46
+ const slots = defineSlots<DropdownMenuContentSlots<T>>()
47
+
48
+ const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui'), emit)
49
+ // @ts-expect-error pass check
50
+ const proxySlots = omit(slots, ['default']) as Record<string, DropdownMenuContentSlots<T>[string]>
51
+
52
+ const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: DropdownMenuItem, active?: boolean, index: number }>()
53
+
54
+ const groups = computed(
55
+ () => props.items?.length
56
+ ? isArrayOfArray(props.items) ? props.items : [props.items]
57
+ : [],
58
+ )
59
+
60
+ const { theme, generateStyle } = useTheme()
61
+ const style = computed(() => generateStyle('dropdownMenu', props))
62
+ </script>
63
+
64
+ <template>
65
+ <DefineItemTemplate v-slot="{ item, active, index }">
66
+ <slot :name="((item.slot || 'item') as keyof DropdownMenuContentSlots<T>)" :item="(item as ExtractItem<T>)" :index="index">
67
+ <slot :name="(`${item.slot || 'item'}-leading` as keyof DropdownMenuContentSlots<T>)" :item="(item as ExtractItem<T>)" :active="active" :index="index">
68
+ <span
69
+ v-if="item.loading"
70
+ :class="style.itemLeadingIcon({ class: [loadingIcon || theme.app.icons.loading, props.ui?.itemLeadingIcon], loading: true })"
71
+ ></span>
72
+ <span
73
+ v-else-if="item.icon"
74
+ :class="style.itemLeadingIcon({ class: [item.icon, props.ui?.itemLeadingIcon], active })"
75
+ ></span>
76
+ <Avatar
77
+ v-else-if="item.avatar"
78
+ v-bind="item.avatar"
79
+ :size="item.avatar.size || props.size"
80
+ :class="style.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar, active })"
81
+ />
82
+ </slot>
83
+
84
+ <span
85
+ v-if="get(item, props.labelKey as string) || !!slots[(`${item.slot || 'item'}-label` as keyof DropdownMenuContentSlots<T>)]"
86
+ :class="style.itemLabel({ class: props.ui?.itemLabel, active })"
87
+ >
88
+ <slot :name="(`${item.slot || 'item'}-label` as keyof DropdownMenuContentSlots<T>)" :item="(item as ExtractItem<T>)" :active="active" :index="index">
89
+ {{ get(item, props.labelKey as string) }}
90
+ </slot>
91
+
92
+ <span
93
+ v-if="item.target === '_blank' && externalIcon !== false"
94
+ :class="style.itemLabelExternalIcon({ class: [typeof externalIcon === 'string' ? externalIcon : theme.app.icons.external, props.ui?.itemLabelExternalIcon], active })"
95
+ ></span>
96
+ </span>
97
+
98
+ <span :class="style.itemTrailing({ class: props.ui?.itemTrailing })">
99
+ <slot :name="(`${item.slot || 'item'}-trailing` as keyof DropdownMenuContentSlots<T>)" :item="(item as ExtractItem<T>)" :active="active" :index="index">
100
+ <span v-if="item.children?.length" :class="style.itemTrailingIcon({ class: [theme.app.icons.chevronRight, props.ui?.itemTrailingIcon], active })"></span>
101
+ <span v-else-if="item.kbds?.length" :class="style.itemTrailingKbds({ class: props.ui?.itemTrailingKbds })">
102
+ <Kbd
103
+ v-for="(kbd, kbdIndex) in item.kbds"
104
+ :key="kbdIndex"
105
+ v-bind="typeof kbd === 'string' ? { value: kbd } : kbd"
106
+ :size="props.size"
107
+ />
108
+ </span>
109
+ </slot>
110
+
111
+ <DropdownMenu.ItemIndicator as-child>
112
+ <span :class="style.itemTrailingIcon({ class: [checkedIcon || theme.app.icons.check, props.ui?.itemTrailingIcon] })"></span>
113
+ </DropdownMenu.ItemIndicator>
114
+ </span>
115
+ </slot>
116
+ </DefineItemTemplate>
117
+
118
+ <DropdownMenu.Portal :disabled="!portal">
119
+ <component :is="sub ? DropdownMenu.SubContent : DropdownMenu.Content" :class="props.class" v-bind="contentProps">
120
+ <DropdownMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="style.group({ class: props.ui?.group })">
121
+ <template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
122
+ <DropdownMenu.Label v-if="item.type === 'label'" :class="style.label({ class: props.ui?.label })">
123
+ <ReuseItemTemplate :item="item" :index="index" />
124
+ </DropdownMenu.Label>
125
+ <DropdownMenu.Separator v-else-if="item.type === 'separator'" :class="style.separator({ class: props.ui?.separator })" />
126
+ <DropdownMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
127
+ <DropdownMenu.SubTrigger
128
+ as="button"
129
+ type="button"
130
+ :disabled="item.disabled"
131
+ :text-value="get(item, props.labelKey as string)"
132
+ :class="style.item({ class: props.ui?.item })"
133
+ >
134
+ <ReuseItemTemplate :item="item" :index="index" />
135
+ </DropdownMenu.SubTrigger>
136
+
137
+ <DropdownMenuContent
138
+ sub
139
+ :class="props.class"
140
+ :ui="props.ui"
141
+ :portal="props.portal"
142
+ :items="item.children"
143
+ side="right"
144
+ align="start"
145
+ :align-offset="-4"
146
+ :side-offset="3"
147
+ :label-key="labelKey"
148
+ :checked-icon="checkedIcon"
149
+ :loading-icon="loadingIcon"
150
+ :external-icon="externalIcon"
151
+ v-bind="item.content"
152
+ >
153
+ <template v-for="(_, name) in proxySlots" #[name]="slotProps">
154
+ <slot :name="(name as keyof DropdownMenuContentSlots<T>)" v-bind="slotProps"></slot>
155
+ </template>
156
+ </DropdownMenuContent>
157
+ </DropdownMenu.Sub>
158
+ <DropdownMenu.CheckboxItem
159
+ v-else-if="item.type === 'checkbox'"
160
+ :model-value="item.checked"
161
+ :disabled="item.disabled"
162
+ :text-value="get(item, props.labelKey as string)"
163
+ :class="style.item({ class: [props.ui?.item, item.class] })"
164
+ @update:model-value="item.onUpdateChecked"
165
+ @select="item.onSelect"
166
+ >
167
+ <ReuseItemTemplate :item="item" :index="index" />
168
+ </DropdownMenu.CheckboxItem>
169
+ <DropdownMenu.Item
170
+ v-else
171
+ as-child
172
+ :disabled="item.disabled"
173
+ :text-value="get(item, props.labelKey as string)"
174
+ @select="item.onSelect"
175
+ >
176
+ <Link v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<DropdownMenuItem, 'type'>)" custom>
177
+ <LinkBase v-bind="slotProps" :class="style.item({ class: [props.ui?.item, item.class], active })">
178
+ <ReuseItemTemplate :item="item" :active="active" :index="index" />
179
+ </LinkBase>
180
+ </Link>
181
+ </DropdownMenu.Item>
182
+ </template>
183
+ </DropdownMenu.Group>
184
+
185
+ <slot></slot>
186
+ </component>
187
+ </DropdownMenu.Portal>
188
+ </template>
@@ -0,0 +1,311 @@
1
+ <script lang="ts">
2
+ import type { ComputedRef, DeepReadonly, Ref } from 'vue'
3
+ import type { form } from '../theme'
4
+ import type { ComponentAttrs, FormError, FormErrorEvent, FormErrorWithId, FormEvent, FormInputEvents, FormSchema, FormSubmitEvent, FormValidateOptions } from '../types'
5
+
6
+ export interface FormEmits<T extends object> {
7
+ submit: [payload: FormSubmitEvent<T>]
8
+ error: [payload: FormErrorEvent]
9
+ }
10
+
11
+ export interface FormSlots {
12
+ default?: (props?: { errors: FormError[] }) => any
13
+ }
14
+
15
+ export interface FormExpose<T extends object> {
16
+ validate: (options?: FormValidateOptions<T>) => Promise<T | false>
17
+ clear: (path?: string) => void
18
+ errors: Ref<FormError[]>
19
+ setErrors: (errors: FormError[], name?: keyof T) => void
20
+ getErrors: (name?: keyof T) => FormError[]
21
+ submit: () => Promise<void>
22
+ disabled: ComputedRef<boolean>
23
+ dirty: ComputedRef<boolean>
24
+ dirtyFields: DeepReadonly<Set<keyof T>>
25
+ touchedFields: DeepReadonly<Set<keyof T>>
26
+ blurredFields: DeepReadonly<Set<keyof T>>
27
+ }
28
+
29
+ export interface FormProps<T extends object> extends Omit<ComponentAttrs<typeof form>, 'ui'> {
30
+ id?: string | number
31
+ /** Schema to validate the form state. */
32
+ schema?: FormSchema<T>
33
+ /** An object representing the current state of the form. */
34
+ state: Partial<T>
35
+ /**
36
+ * Custom validation function to validate the form state.
37
+ * @param state - The current state of the form.
38
+ * @returns A promise that resolves to an array of FormError objects, or an array of FormError objects directly.
39
+ */
40
+ validate?: (state: Partial<T>) => Promise<FormError[]> | FormError[]
41
+ /**
42
+ * The list of input events that trigger the form validation.
43
+ * @default ["blur", "change", "input"]
44
+ */
45
+ validateOn?: FormInputEvents[]
46
+ /** Disable all inputs inside the form. */
47
+ disabled?: boolean
48
+ /**
49
+ * Delay in milliseconds before validating the form on input events.
50
+ * @default 300
51
+ */
52
+ validateOnInputDelay?: number
53
+ /**
54
+ * If true, schema transformations will be applied to the state on submit.
55
+ * @default true
56
+ */
57
+ transform?: boolean
58
+ onSubmit?: ((event: FormSubmitEvent<T>) => void | Promise<void>) | (() => void | Promise<void>)
59
+ }
60
+
61
+ export class FormValidationExceptionError extends Error {
62
+ formId: string | number
63
+ errors: FormErrorWithId[]
64
+ children?: FormValidationExceptionError[]
65
+
66
+ constructor(formId: string | number, errors: FormErrorWithId[], childErrors?: FormValidationExceptionError[]) {
67
+ super('Form validation exception')
68
+ this.name = 'FormValidationExceptionError'
69
+ this.formId = formId
70
+ this.errors = errors
71
+ this.children = childErrors
72
+ Object.setPrototypeOf(this, FormValidationExceptionError.prototype)
73
+ }
74
+ }
75
+ </script>
76
+
77
+ <script setup lang="ts" generic="T extends object">
78
+ import { useEventBus } from '@vueuse/core'
79
+ import { computed, nextTick, onMounted, onUnmounted, readonly, ref, useId } from 'vue'
80
+ import { injectFormBus, provideFormBus, provideFormErrors, provideFormInputs, provideFormLoading, provideFormOptions } from '../app/injections'
81
+ import { useTheme } from '../composables/useTheme'
82
+ import { validateSchema } from '../utils'
83
+
84
+ const props = withDefaults(defineProps<FormProps<T>>(), {
85
+ validateOn: () => ['input', 'blur', 'change'],
86
+ validateOnInputDelay: 300,
87
+ transform: true,
88
+ })
89
+
90
+ const emit = defineEmits<FormEmits<T>>()
91
+ defineSlots<FormSlots>()
92
+
93
+ const formId = props.id ?? useId()
94
+
95
+ const bus = useEventBus<FormEvent<T>>(`form-${formId}`)
96
+ const parentBus = injectFormBus()
97
+
98
+ provideFormBus(bus)
99
+
100
+ const nestedForms = ref<Map<string | number, { validate: typeof validateFn }>>(new Map())
101
+
102
+ onMounted(() => {
103
+ bus.on(async (event) => {
104
+ if (event.type === 'attach') {
105
+ nestedForms.value.set(event.formId, { validate: event.validate })
106
+ }
107
+ else if (event.type === 'detach') {
108
+ nestedForms.value.delete(event.formId)
109
+ }
110
+ else if (props.validateOn?.includes(event.type) && !loading.value) {
111
+ if (event.type !== 'input')
112
+ await validateFn({ name: event.name, silent: true, nested: false })
113
+ else if (event.eager || blurredFields.has(event.name))
114
+ await validateFn({ name: event.name, silent: true, nested: false })
115
+ }
116
+
117
+ if (event.type === 'blur')
118
+ blurredFields.add(event.name)
119
+
120
+ if (event.type === 'change' || event.type === 'input' || event.type === 'blur' || event.type === 'focus')
121
+ touchedFields.add(event.name)
122
+
123
+ if (event.type === 'change' || event.type === 'input')
124
+ dirtyFields.add(event.name)
125
+ })
126
+ })
127
+
128
+ onUnmounted(() => {
129
+ bus.reset()
130
+ })
131
+
132
+ onMounted(async () => {
133
+ if (parentBus) {
134
+ await nextTick()
135
+ parentBus.emit({ type: 'attach', validate: validateFn as FormExpose<T>['validate'], formId })
136
+ }
137
+ })
138
+
139
+ onUnmounted(() => {
140
+ if (parentBus)
141
+ parentBus.emit({ type: 'detach', formId })
142
+ })
143
+
144
+ const errors = ref<FormErrorWithId[]>([])
145
+ provideFormErrors(errors)
146
+
147
+ const inputs = ref<{ [P in keyof T]?: { id?: string, pattern?: RegExp } }>({})
148
+ provideFormInputs(inputs as any)
149
+
150
+ const dirtyFields = new Set<keyof T>()
151
+ const touchedFields = new Set<keyof T>()
152
+ const blurredFields = new Set<keyof T>()
153
+
154
+ function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
155
+ return errs.map((err) => ({
156
+ ...err,
157
+ id: err?.name ? inputs.value[err.name]?.id : undefined,
158
+ }))
159
+ }
160
+
161
+ const transformedState = ref<T | null>(null)
162
+
163
+ async function getErrors(): Promise<FormErrorWithId[]> {
164
+ let errs = props.validate ? (await props.validate(props.state)) ?? [] : []
165
+
166
+ if (props.schema) {
167
+ const { errors, result } = await validateSchema(props.state, props.schema as FormSchema<typeof props.state>)
168
+
169
+ if (errors)
170
+ errs = errs.concat(errors)
171
+ else
172
+ transformedState.value = result
173
+ }
174
+
175
+ return resolveErrorIds(errs)
176
+ }
177
+
178
+ async function validateFn(options: FormValidateOptions<T>): Promise<T | false> {
179
+ const _options: FormValidateOptions<T> = {
180
+ silent: false,
181
+ nested: true,
182
+ transform: false,
183
+ ...options,
184
+ }
185
+
186
+ const names = _options.name && !Array.isArray(_options.name) ? [_options.name] : _options.name as (keyof T)[]
187
+
188
+ const nestedValidatePromises = !names && _options.nested
189
+ ? Array.from(nestedForms.value.values()).map(
190
+ ({ validate }) => validate(_options).then(() => {}).catch((error: Error) => {
191
+ if (!(error instanceof FormValidationExceptionError))
192
+ throw error
193
+
194
+ return error
195
+ }),
196
+ )
197
+ : []
198
+
199
+ if (names) {
200
+ const otherErrors = errors.value.filter((error) => !names.some((name) => {
201
+ const pattern = inputs.value?.[name]?.pattern
202
+ return name === error.name || (pattern && error.name?.match(pattern))
203
+ }))
204
+
205
+ // eslint-disable-next-line unicorn/no-await-expression-member
206
+ const pathErrors = (await getErrors()).filter((error) => names.some((name) => {
207
+ const pattern = inputs.value?.[name]?.pattern
208
+ return name === error.name || (pattern && error.name?.match(pattern))
209
+ }))
210
+
211
+ errors.value = otherErrors.concat(pathErrors)
212
+ }
213
+ else {
214
+ errors.value = await getErrors()
215
+ }
216
+
217
+ // eslint-disable-next-line unicorn/no-await-expression-member
218
+ const childErrors = (await Promise.all(nestedValidatePromises)).filter((val) => val !== undefined)
219
+
220
+ if (errors.value.length + childErrors.length > 0) {
221
+ if (_options.silent)
222
+ return false
223
+
224
+ throw new FormValidationExceptionError(formId, errors.value, childErrors)
225
+ }
226
+
227
+ if (_options.transform)
228
+ Object.assign(props.state, transformedState.value)
229
+
230
+ return props.state as T
231
+ }
232
+
233
+ const loading = ref(false)
234
+ provideFormLoading(readonly(loading))
235
+
236
+ async function onSubmitWrapper(payload: Event) {
237
+ loading.value = true
238
+
239
+ const event = payload as FormSubmitEvent<any>
240
+
241
+ try {
242
+ event.data = await validateFn({ nested: true, transform: props.transform })
243
+ await props.onSubmit?.(event)
244
+ dirtyFields.clear()
245
+ }
246
+ catch (error) {
247
+ if (!(error instanceof FormValidationExceptionError))
248
+ throw error
249
+
250
+ const errorEvent: FormErrorEvent = {
251
+ ...event,
252
+ errors: error.errors,
253
+ children: error.children,
254
+ }
255
+ emit('error', errorEvent)
256
+ }
257
+ finally {
258
+ loading.value = false
259
+ }
260
+ }
261
+
262
+ const disabled = computed(() => props.disabled || loading.value)
263
+
264
+ provideFormOptions(computed(() => ({
265
+ disabled: disabled.value,
266
+ validateOnInputDelay: props.validateOnInputDelay,
267
+ })))
268
+
269
+ defineExpose<FormExpose<T>>({
270
+ validate: validateFn as FormExpose<T>['validate'],
271
+ errors,
272
+
273
+ setErrors(errs: FormError[], name?: keyof T) {
274
+ errors.value = name
275
+ ? errors.value
276
+ .filter((error) => error.name !== name)
277
+ .concat(resolveErrorIds(errs))
278
+ : resolveErrorIds(errs)
279
+ },
280
+
281
+ async submit() {
282
+ await onSubmitWrapper(new Event('submit'))
283
+ },
284
+
285
+ getErrors(name?: keyof T) {
286
+ if (name)
287
+ return errors.value.filter((err) => err.name === name)
288
+
289
+ return errors.value
290
+ },
291
+
292
+ clear(name?: string) {
293
+ errors.value = name ? errors.value.filter((err) => err.name !== name) : []
294
+ },
295
+
296
+ disabled,
297
+ dirty: computed(() => dirtyFields.size > 0),
298
+ dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof T>>,
299
+ blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof T>>,
300
+ touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof T>>,
301
+ })
302
+
303
+ const { generateStyle } = useTheme()
304
+ const style = computed(() => generateStyle('form', props))
305
+ </script>
306
+
307
+ <template>
308
+ <component :is="parentBus ? 'div' : 'form'" :id="formId" :class="style" @submit.prevent="onSubmitWrapper">
309
+ <slot :errors="errors"></slot>
310
+ </component>
311
+ </template>
@@ -0,0 +1,129 @@
1
+ <script lang="ts">
2
+ import type { VariantProps } from '@byyuurin/ui-kit'
3
+ import type { PrimitiveProps } from 'reka-ui'
4
+ import type { formItem } from '../theme'
5
+ import type { ComponentAttrs } from '../types'
6
+
7
+ export interface FormFieldSlots {
8
+ label?: (props: { label?: string }) => any
9
+ hint?: (props: { hint?: string }) => any
10
+ description?: (props: { description?: string }) => any
11
+ help?: (props: { help?: string }) => any
12
+ error?: (props: { error?: string | boolean }) => any
13
+ default?: (props: { error?: string | boolean }) => any
14
+ }
15
+
16
+ type FormItemVariants = VariantProps<typeof formItem>
17
+
18
+ export interface FormItemProps extends ComponentAttrs<typeof formItem> {
19
+ /**
20
+ * The element or component this component should render as.
21
+ * @default "div"
22
+ */
23
+ as?: PrimitiveProps['as']
24
+ /** The name of the FormItem. Also used to match form errors. */
25
+ name?: string
26
+ /** A regular expression to match form error names. */
27
+ errorPattern?: RegExp
28
+ label?: string
29
+ description?: string
30
+ help?: string
31
+ error?: string | boolean
32
+ hint?: string
33
+ /**
34
+ * @default 'md'
35
+ */
36
+ size?: FormItemVariants['size']
37
+ required?: boolean
38
+ /** If true, validation on input will be active immediately instead of waiting for a blur event. */
39
+ eagerValidation?: boolean
40
+ /**
41
+ * Delay in milliseconds before validating the form on input events.
42
+ * @default 300
43
+ */
44
+ validateOnInputDelay?: number
45
+ }
46
+ </script>
47
+
48
+ <script setup lang="ts">
49
+ import { Label, Primitive } from 'reka-ui'
50
+ import { computed, ref, useId } from 'vue'
51
+ import { injectFormErrors, provideFormInputId, provideFormItem } from '../app/injections'
52
+ import { useTheme } from '../composables/useTheme'
53
+
54
+ const props = defineProps<FormItemProps>()
55
+ const slots = defineSlots<FormFieldSlots>()
56
+
57
+ const id = ref(useId())
58
+ // Copies id's initial value to bind aria-attributes such as aria-describedby.
59
+ // This is required for the RadioGroup component which unsets the id value.
60
+ const ariaId = id.value
61
+ provideFormInputId(id)
62
+
63
+ const formErrors = injectFormErrors()
64
+ const error = computed(() => {
65
+ if (props.error)
66
+ return props.error
67
+
68
+ const formError = formErrors?.value.find((error) => {
69
+ if (error.name && error.name === props.name)
70
+ return true
71
+
72
+ if (error.name && props.errorPattern)
73
+ return error.name.match(props.errorPattern)
74
+
75
+ return false
76
+ })
77
+
78
+ return formError?.message
79
+ })
80
+
81
+ provideFormItem(computed(() => ({
82
+ ...props,
83
+ error: error.value,
84
+ ariaId,
85
+ })))
86
+
87
+ const { generateStyle } = useTheme()
88
+ const style = computed(() => generateStyle('formItem', props))
89
+ </script>
90
+
91
+ <template>
92
+ <Primitive :as="props.as" :class="style.root({ class: [props.class, props.ui?.root] })">
93
+ <div :class="style.wrapper({ class: props.ui?.wrapper })">
94
+ <div v-if="props.label || slots.label" :class="style.labelWrapper({ class: props.ui?.labelWrapper })">
95
+ <Label :for="id" :class="style.label({ class: props.ui?.label })">
96
+ <slot name="label" :label="props.label">
97
+ {{ props.label }}
98
+ </slot>
99
+ </Label>
100
+ <span v-if="props.hint || slots.hint" :id="`${ariaId}-hint`" :class="style.hint({ class: props.ui?.hint })">
101
+ <slot name="hint" :hint="props.hint">
102
+ {{ props.hint }}
103
+ </slot>
104
+ </span>
105
+ </div>
106
+
107
+ <p v-if="props.description || slots.description" :id="`${ariaId}-description`" :class="style.description({ class: props.ui?.description })">
108
+ <slot name="description" :description="props.description">
109
+ {{ props.description }}
110
+ </slot>
111
+ </p>
112
+ </div>
113
+
114
+ <div :class="(props.label || slots.label || props.description || slots.description) && style.container({ class: props.ui?.container })">
115
+ <slot :error="error"></slot>
116
+
117
+ <p v-if="(typeof error === 'string' && error) || slots.error" :id="`${ariaId}-error`" :class="style.error({ class: props.ui?.error })">
118
+ <slot name="error" :error="error">
119
+ {{ error }}
120
+ </slot>
121
+ </p>
122
+ <p v-else :class="style.help({ class: props.ui?.help })">
123
+ <slot name="help" :help="props.help">
124
+ {{ help }}
125
+ </slot>
126
+ </p>
127
+ </div>
128
+ </Primitive>
129
+ </template>