From c38e2af14d50a628cd9d2237a5ec3af8b0862278 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Sun, 2 Mar 2025 12:57:42 +0200 Subject: [PATCH] feat: basic implementation of constraints simulator --- packages/core/src/useSelect/useSelect.ts | 19 +++- .../src/validation/useContraintsValidator.ts | 95 +++++++++++++++++++ packages/playground/src/App.vue | 4 +- .../playground/src/components/InputSelect.vue | 4 +- 4 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 packages/core/src/validation/useContraintsValidator.ts diff --git a/packages/core/src/useSelect/useSelect.ts b/packages/core/src/useSelect/useSelect.ts index b4621f22..ba9982be 100644 --- a/packages/core/src/useSelect/useSelect.ts +++ b/packages/core/src/useSelect/useSelect.ts @@ -15,6 +15,7 @@ import { useListBox } from '../useListBox'; import { useLabel, useErrorMessage } from '../a11y'; import { FieldTypePrefixes } from '../constants'; import { registerField } from '@formwerk/devtools'; +import { useConstraintsValidator } from '../validation/useContraintsValidator'; export interface SelectProps { /** @@ -32,6 +33,11 @@ export interface SelectProps { */ description?: string; + /** + * Whether the select field is required. + */ + required?: boolean; + /** * Placeholder text when no option is selected. */ @@ -93,7 +99,7 @@ export function useSelect(_props: Reactivify(); const { fieldValue, setValue, errorMessage, isDisabled } = field; const isMutable = () => !isDisabled.value && !toValue(props.readonly); const { labelProps, labelledByProps } = useLabel({ @@ -126,7 +132,14 @@ export function useSelect(_props: Reactivify toValue(props.required), + value: fieldValue, + source: triggerEl, + }); + + const { updateValidity } = useInputValidity({ field, inputEl }); const { descriptionProps, describedByProps } = createDescribedByProps({ inputId, description: props.description, @@ -251,8 +264,6 @@ export function useSelect(_props: Reactivify(); - const triggerProps = computed(() => { return withRefCapture( { diff --git a/packages/core/src/validation/useContraintsValidator.ts b/packages/core/src/validation/useContraintsValidator.ts new file mode 100644 index 00000000..02b022bc --- /dev/null +++ b/packages/core/src/validation/useContraintsValidator.ts @@ -0,0 +1,95 @@ +import { MaybeRefOrGetter, onMounted, Ref, shallowRef, toValue, watchEffect } from 'vue'; +import { Maybe } from '../types'; +import { useEventListener } from '../helpers/useEventListener'; + +export interface ConstraintOptions { + value: MaybeRefOrGetter; + source: Ref>; +} + +interface BaseConstraints extends ConstraintOptions { + required?: MaybeRefOrGetter>; +} + +interface TextualConstraints extends BaseConstraints { + type: 'text'; + minLength?: MaybeRefOrGetter>; + maxLength?: MaybeRefOrGetter>; +} + +interface SelectConstraints extends BaseConstraints { + type: 'select'; +} + +interface NumericConstraints extends BaseConstraints { + type: 'number'; + min?: MaybeRefOrGetter>; + max?: MaybeRefOrGetter>; +} + +interface DateConstraints extends BaseConstraints { + type: 'date'; + min?: MaybeRefOrGetter>; + max?: MaybeRefOrGetter>; +} + +export type Constraints = TextualConstraints | SelectConstraints | NumericConstraints | DateConstraints; + +export function useConstraintsValidator(constraints: Constraints) { + const element = shallowRef(); + + onMounted(() => { + element.value = document.createElement('input'); + element.value.type = constraints.type === 'select' ? 'text' : constraints.type; + }); + + watchEffect(() => { + if (!element.value) { + return; + } + + element.value.required = toValue(constraints.required) ?? false; + + if (constraints.type === 'text') { + element.value.setAttribute('minlength', toValue(constraints.minLength)?.toString() ?? ''); + element.value.setAttribute('maxlength', toValue(constraints.maxLength)?.toString() ?? ''); + } + + if (constraints.type === 'number') { + element.value.setAttribute('min', toValue(constraints.min)?.toString() ?? ''); + element.value.setAttribute('max', toValue(constraints.max)?.toString() ?? ''); + } + + if (constraints.type === 'date') { + element.value.setAttribute('min', toValue(constraints.min)?.toISOString() ?? ''); + element.value.setAttribute('max', toValue(constraints.max)?.toISOString() ?? ''); + } + }); + + watchEffect(() => { + if (!element.value) { + return; + } + + const val = toValue(constraints.value); + if (constraints.type === 'text' || element.value.type === 'text') { + element.value.value = String(val ?? ''); + } + + if (constraints.type === 'number') { + element.value.value = String(val ?? ''); + } + + if (constraints.type === 'date') { + element.value.value = val ? new Date(String(val)).toISOString() : ''; + } + }); + + useEventListener(constraints.source, ['change', 'blur', 'input'], evt => { + element.value?.dispatchEvent(new Event(evt.type)); + }); + + return { + element, + }; +} diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index 83d7f8ca..8ded3ae8 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -11,6 +11,7 @@ import Switch from '@/components/Switch.vue'; import InputTextArea from '@/components/InputTextArea.vue'; import { ref } from 'vue'; import AllForm from './components/AllForm.vue'; +import Slider from './components/Slider.vue'; const min = new Date(2025, 0, 4, 0, 0, 0, 0); const value = new Date('2025-01-15'); @@ -27,14 +28,13 @@ const isNotificationsEnabled = ref(false);