1
votes

I am getting some strange behaviour that I cannot wrap my head around.

I have a simple radio button component that's used as a "wrapper" for an actual radio button.

On this component, I have inheritAttrs: false and use v-bind="$attrs" on the element itself so I can use v-model and value etc.

However, upon selecting a radio button, an error is thrown that the prop value is invalid (because it's an event and not a string) and interestingly I noticed that on initial render the value prop is blank in Vue Devtools.

I'm simply trying to get these radio buttons updating the parent's data object value for location with a string value of the radio button selected.

I can't figure out where I'm going wrong here exactly. Any help greatly appreciated.

Example project of the problem: https://codesandbox.io/embed/m40y6y10mx

FormMain.vue

<template>
  <div>
    <p>Location: {{ location }}</p>
    <form-radio
      id="location-chicago"
      v-model="location"
      value="Chicago"
      name="location"
      label="Chicago"
      @change="changed"
    />
    <form-radio
      id="location-london"
      v-model="location"
      value="London"
      name="location"
      label="London"
      @change="changed"
    />
  </div>
</template>

<script>
import FormRadio from "./FormRadio.vue";

export default {
  name: "FormMain",
  components: {
    FormRadio
  },
  data() {
    return {
      location: ""
    };
  },
  methods: {
    changed(e) {
      console.log("Change handler says...");
      console.log(e);
    }
  }
};
</script>

FormRadio.vue

<template>
  <div>
    <label :for="id">
      {{ label }}
      <input
        :id="id"
        type="radio"
        :value="value"
        v-on="listeners"
        v-bind="$attrs"
      >
    </label>
  </div>
</template>

<script>
export default {
  name: "FormRadio",
  inheritAttrs: false,
  props: {
    id: {
      type: String,
      required: true
    },
    label: {
      type: String,
      required: true
    },
    value: {
      type: String,
      required: true
    }
  },
  computed: {
    listeners() {
      return {
        ...this.$listeners,
        change: event => {
          console.log("Change event says...");
          console.log(event.target.value);
          this.$emit("change", event.target.value);
        }
      };
    }
  }
};
</script>
2
It's odd that you're setting both v-model and value - v-model should be the source of truth for the valueDerek Pollard
But then how would I specify that each radio button has its own value to set? How else would you do that?Michael Giovanni Pumo
Gonna build an example answer...Derek Pollard
Thank you. If you could utilise the Codesandbox that would be great.Michael Giovanni Pumo
It's a bit confusing how you're trying to use the same v-model on 2 different elements, since it would dictate the value for both elementsDerek Pollard

2 Answers

2
votes

Edit

Found this neat article which describes the model property of a component. Basically it allows you to customise how v-model works. Using this, FormMain.vue would not have to change. Simply remove the value prop from FormRadio and add the model property with your own definition

See updated codepen:

FormRadio Script

<script>
export default {
  name: "FormRadio",
  inheritAttrs: false,
  props: {
    id: {
      type: String,
      required: true
    },
    label: {
      type: String,
      required: true
    }
  },
  // customize the event/prop pair accepted by v-model
  model: {
    prop: "radioModel",
    event: "radio-select"
  },
  computed: {
    listeners() {
      return {
        ...this.$listeners,
        change: event => {
          console.log("Change event says...");
          console.log(event.target.value);
          // emit the custom event to update the v-model value
          this.$emit("radio-select", event.target.value);
          // the change event that the parent was listening for
          this.$emit("change", event.target.value);
        }
      };
    }
  }
};
</script>

Before Edit:

Vue seems to ignore the value binding attribute if v-model is present. I got around this by using a custom attribute for the value like radio-value.

FormMain.vue

<form-radio
  id="location-chicago"
  v-model="location"
  radio-value="Chicago"
  name="location"
  label="Chicago"
  @change="changed"
/>
<form-radio
  id="location-london"
  v-model="location"
  radio-value="London"
  name="location"
  label="London"
  @change="changed"
/>

The input event handler will update the v-model.

FormRadio.vue

<template>
  <div>
    <label :for="(id) ? `field-${id}` : false">
      {{ label }}
      <input
        :id="`field-${id}`"
        type="radio"
        :value="radioValue"
        v-on="listeners"
        v-bind="$attrs"
      >
    </label>
  </div>
</template>

<script>
export default {
  name: "FormRadio",
  inheritAttrs: false,
  props: {
    id: {
      type: String,
      required: true
    },
    label: {
      type: String,
      required: true
    },
    radioValue: {
      type: String,
      required: true
    }
  },
  computed: {
    listeners() {
      return {
        ...this.$listeners,
        input: event => {
          console.log("input event says...");
          console.log(event.target.value);
          this.$emit("input", event.target.value);
        },
        change: event => {
          console.log("Change event says...");
          console.log(event.target.value);
          this.$emit("change", event.target.value);
        }
      };
    }
  }
};
</script>

See forked codepen

1
votes

I removed the v-model entirely from the child component call (this was conflicting);

<form-radio
  id="location-chicago"
  value="Chicago"
  name="location"
  label="Chicago"
  @change="changed"
/>
<form-radio
  id="location-london"
  value="London"
  name="location"
  label="London"
  @change="changed"
/>

I then updated your changed method to include to set the location variable

methods: {
  changed(e) {
    console.log("Change handler says...");
    console.log(e);
    this.location = e;
  }
}

Updated: Link to updated CodeSandbox