Vue.js v-model with dynamic list of radio buttons
Asked Answered
B

4

8

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?

Bugg answered 28/10, 2019 at 12:42 Comment(0)
B
13

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>
Bantam answered 28/10, 2019 at 13:43 Comment(1)
Good answer, I create a version using array/list of object like [ { ... }, ... ], and also one using VuetifyOmeromero
S
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

Superfine answered 28/10, 2019 at 13:24 Comment(0)
O
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>

Orman answered 21/9, 2021 at 4:14 Comment(0)
O
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/[email protected]/css/materialdesignicons.min.css" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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.

Omeromero answered 30/12, 2020 at 0:18 Comment(1)
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.Omnivore

© 2022 - 2024 — McMap. All rights reserved.