Skip to content

Commit

Permalink
feat: add standalone prop for fields (#3379)
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Jul 10, 2021
1 parent d930271 commit 3689437
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 19 deletions.
31 changes: 16 additions & 15 deletions docs/content/api/field.md
Expand Up @@ -123,21 +123,22 @@ Note that you no longer should use `v-model` on your input as `v-bind="field"` w

### Props

| Prop | Type | Required/Default | Description |
| :-------------------- | :----------------------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| as | `string` | `"span"` | The element to render as a root node, defaults to `input` |
| name | `string` | Required | The field's name, must be inside `<Form />` |
| rules | `object \| string \| Function` | `null` | The field's validation rules |
| validateOnMount | `boolean` | `false` | If true, field will be validated when the component is mounted |
| validateOnInput | `boolean` | `false` | If true, field will be validated when `input` event is dispatched/emitted |
| validateOnChange | `boolean` | `true` | If true, field will be validated when `change` event is dispatched/emitted |
| validateOnBlur | `boolean` | `true` | If true, field will be validated when `blur` event is dispatched/emitted |
| validateOnModelUpdate | `boolean` | `true` | If true, field will be validated when `update:modelValue` event is emitted |
| bails | `boolean` | `true` | Stops validating as soon as a rule fails the validation |
| label | `string` | `undefined` | A different string to override the field `name` prop in error messages, useful for display better or formatted names. The generated message won't be updated if this prop changes, you will need to re-validate the input. |
| value | `any` | `undefined` | The field's initial value, optional as long as the field type is not `checkbox` or `radio`. |
| type | `string` | `undefined` | The field type, must be provided if you want your field to behave as a `checkbox` or a `radio` input. |
| unchecked-value | `any` | `undefined` | Only useful when the `type="checkbox"` and the field is a single checkbox field (not bound to an array value). Controls the input's value when it's unchecked. |
| Prop | Type | Required/Default | Description |
| :-------------------- | :----------------------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| as | `string` | `"span"` | The element to render as a root node, defaults to `input` |
| name | `string` | Required | The field's name, must be inside `<Form />` |
| rules | `object \| string \| Function` | `null` | The field's validation rules |
| validateOnMount | `boolean` | `false` | If true, field will be validated when the component is mounted |
| validateOnInput | `boolean` | `false` | If true, field will be validated when `input` event is dispatched/emitted |
| validateOnChange | `boolean` | `true` | If true, field will be validated when `change` event is dispatched/emitted |
| validateOnBlur | `boolean` | `true` | If true, field will be validated when `blur` event is dispatched/emitted |
| validateOnModelUpdate | `boolean` | `true` | If true, field will be validated when `update:modelValue` event is emitted |
| bails | `boolean` | `true` | Stops validating as soon as a rule fails the validation |
| label | `string` | `undefined` | A different string to override the field `name` prop in error messages, useful for display better or formatted names. The generated message won't be updated if this prop changes, you will need to re-validate the input. |
| value | `any` | `undefined` | The field's initial value, optional as long as the field type is not `checkbox` or `radio`. |
| type | `string` | `undefined` | The field type, must be provided if you want your field to behave as a `checkbox` or a `radio` input. |
| unchecked-value | `any` | `undefined` | Only useful when the `type="checkbox"` and the field is a single checkbox field (not bound to an array value). Controls the input's value when it's unchecked. |
| standalone | `boolean` | `false` | Excludes the field from participating in any `Form` or `useForm` contexts, useful for creating inputs that do contribute to the `values` object. In other words, the form won't pick up or validate fields marked as standalone |

### Slots

Expand Down
1 change: 1 addition & 0 deletions docs/content/api/use-field.md
Expand Up @@ -90,6 +90,7 @@ interface FieldOptions {
type?: string; // The input type, can be any string. Toggles specific toggle mode for `checkbox`
checkedValue?: string; // Used the input type is `checkbox` or `radio` otherwise ignored
uncheckedValue?: string; // Used the input type is `checkbox` otherwise ignored
standalone?: boolean; // Excludes the field from participating in any `Form` or `useForm` contexts, useful for creating inputs that do contribute to the `values`, In other words, the form won't pick up or validate fields marked as standalone
}

interface ValidationResult {
Expand Down
6 changes: 6 additions & 0 deletions packages/vee-validate/src/Field.ts
Expand Up @@ -53,6 +53,7 @@ export const Field = defineComponent({
type: Boolean,
default: () => getConfig().bails,
},

label: {
type: String,
default: undefined,
Expand All @@ -73,6 +74,10 @@ export const Field = defineComponent({
type: null as unknown as PropType<((e: any) => unknown) | undefined>,
default: undefined,
},
standalone: {
type: Boolean,
default: false,
},
},
setup(props, ctx) {
const rules = toRef(props, 'rules');
Expand All @@ -98,6 +103,7 @@ export const Field = defineComponent({
} = useField(name, rules, {
validateOnMount: props.validateOnMount,
bails: props.bails,
standalone: props.standalone,
type: ctx.attrs.type as string,
initialValue: resolveInitialValue(props, ctx),
// Only for checkboxes and radio buttons
Expand Down
23 changes: 19 additions & 4 deletions packages/vee-validate/src/useField.ts
Expand Up @@ -50,6 +50,7 @@ interface FieldOptions<TValue = unknown> {
checkedValue?: MaybeRef<TValue>;
uncheckedValue?: MaybeRef<TValue>;
label?: MaybeRef<string | undefined>;
standalone?: boolean;
}

type RuleExpression<TValue> =
Expand All @@ -71,10 +72,20 @@ export function useField<TValue = unknown>(
opts?: Partial<FieldOptions<TValue>>
): FieldComposable<TValue> {
const fid = ID_COUNTER >= Number.MAX_SAFE_INTEGER ? 0 : ++ID_COUNTER;
const { initialValue, validateOnMount, bails, type, checkedValue, label, validateOnValueUpdate, uncheckedValue } =
normalizeOptions(unref(name), opts);
const {
initialValue,
validateOnMount,
bails,
type,
checkedValue,
label,
validateOnValueUpdate,
uncheckedValue,
standalone,
} = normalizeOptions(unref(name), opts);

const form = !standalone ? injectWithSelf(FormContextSymbol) : undefined;

const form = injectWithSelf(FormContextSymbol);
const {
meta,
errors,
Expand All @@ -92,6 +103,7 @@ export function useField<TValue = unknown>(
form,
type,
checkedValue,
standalone,
});

const normalizedRules = computed(() => {
Expand Down Expand Up @@ -293,6 +305,7 @@ function normalizeOptions<TValue>(name: string, opts: Partial<FieldOptions<TValu
rules: '',
label: name,
validateOnValueUpdate: true,
standalone: false,
});

if (!opts) {
Expand All @@ -318,15 +331,17 @@ function useValidationState<TValue>({
form,
type,
checkedValue,
standalone,
}: {
name: MaybeRef<string>;
checkedValue?: MaybeRef<TValue>;
initValue?: MaybeRef<TValue>;
form?: FormContext;
type?: string;
standalone?: boolean;
}) {
const { errors, errorMessage, setErrors } = useFieldErrors(name, form);
const formInitialValues = injectWithSelf(FormInitialValuesSymbol, undefined);
const formInitialValues = standalone ? undefined : injectWithSelf(FormInitialValuesSymbol, undefined);
// clones the ref value to a mutable version
const initialValueRef = ref(unref(initValue)) as Ref<TValue>;

Expand Down
34 changes: 34 additions & 0 deletions packages/vee-validate/tests/Form.spec.ts
Expand Up @@ -2143,4 +2143,38 @@ describe('<Form />', () => {
// field was re-checked
expect(span.textContent).toBe('');
});

test('standalone fields are excluded from form state', async () => {
const wrapper = mountWithHoc({
setup() {
return {};
},
template: `
<VForm v-slot="{ errors, meta }">
<Field name="fname" standalone v-slot="{ errorMessage, field }" rules="required">
<input v-bind="field" />
<span id="fieldError">{{ errorMessage }}</span>
</Field>
<span id="formError">{{ errors.fname }}</span>
<span id="meta">{{ meta.valid }}</span>
</VForm>
`,
});

await flushPromises();
const formError = wrapper.$el.querySelector('#formError');
const fieldError = wrapper.$el.querySelector('#fieldError');
const meta = wrapper.$el.querySelector('#meta');

expect(formError.textContent).toBe('');
expect(fieldError.textContent).toBe('');
expect(meta.textContent).toBe('true');

setValue(wrapper.$el.querySelector('input'), '');
await flushPromises();

expect(formError.textContent).toBe('');
expect(fieldError.textContent).toBe(REQUIRED_MESSAGE);
expect(meta.textContent).toBe('true');
});
});

0 comments on commit 3689437

Please sign in to comment.