Skip to content

Commit

Permalink
feat: basic implementation of constraints simulator
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Mar 2, 2025
1 parent a764650 commit c38e2af
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 8 deletions.
19 changes: 15 additions & 4 deletions packages/core/src/useSelect/useSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TOption, TValue = TOption> {
/**
Expand All @@ -32,6 +33,11 @@ export interface SelectProps<TOption, TValue = TOption> {
*/
description?: string;

/**
* Whether the select field is required.
*/
required?: boolean;

/**
* Placeholder text when no option is selected.
*/
Expand Down Expand Up @@ -93,7 +99,7 @@ export function useSelect<TOption, TValue = TOption>(_props: Reactivify<SelectPr
disabled: props.disabled,
schema: props.schema,
});

const triggerEl = ref<HTMLElement>();
const { fieldValue, setValue, errorMessage, isDisabled } = field;
const isMutable = () => !isDisabled.value && !toValue(props.readonly);
const { labelProps, labelledByProps } = useLabel({
Expand Down Expand Up @@ -126,7 +132,14 @@ export function useSelect<TOption, TValue = TOption>(_props: Reactivify<SelectPr
onToggleAfter: toggleAfter,
});

const { updateValidity } = useInputValidity({ field });
const { element: inputEl } = useConstraintsValidator({
type: 'select',
required: () => toValue(props.required),
value: fieldValue,
source: triggerEl,
});

const { updateValidity } = useInputValidity({ field, inputEl });
const { descriptionProps, describedByProps } = createDescribedByProps({
inputId,
description: props.description,
Expand Down Expand Up @@ -251,8 +264,6 @@ export function useSelect<TOption, TValue = TOption>(_props: Reactivify<SelectPr
},
};

const triggerEl = ref<HTMLElement>();

const triggerProps = computed<SelectTriggerDomProps>(() => {
return withRefCapture(
{
Expand Down
95 changes: 95 additions & 0 deletions packages/core/src/validation/useContraintsValidator.ts
Original file line number Diff line number Diff line change
@@ -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<TValue> {
value: MaybeRefOrGetter<TValue>;
source: Ref<Maybe<HTMLElement>>;
}

interface BaseConstraints extends ConstraintOptions<unknown> {
required?: MaybeRefOrGetter<Maybe<boolean>>;
}

interface TextualConstraints extends BaseConstraints {
type: 'text';
minLength?: MaybeRefOrGetter<Maybe<number>>;
maxLength?: MaybeRefOrGetter<Maybe<number>>;
}

interface SelectConstraints extends BaseConstraints {
type: 'select';
}

interface NumericConstraints extends BaseConstraints {
type: 'number';
min?: MaybeRefOrGetter<Maybe<number>>;
max?: MaybeRefOrGetter<Maybe<number>>;
}

interface DateConstraints extends BaseConstraints {
type: 'date';
min?: MaybeRefOrGetter<Maybe<Date>>;
max?: MaybeRefOrGetter<Maybe<Date>>;
}

export type Constraints = TextualConstraints | SelectConstraints | NumericConstraints | DateConstraints;

export function useConstraintsValidator(constraints: Constraints) {
const element = shallowRef<HTMLInputElement>();

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,
};
}
4 changes: 2 additions & 2 deletions packages/playground/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -27,14 +28,13 @@ const isNotificationsEnabled = ref(false);

<template>
<div class="">
<DateField name="birthdate" label="Birth Date" :value="value" :min="min" :max="max" />
<InputSelect name="country" label="Country" required :options="options" />

<!-- <h2 class="text-2xl font-bold mb-6">Fields Outside Form</h2>
<InputText name="username" label="Username" placeholder="Enter username" />
<InputNumber name="age" label="Age" :min="0" :max="120" />
<InputTextArea name="description" label="Description" placeholder="Enter description" />
<DateField name="birthdate" label="Birth Date" :value="value" :min="min" :max="max" />
<InputSelect name="country" label="Country" :options="options" />
<CheckboxGroup name="hobbies" label="Hobbies">
<CheckboxItem name="hobbies" value="reading" label="Reading" />
Expand Down
4 changes: 2 additions & 2 deletions packages/playground/src/components/InputSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface TheProps<TOption, TValue> extends SelectProps<TOption, TValue>
const props = defineProps<TheProps<TOption, TValue>>();
const { triggerProps, labelProps, errorMessageProps, isTouched, displayError, fieldValue, listBoxProps } =
const { triggerProps, labelProps, errorMessageProps, isTouched, errorMessage, fieldValue, listBoxProps } =
useSelect(props);
</script>

Expand Down Expand Up @@ -64,7 +64,7 @@ const { triggerProps, labelProps, errorMessageProps, isTouched, displayError, fi
</div>

<span v-bind="errorMessageProps" class="error-message">
{{ displayError() }}
{{ errorMessage }}
</span>
</div>
</template>
Expand Down

0 comments on commit c38e2af

Please sign in to comment.