All radio inputs in group emit @update:modelValue event even though only input one should

72 Views Asked by At

I was using Histoire to test some components and came across some strange behavior in one of my components.

I have a Vue component called RadioButton. I also have a RadioButtonGroup component. Both use vee-validate. When I use the RadioButtonGroup and click a radio input, only 1 update:modelValue event is fired, which is what I want.

However, when I use the RadioButton component in a v-for loop, all inputs with the same name attribute emit the update:modelValue event. And it would kind of make sense if two events fired, 1 where a radio got checked and one where a radio got unchecked but it doesn't do that. All of the radio inputs with the given name fire the event, this could be 2 or 8.

RadioButton.vue

<template>
    <label
        :for="id"
        class="elab-ui-radio">
        <input
            :id="id"
            type="radio"
            :name="name"
            :value="val"
            ref="radio"
            :checked="modelValue === val"
            :disabled="disabled"
            @click="onClick"
        />
        {{ label }}
    </label>
</template>
<script lang="ts" setup>
import {toRefs} from 'vue';
import {useField} from 'vee-validate';

const props = defineProps({
    label: {
        type: String,
        default: ''
    },
    name: {
        type: String,
        default: ''
    },
    modelValue: {
        type: String
    },
    val: {
        type: String,
        default: ''
    },
    id: {
        type: String,
        default: ''
    },
    disabled: {
        type: Boolean,
        required: false,
        default: false
    },
    rules: {
        type: [String, Object],
        required: false
    },
    validateOnMount: {
        type: Boolean,
        required: false
    }
});

const { name, rules } = toRefs(props);
const { handleChange } = useField(
    name,
    rules,
    {
        validateOnMount: props.validateOnMount
    }
);

const onClick = (value: any) => {
    handleChange(value)
}
</script>

RadioButtonGroup

<template>
    <div class="elab-ui-radio-group">
        <span class="group-label">
            {{ label }}
            <required-indicator v-if="rules"/>
            <ToolTip v-if="note && note.length>0" :content="note">
                <font-awesome-icon icon="fas fa-exclamation-circle" />
            </ToolTip>
        </span>
        <RadioButton
            v-for="(item, key) in items"
            :key="key"
            :label="getItemLabel(item)"
            :val="getItemValue(item)"
            :id="'radio-option-' + getItemLabel(item)"
            :name="name"
            :disabled="disabled"
            v-model="value"
            :rules="rules"
            :validate-on-mount="validateOnMount"/>
        <ErrorMessage :name="name" as="div" class="error-message" />
    </div>
</template>
<script lang="ts" setup>
import {ErrorMessage, useField} from 'vee-validate';
import {toRefs, watch} from 'vue';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
import {library} from '@fortawesome/fontawesome-svg-core';
import {faExclamationCircle} from '@fortawesome/free-solid-svg-icons';
import RequiredIndicator from '../layout/RequiredIndicator.vue';
import RadioButton from '../../../components/forms/input/RadioButton.vue';
import ToolTip from '../../../components/elements/ToolTip.vue';

export interface RadioButtonItem {
    label: string,
    value: any
}

const props = defineProps({
    name: {
        type: String,
        required: true
    },
    label: String,
    modelValue: {
        required: true
    },
    items: {
        type: Array,
        default: () => []
    },
    note: {
        type: String,
        default: '',
        required: false
    },
    dataTest: {
        type: String
    },
    rules: {
        type: [Object, String],
        default: ''
    },
    validateOnMount: {
        type: Boolean,
        default: false
    },
    disabled: {
        type: Boolean,
        default: false
    }
})

library.add(faExclamationCircle);
const { name, rules } = toRefs(props);
const {value, handleChange, errorMessage, meta} = useField(
    name,
    rules,
    {
        validateOnMount: props.validateOnMount
    }
);

const getItemValue = function (item: string | RadioButtonItem): string {
    if (typeof item === 'string') {
        return item;
    }
    return item.value ? item.value : item.label;
};

const getItemLabel = function (item: string | RadioButtonItem): string {
    if (typeof item === 'string') {
        return item;
    }
    return item.label ? item.label : item.value;
};

watch(() => props.modelValue, (newVal) => {
    handleChange(newVal);
});

const onChange = (value: any) => {
    handleChange(value);
};
</script>

The way i'm testing these components is in Histoire. The state variable is a function that returns the following object.

function radioButtonGroupInitState() {
    return {
        label: 'Click for memory check',
        options: [
            {label: 'HDD', value: '2TB'},
            {label: 'SSD', value: '512GB'},
            {label: 'RAM', value: '16GB'},
            {label: 'Floppy', value: '1MB'}
        ],
        modelValue: '',
        required: true,
        validateOnMount: true,
        disabled: false,
        note: ''
    }
}

Using RadioButtonGroup

<Form>
    <RadioButtonGroup
        v-model="state.modelValue"
        :items="state.options"
        :label="state.label"
        name="radioButtonGroup"
        :note="state.note"
        :disabled="state.disabled"
        :rules="state.required ? 'required' : ''"
        :validate-on-mount="state.validateOnMount"
        @update:modelValue="logEvent('Radio changed', $event)"/>
    Modelled value: {{ state.modelValue }}
</Form>

Clicking input in RadioButtonGroup

enter image description here

Using RadioButton with v-for

<Form class="custom-radio-form">
    <h4>{{state.label}}</h4>
    <div
        class="custom-radio-wrapper"
        v-for="(option, key) in state.options"
        :key="key" >
        <RadioButton
            :label="option.label"
            :val="option.value"
            :id="'radio-option-' + option.label"
            name="custom-radio-group"
            :disabled="state.disabled"
            v-model="state.modelValue"
            :rules="state.required ? 'required' : ''"
            :validate-on-mount="state.validateOnMount"
            @update:modelValue="logEvent('option-'+option.label, $event)"/>
    </div>
    <ErrorMessage name="custom-radio-group" as="div"/>
    <p>Model value: {{ state.modelValue }}</p>
</Form>

Clicking RadioButton with v-for

enter image description here

I have a feeling it has something to do with vee-validate in the RadioButton component and the handleChange function firing in each RadioButton instance and that causing all components to think that they must update the model value.

1

There are 1 best solutions below

0
On

So it seemed I was right. The vee-validate function in RadioButton.vue were causing the issue. Each time a change occurred in the radio inputs the handleChange function was called, which in turn emitted the update:modelValue event. The way I fixed that was removing vee-validate from the RadioButton component and only using it in the components or other code that implemented the RadioButton component.

I only really changed the script part as seen below:

<script lang="ts" setup>

const props = defineProps({
    label: {
        type: String,
        default: ''
    },
    name: {
        type: String,
        default: ''
    },
    modelValue: {
        type: String
    },
    val: {
        type: String,
        default: ''
    },
    id: {
        type: String,
        default: ''
    },
    disabled: {
        type: Boolean,
        required: false,
        default: false
    },
    rules: {
        type: [String, Object],
        required: false
    },
    validateOnMount: {
        type: Boolean,
        required: false
    }
});

const emit = defineEmits(['update:modelValue']);

const onClick = (event: any) => {
    emit('update:modelValue', event.target.value);
}
</script>

Now instead of using handleChange I just emit the value of the radio button that was clicked.