Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add disableHtmlValidation options as config and props #55

Merged
merged 2 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading