7

I am trying to make a reuseable vue radio-button component that will take a variable name, and an object containing labels and values, then render a list of radio buttons using v-for.

I have had success with each half of the problem, but have not managed to combine them:

  1. I can make a set of radio buttons bound to the data model, where the buttons are defined statically in the template, but I don't know how to make the list dynamic. Here is the code for that:

//component
const Radio = {
 template: '#test',
  prop: ['value'],
  data () {
    return {
      selected: this.value
    }
  },
  model: {
   prop: 'value',
      event: 'change'
  },
  methods: {
    handleClickInput (e) {
      this.$emit('change', this.selected)
    }
  }
}

//app
var app2 = new Vue({
  el: '#app2',
  data: {  
    door: '',
   doorOptions: {
    'Yes': 1,
    'No': 0,
   }
 },
  components: { Radio, }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app2">
  <radio v-model="door"></radio>
  <p>
    door = {{door}}
  </p>
</div>


<template id="test">
  <div>
    <input type="radio" value="0" v-model="selected" @change="handleClickInput">0
    <input type="radio" value="1" v-model="selected" @change="handleClickInput">1
  </div>
</template>
  1. I can make a dynamic list of radio buttons based on an "options" object, but can't find a way to bind them to the data model. Here is the code for that:

// component
Vue.component('radio-set', {
  template: '#radio-set',
  props: {
   'label-name': '',
   'variable': '',
   'options': '',
  },
  methods: {
    clicked: function(variable, key, value) {
    // none of this is right, it doesn't update the vue data model
  window[variable] = value; //assign the new value to the dynamic variable name
  selected = value;
  this.$emit("click-event", variable) //create the click event for model updating by the parent
    }
  },
})

//app
var app = new Vue({
  el: '#vueApp',
  data: {
    door:'initial value',
  doorOptions: {
    'Yes':1,
    'No':0,
      'Maybe':5,
      'A new option':25
  },
  
  },
  methods: {
  buttonClick: function(p1){
   console.log(p1+': '+window[p1]); //the variable was assigned inside the child component
  }
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="vueApp">
 <radio-set 
  label-name="Radio button set" 
  variable="door" 
  :options="doorOptions"
  @click-event="buttonClick" 
 >door: {{door}}
 </radio-set>
</div>

<template id="radio-set">
 <div>
  <label>{{labelName}}:</label>
  <button 
      type="button" 
      v-for="(val, key) in options"
      @click="clicked(variable, key, val)" 
      >
      {{ key }}
  </button>
  </div>
</template>

Could anyone help with a couple of pointers on how I could move forwards?

Tom
  • 364
  • 1
  • 4
  • 13

4 Answers4

12

As @PierreSaid mentioned, you can read more about v-model usage on custom componet.

This is an other example to use input[type="radio"] and emit change event back to parent componet.

// component
Vue.component('radio-set', {
  template: '#radio-set',
  props: {
    'label-name': '',
    'value': '',
    'options': '',
  }
})

//app
var app = new Vue({
  el: '#vueApp',
  data() {
    return {
      door: null,
      doorOptions: {
        'Yes': 1,
        'No': 0,
        'Maybe': 5,
        'A new option': 25
      }
    };
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="vueApp">
  <radio-set label-name="Radio button set" v-model="door" :options="doorOptions"></radio-set>
  door: {{door}}
</div>

<template id="radio-set">
  <div>
    <div>{{labelName}}:</div>
    <label v-for="(val, key) in options" :key="val">
      <input type="radio" 
        :name="labelName" 
        :value="val" 
        :checked="val == value" 
        @change="$emit('input', val)"> 
      {{ key }}
    </label>
  </div>
</template>
Ja9ad335h
  • 4,995
  • 2
  • 21
  • 29
  • Good answer, I create a version using array/list of object like `[ { ... }, ... ]`, and also one using Vuetify – Adam Cox Dec 30 '20 at 03:25
2

First of all : For your options it would be easier to have an array.

      doorOptions: [
        { key: "Yes", value: 1 },
        { key: "No", value: 0 },
        { key: "Maybe", value: 5 },
        { key: "A new option", value: 25 }
      ]
    };

That way you can iterate over it.

Also a good way to synchronise the selected value between your custom component and your app would be to use v-model.

A tutorial to implement v-model

That way we can create a reusable component like that :

<template>
  <div>
    <label>{{labelName}}:</label>
    <button
      type="button"
      v-for="(val, idx) in options"
      :key="idx"
      @click="clicked(val)"
    >{{ val.key }}</button>
  </div>
</template>

<script>
export default {
  props: ["value", "options", "labelName"],
  methods: {
    clicked(val) {
      this.$emit("input", val);
    }
  }
};
</script>

And use it like this

<template>
  <div id="app">
    <radio-set v-model="selected" label-name="Radio button set" :options="doorOptions"/>
    Selected : {{selected.key}}
  </div>
</template>

<script>
import RadioSet from "./components/RadioSet";

export default {
  name: "App",
  components: {
    RadioSet
  },
  data() {
    return {
      selected: null,
      doorOptions: [
        { key: "Yes", value: 1 },
        { key: "No", value: 0 },
        { key: "Maybe", value: 5 },
        { key: "A new option", value: 25 }
      ]
    };
  }
};
</script>

Live demo

tony19
  • 125,647
  • 18
  • 229
  • 307
Pierre Said
  • 3,660
  • 1
  • 16
  • 28
1

Based on one of the other answers, I ended up with this for Vue 3. There are some breaking changes with how v-model works, namely in the props names. (https://v3-migration.vuejs.org/breaking-changes/v-model.html)

Styling is rudimentary, but I wanted something to indicate what the active selection was.

<template>
  <div>
    <label>{{ labelName }}:</label>
    <button
      :class="val.key === modelValue ? 'active' : 'inactive'"
      type="button"
      v-for="(val, idx) in options"
      :key="idx"
      @click="clicked(val)"
    >
      {{ val.key }}
    </button>
  </div>
</template>

<script>
export default {
  props: ["modelValue", "options", "labelName"],
  methods: {
    clicked(val) {
      console.log("emitting click", val.key);
      this.$emit("update:modelValue", val.key);
    },
  }
};
</script>

<style scoped>
button {
  padding: 10px;
  margin: 5px;
}
.active {
  background-color: #42b983;
}
.inactive {
  background-color: lightgray;
}
</style>

It gets used in the same way. I just map a basic array to {key, value} since that's all I need, but it would be easy to do something more explicit.

<template>
    <radio-set
      label-name="On / Off"
      v-model="myValue"
      :options="options.radioOptions"
    ></radio-set>
</template>


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

let options = {
  radioOptions: ["on", "off"].map((x) => {
    return { key: x, val: x };
  }),
};

export default {
  name: "MyComponent",
  components: {
    RadioSet,
  },
  data: () => {
    return {
      options,
      myValue: "unknown",
    };
  },
};
</script>

tony19
  • 125,647
  • 18
  • 229
  • 307
emragins
  • 4,607
  • 2
  • 33
  • 48
0

While I studied and toyed with these answers, I came up with the following methods:

  1. Preferred method of using array of objects and Vuetify:

new Vue({
    el: '#app',
    vuetify: new Vuetify(),
    data: () => ({
      color: {
        label: "Choose:",
        // Array List
        items: [
          { key: 1, value: "Red", color: "red" },
          { key: 2, value: "Green", color: "green" },
          { key: 3, value: "Blue", color: "blue" }
        ],
        selectedItemKey: 2 // Red
      }
    }),
    computed: {
      selectedColorItem() {
        return this.color.items.find(item => item.key===this.color.selectedItemKey)
      }
    }
  })
<div id="app">
  <v-app id="inspire">
    <v-container fluid mx-4>
          <v-radio-group
            :label="color.label"
            row
            v-model="color.selectedItemKey"
            >
            <v-radio
              :color="item.color"
              :key="item.key"
              :label="item.value"
              :value="item.key"
              v-for="(item, index) in color.items"
              ></v-radio>
          </v-radio-group>
          Color: <span :class=`${selectedColorItem?.color}--text`>
            {{selectedColorItem?.value}}
          </span>(key: {{color.selectedItemKey}})
    </v-container>
  </v-app>
</div>

<link href="https://cdn.jsdelivr.net/npm/@mdi/font@3.x/css/materialdesignicons.min.css" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.0.11/dist/vuetify.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
  1. Array list & <label>) - codepen

  2. Object list & <Label>) - codepen

  3. I took a look at using buttons like in @Pierre Said answer, but seems I would need to use a bunch of CSS to make a button appear like a radio-button.

Adam Cox
  • 3,341
  • 1
  • 36
  • 46
  • Both Vuetify and BootstrapVue offer flexible radio group components. It makes sense to use them if your project already uses one of the libs. But this question is about creating a reusable radio-group component from scratch. Loading a lib the size of Vuetify for its radio buttons alone doesn't make much sense. Other than that, looking at the other two options you list, they seem to be plagiarisms of the accepted answer. When you "tweak" and re-post an existing solution you should properly attribute it and clarify precisely your contribution to the modified version, if any. – tao Dec 30 '20 at 03:29