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 standalone prop for fields #3379

Merged
merged 2 commits into from Jul 10, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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');
});
});