Skip to content

Commit

Permalink
feat: add disableHtmlValidation options as config and props (#55)
Browse files Browse the repository at this point in the history
* feat: add `disableHtmlValidation` option on config, form and form group

* feat: add disable html validation on field level
  • Loading branch information
logaretm authored Aug 23, 2024
1 parent aac06c2 commit 9141f74
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 14 deletions.
15 changes: 10 additions & 5 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { PartialDeep } from 'type-fest';
import { shallowRef } from 'vue';
import { getSiteLocale } from './i18n/getSiteLocale';
import { merge } from '../../shared/src';

interface Config {
locale: string;
validation: {
disableHtmlValidation: boolean;
};
}

const currentConfig = shallowRef<Config>({
locale: getSiteLocale(),
validation: {
disableHtmlValidation: false,
},
});

export function configure(config: Partial<Config>) {
currentConfig.value = {
...currentConfig.value,
...config,
};
export function configure(config: PartialDeep<Config>) {
currentConfig.value = merge({ ...currentConfig.value }, config);
}

export function getConfig() {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/useCheckbox/useCheckbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface CheckboxProps<TValue = string> {
indeterminate?: boolean;

schema?: TypedSchema<TValue>;

disableHtmlValidation?: boolean;
}

export interface CheckboxDomInputProps extends AriaLabelableProps, InputBaseAttributes {
Expand Down Expand Up @@ -64,7 +66,7 @@ export function useCheckbox<TValue = string>(
const group: CheckboxGroupContext<TValue> | null = inject(CheckboxGroupKey, null);
const inputRef = elementRef || ref<HTMLElement>();
const field = useCheckboxField(props);
useInputValidity({ inputRef, field });
useInputValidity({ inputRef, field, disableHtmlValidation: props.disableHtmlValidation });
const { fieldValue, isTouched, setTouched, setValue, errorMessage, setErrors } = field;

const checked = computed({
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/useForm/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,22 @@ import { FormTransactionManager, useFormTransactions } from './useFormTransactio
import { useFormActions } from './useFormActions';
import { useFormSnapshots } from './formSnapshot';
import { findLeaf } from '../utils/path';
import { getConfig } from '../config';

export interface FormOptions<TForm extends FormObject = FormObject, TOutput extends FormObject = TForm> {
id: string;
initialValues: MaybeGetter<MaybeAsync<TForm>>;
initialTouched: TouchedSchema<TForm>;
schema: TypedSchema<TForm, TOutput>;
disableHtmlValidation: boolean;
}

export interface FormContext<TForm extends FormObject = FormObject, TOutput extends FormObject = TForm>
extends BaseFormContext<TForm>,
FormTransactionManager<TForm> {
requestValidation(): Promise<FormValidationResult<TOutput>>;
onSubmitAttempt(cb: () => void): void;
isHtmlValidationDisabled(): boolean;
onValidationDispatch(
cb: (enqueue: (promise: Promise<ValidationResult | GroupValidationResult>) => void) => void,
): void;
Expand All @@ -47,6 +50,7 @@ export function useForm<TForm extends FormObject = FormObject, TOutput extends F
schema: opts?.schema,
});

const isHtmlValidationDisabled = () => opts?.disableHtmlValidation ?? getConfig().validation.disableHtmlValidation;
const values = reactive(cloneDeep(valuesSnapshot.originals.value)) as TForm;
const touched = reactive(cloneDeep(touchedSnapshot.originals.value)) as TouchedSchema<TForm>;
const disabled = {} as DisabledSchema<TForm>;
Expand Down Expand Up @@ -103,6 +107,7 @@ export function useForm<TForm extends FormObject = FormObject, TOutput extends F
...ctx,
...transactionsManager,
...privateActions,
isHtmlValidationDisabled,
} as FormContext<TForm, TOutput>);

if (ctx.getValidationMode() === 'schema') {
Expand Down
143 changes: 141 additions & 2 deletions packages/core/src/useFormGroup/useFormGroup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ import { useTextField } from '../useTextField';
import { useForm } from '../useForm';
import { fireEvent, render, screen } from '@testing-library/vue';
import { flush } from '@test-utils/flush';
import { configure } from '../config';

function createInputComponent(): Component {
return {
inheritAttrs: false,
setup: (_, { attrs }) => {
const name = (attrs.name || 'test') as string;
const schema = attrs.schema as TypedSchema<any>;
const { errorMessage, inputProps } = useTextField({ name, label: name, schema });
const { errorMessage, inputProps } = useTextField({
name,
label: name,
schema,
disableHtmlValidation: attrs.disableHtmlValidation as any,
});

return { errorMessage: errorMessage, inputProps, name, attrs };
},
Expand All @@ -30,7 +36,7 @@ function createGroupComponent(fn?: (fg: ReturnType<typeof useFormGroup>) => void
setup: (_, { attrs }) => {
const name = (attrs.name || 'test') as string;
const schema = attrs.schema as TypedSchema<any>;
const fg = useFormGroup({ name, label: name, schema });
const fg = useFormGroup({ name, label: name, schema, disableHtmlValidation: attrs.disableHtmlValidation as any });
fn?.(fg);

return {};
Expand Down Expand Up @@ -384,3 +390,136 @@ test('submission combines group data with form data', async () => {
third: 'third',
});
});

describe('disabling HTML validation', () => {
test('can be disabled on the group level', async () => {
await render({
components: { TInput: createInputComponent(), TGroup: createGroupComponent() },
setup() {
useForm();

return {};
},
template: `
<TGroup :disableHtmlValidation="true">
<TInput name="field1" :required="true" />
</TGroup>
<TInput name="field2" :required="true" />
`,
});

await flush();
await fireEvent.touch(screen.getByTestId('field1'));
await fireEvent.touch(screen.getByTestId('field2'));

const errors = screen.getAllByTestId('err');
expect(errors[0]).toHaveTextContent('');
expect(errors[1]).toHaveTextContent('Constraints not satisfied');
});

test('can be disabled on the form level', async () => {
await render({
components: { TInput: createInputComponent(), TGroup: createGroupComponent() },
setup() {
useForm({ disableHtmlValidation: true });

return {};
},
template: `
<TGroup>
<TInput name="field1" :required="true" />
</TGroup>
<TInput name="field2" :required="true" />
<TGroup :disableHtmlValidation="false">
<TInput name="field3" :required="true" />
</TGroup>
`,
});

await flush();
await fireEvent.touch(screen.getByTestId('field1'));
await fireEvent.touch(screen.getByTestId('field2'));
await fireEvent.touch(screen.getByTestId('field3'));

const errors = screen.getAllByTestId('err');
expect(errors[0]).toHaveTextContent('');
expect(errors[1]).toHaveTextContent('');
expect(errors[2]).toHaveTextContent('Constraints not satisfied');
});

test('can be disabled on the field level', async () => {
await render({
components: { TInput: createInputComponent(), TGroup: createGroupComponent() },
setup() {
useForm();

return {};
},
template: `
<TGroup>
<TInput name="field1" :required="true" />
<TInput name="field2" :required="true" :disableHtmlValidation="true" />
</TGroup>
<TInput name="field3" :required="true" :disableHtmlValidation="true" />
`,
});

await flush();
await fireEvent.touch(screen.getByTestId('field1'));
await fireEvent.touch(screen.getByTestId('field2'));
await fireEvent.touch(screen.getByTestId('field3'));

const errors = screen.getAllByTestId('err');
expect(errors[0]).toHaveTextContent('Constraints not satisfied');
expect(errors[1]).toHaveTextContent('');
expect(errors[2]).toHaveTextContent('');
});

test('can be disabled globally and overridden', async () => {
configure({
validation: {
disableHtmlValidation: true,
},
});

await render({
components: { TInput: createInputComponent(), TGroup: createGroupComponent() },
setup() {
useForm({ disableHtmlValidation: true });

return {};
},
template: `
<TGroup>
<TInput name="field1" :required="true" />
</TGroup>
<TInput name="field2" :required="true" />
<TGroup :disableHtmlValidation="false">
<TInput name="field3" :required="true" />
</TGroup>
`,
});

await flush();
await fireEvent.touch(screen.getByTestId('field1'));
await fireEvent.touch(screen.getByTestId('field2'));
await fireEvent.touch(screen.getByTestId('field3'));

const errors = screen.getAllByTestId('err');
expect(errors[0]).toHaveTextContent('');
expect(errors[1]).toHaveTextContent('');
expect(errors[2]).toHaveTextContent('Constraints not satisfied');

configure({
validation: {
disableHtmlValidation: false,
},
});
});
});
8 changes: 8 additions & 0 deletions packages/core/src/useFormGroup/useFormGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ import { FormKey } from '../useForm';
import { useValidationProvider } from '../validation/useValidationProvider';
import { FormValidationMode } from '../useForm/formContext';
import { prefixPath as _prefixPath } from '../utils/path';
import { getConfig } from '@core/config';

export interface FormGroupProps<TInput extends FormObject = FormObject, TOutput extends FormObject = TInput> {
name: string;
label?: string;
schema?: TypedSchema<TInput, TOutput>;
disableHtmlValidation?: boolean;
}

interface GroupProps extends AriaLabelableProps {
Expand All @@ -45,6 +47,7 @@ interface FormGroupContext<TOutput extends FormObject = FormObject> {
onValidationDispatch(cb: (enqueue: (promise: Promise<ValidationResult>) => void) => void): void;
requestValidation(): Promise<GroupValidationResult<TOutput>>;
getValidationMode(): FormValidationMode;
isHtmlValidationDisabled(): boolean;
}

export const FormGroupKey: InjectionKey<FormGroupContext> = Symbol('FormGroup');
Expand All @@ -58,6 +61,10 @@ export function useFormGroup<TInput extends FormObject = FormObject, TOutput ext
const getPath = () => toValue(props.name);
const groupRef = elementRef || shallowRef<HTMLInputElement>();
const form = inject(FormKey, null);
const isHtmlValidationDisabled = () =>
toValue(props.disableHtmlValidation) ??
form?.isHtmlValidationDisabled() ??
getConfig().validation.disableHtmlValidation;
const { validate, onValidationDispatch, defineValidationRequest } = useValidationProvider({
getValues,
getPath,
Expand Down Expand Up @@ -138,6 +145,7 @@ export function useFormGroup<TInput extends FormObject = FormObject, TOutput ext
onValidationDispatch,
requestValidation,
getValidationMode: () => (props.schema ? 'schema' : 'aggregate'),
isHtmlValidationDisabled,
};

// Whenever the form is validated, it is deferred to the form group to do that.
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/useNumberField/useNumberField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export interface NumberFieldProps {
formatOptions?: Intl.NumberFormatOptions;

schema?: TypedSchema<number>;

disableHtmlValidation?: boolean;
}

export function useNumberField(
Expand All @@ -81,7 +83,7 @@ export function useNumberField(
schema: props.schema,
});

const { validityDetails } = useInputValidity({ inputRef, field });
const { validityDetails } = useInputValidity({ inputRef, field, disableHtmlValidation: props.disableHtmlValidation });
const { displayError } = useErrorDisplay(field);
const { fieldValue, setValue, setTouched, isTouched, errorMessage } = field;
const formattedText = computed<string>(() => {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/useRadio/useRadioGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface RadioGroupContext<TValue> {
required: boolean;

readonly modelValue: TValue | undefined;

setErrors(message: Arrayable<string>): void;
setValue(value: TValue): void;
setTouched(touched: boolean): void;
Expand Down Expand Up @@ -59,6 +60,8 @@ export interface RadioGroupProps<TValue = string> {
required?: boolean;

schema?: TypedSchema<TValue>;

disableHtmlValidation?: boolean;
}

interface RadioGroupDomProps extends AriaLabelableProps, AriaDescribableProps, AriaValidatableProps {
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/useSearchField/useSearchField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export interface SearchFieldProps {
schema?: TypedSchema<string>;

onSubmit?: (value: string) => void;

disableHtmlValidation?: boolean;
}

export function useSearchField(
Expand All @@ -71,7 +73,11 @@ export function useSearchField(
schema: props.schema,
});

const { validityDetails, updateValidity } = useInputValidity({ inputRef, field });
const { validityDetails, updateValidity } = useInputValidity({
inputRef,
field,
disableHtmlValidation: props.disableHtmlValidation,
});
const { displayError } = useErrorDisplay(field);
const { fieldValue, setValue, isTouched, setTouched, errorMessage, isValid, errors } = field;

Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/useSwitch/useSwitch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export type SwitchProps = {
falseValue?: unknown;

schema?: TypedSchema<unknown>;

disableHtmlValidation?: boolean;
};

export function useSwitch(_props: Reactivify<SwitchProps, 'schema'>, elementRef?: Ref<HTMLInputElement>) {
Expand All @@ -72,7 +74,7 @@ export function useSwitch(_props: Reactivify<SwitchProps, 'schema'>, elementRef?
schema: props.schema,
});

useInputValidity({ field, inputRef });
useInputValidity({ field, inputRef, disableHtmlValidation: props.disableHtmlValidation });
const { fieldValue, setValue, isTouched, setTouched, errorMessage } = field;
const { errorMessageProps, accessibleErrorProps } = createAccessibleErrorMessageProps({
inputId,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/useTextField/useTextField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export interface TextFieldProps {
disabled?: boolean;

schema?: TypedSchema<string>;

disableHtmlValidation?: boolean;
}

export function useTextField(
Expand All @@ -73,7 +75,7 @@ export function useTextField(
schema: props.schema,
});

const { validityDetails } = useInputValidity({ inputRef, field });
const { validityDetails } = useInputValidity({ inputRef, field, disableHtmlValidation: props.disableHtmlValidation });
const { displayError } = useErrorDisplay(field);
const { fieldValue, setValue, isTouched, setTouched, errorMessage, isValid, errors, setErrors } = field;
const { labelProps, labelledByProps } = useLabel({
Expand Down
Loading

0 comments on commit 9141f74

Please sign in to comment.