Skip to content

Commit

Permalink
feat: added value change support for native multi select (#3146)
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm authored Feb 1, 2021
1 parent 1284c7a commit 0601586
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 7 deletions.
21 changes: 21 additions & 0 deletions docs/content/api/field.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ For example you could render a `select` input like this:
</Field>
```

<doc-tip>

The `Field` component has partial support for native `select[multiple]` element, while it picks up the multiple values correctly, it doesn't set the initial values UI state on the element itself. You may use `v-model` here or bind the `selected` attributes on the options which is straightforward with the `value` prop exposed on the slot props.

```vue
<Field v-slot="{ value }" name="drink" as="select" multiple>
<option value="" disabled>Select a drink</option>
<option v-for="drink in drinks" :key="drink" :value="drink" :selected="value && value.includes(drink)">{{ drink }}</option>
</Field>
```

</doc-tip>

You can also render any globally defined components:

```vue
Expand Down Expand Up @@ -168,6 +181,14 @@ An array containing all error messages for the field.

<code-title level="4">

`value: unknown`

</code-title>

The current value of the field, useful to compare and do conditional rendering based on the field value. **You should not use it as a target of `v-model` or `:value` binding**. Instead use the `field` prop.

<code-title level="4">

`errorMessage: ComputedRef<string | undefined>`

</code-title>
Expand Down
6 changes: 4 additions & 2 deletions packages/vee-validate/src/Field.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { h, defineComponent, toRef, SetupContext, resolveDynamicComponent, computed, watch } from 'vue';
import { getConfig } from './config';
import { useField } from './useField';
import { normalizeChildren, hasCheckedAttr, isFileInput } from './utils';
import { normalizeChildren, hasCheckedAttr, shouldHaveValueBinding } from './utils';

interface ValidationTriggersProps {
validateOnMount: boolean;
Expand Down Expand Up @@ -148,7 +148,8 @@ export const Field = defineComponent({
attrs.value = value.value;
}

if (isFileInput(resolveTag(props, ctx), ctx.attrs.type as string)) {
const tag = resolveTag(props, ctx);
if (shouldHaveValueBinding(tag, ctx.attrs)) {
delete attrs.value;
}

Expand All @@ -158,6 +159,7 @@ export const Field = defineComponent({
const slotProps = computed(() => {
return {
field: fieldProps.value,
value: value.value,
meta,
errors: errors.value,
errorMessage: errorMessage.value,
Expand Down
33 changes: 31 additions & 2 deletions packages/vee-validate/src/utils/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export function isHTMLTag(tag: string) {
/**
* Checks if an input is of type file
*/
export function isFileInput(tag: string, type: string) {
return isHTMLTag(tag) && type === 'file';
export function isFileInputNode(tag: string, attrs: Record<string, unknown>) {
return isHTMLTag(tag) && attrs.type === 'file';
}

type YupValidator = { validate: (value: any) => Promise<void | boolean> };
Expand Down Expand Up @@ -50,3 +50,32 @@ export function isEmptyContainer(value: unknown): boolean {
export function isNotNestedPath(path: string) {
return /^\[.+\]$/i.test(path);
}

/**
* Checks if an element is a native HTML5 multi-select input element
*/
export function isNativeMultiSelect(el: HTMLElement): el is HTMLSelectElement {
return el.tagName === 'SELECT' && (el as HTMLSelectElement).multiple;
}

/**
* Checks if a tag name with attrs object will render a native multi-select element
*/
export function isNativeMultiSelectNode(tag: string, attrs: Record<string, unknown>) {
// The falsy value array is the values that Vue won't add the `multiple` prop if it has one of these values
const hasTruthyBindingValue =
![false, null, undefined, 0].includes(attrs.multiple as boolean) && !Number.isNaN(attrs.multiple);

return tag === 'select' && 'multiple' in attrs && hasTruthyBindingValue;
}

/**
* Checks if a node should have a `:value` binding or not
*
* These nodes should not have a value binding
* For files, because they are not reactive
* For multi-selects because the value binding will reset the value
*/
export function shouldHaveValueBinding(tag: string, attrs: Record<string, unknown>) {
return isNativeMultiSelectNode(tag, attrs) || isFileInputNode(tag, attrs);
}
13 changes: 10 additions & 3 deletions packages/vee-validate/src/utils/events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isCallable } from '../../../shared';
import { hasCheckedAttr } from './assertions';
import { hasCheckedAttr, isNativeMultiSelect } from './assertions';
import { getBoundValue, hasValueBinding } from './vnode';

export const isEvent = (evt: any): evt is Event => {
if (!evt) {
Expand Down Expand Up @@ -27,13 +28,19 @@ export function normalizeEventValue(value: unknown): any {
const input = value.target as HTMLInputElement;
// Vue sets the current bound value on `_value` prop
// for checkboxes it it should fetch the value binding type as is (boolean instead of string)
if (hasCheckedAttr(input.type) && '_value' in input) {
return (input as any)._value;
if (hasCheckedAttr(input.type) && hasValueBinding(input)) {
return getBoundValue(input);
}

if (input.type === 'file' && input.files) {
return Array.from(input.files);
}

if (isNativeMultiSelect(input)) {
return Array.from(input.options)
.filter(opt => opt.selected && !opt.disabled)
.map(getBoundValue);
}

return input.value;
}
22 changes: 22 additions & 0 deletions packages/vee-validate/src/utils/vnode.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
import { SetupContext } from 'vue';

type HTMLElementWithValueBinding = HTMLElement & { _value: unknown };

export const normalizeChildren = (context: SetupContext<any>, slotProps: any) => {
if (!context.slots.default) {
return context.slots.default;
}

return context.slots.default(slotProps);
};

/**
* Vue adds a `_value` prop at the moment on the input elements to store the REAL value on them, real values are different than the `value` attribute
* as they do not get casted to strings unlike `el.value` which preserves user-code behavior
*/
export function getBoundValue(el: HTMLElement): unknown {
if (hasValueBinding(el)) {
return el._value;
}

return undefined;
}

/**
* Vue adds a `_value` prop at the moment on the input elements to store the REAL value on them, real values are different than the `value` attribute
* as they do not get casted to strings unlike `el.value` which preserves user-code behavior
*/
export function hasValueBinding(el: HTMLElement): el is HTMLElementWithValueBinding {
return '_value' in el;
}

0 comments on commit 0601586

Please sign in to comment.