Vue.JS radio input without v-model - vue.js

I have a component that takes an array of options objects, a name, and a v-model, and puts out a radio input with matching label for each of the options in that array of options.
options = [
{text: "Option 1", value="option-1" },
{text: "Option 2", value="option-2" },
]
"updateValue" : function(){
this.$emit('input', this.$refs.input.value);
}
<template v-for="option in options" >
<input
:value="option.value"
:id="name + option.value"
:name="name"
#input="updateValue()"
ref="input" />
<label :for="name + option.value">{{option.text}}</label>
</template>
I've found a similar pattern in this guide where a v-model is taken and then value emitted by watching the #input event.
I cannot wrap my head around how to do this with a radio-type input. Setting :value="value" as with say a text input would make all the radio values identical, which really doesn't work.

Here's an example of a really primitive radio button component that uses v-model to update data when an entry is clicked:
const radio = {
props: ['option', 'value'],
template: `<div #click="onClick">{{ option === value ? 'o' : 'x' }}<slot /></div>`,
methods: {
onClick () {
this.$emit('input', this.option)
}
}
}
new Vue({
el: '#app',
components: {
radio
},
data () {
return {
options: ['red', 'green', 'blue'],
selectedOption: 'red'
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<radio v-for="option in options"
v-model="selectedOption"
:option="option"
>
{{ option }}
</radio>
</div>
With this implementation each radio button is passed both the currently selected value (via v-model) as well as the value for that particular option (via option). It uses === inside each radio button to determine whether it is the currently selected option.
Vue's support for native radio button's is described here:
https://v2.vuejs.org/v2/guide/forms.html#Radio
It is effectively the same as my example except that what I've called option it calls value. Don't be confused by the way it uses both v-model and value. While v-model binds to value and input by default it can actually be bound to any prop and event (see https://v2.vuejs.org/v2/api/#model). In the case of native checkboxes and radio buttons it binds to the checked property and the change event (mentioned in https://v2.vuejs.org/v2/guide/forms.html#Basic-Usage).
Update:
I think I may have misunderstood your question. My answer provides relevant background but it may be backwards for what you want. v-model just expands to a prop and an event, so if you want to replace v-model with something else you just need to find out which prop and event it's using and use those directly instead.

Related

Checkbox renders false but the data and DOM attribute is true

I want to create a checkbox group component. Currently there is no native solution so I tried to create one on my own
<template>
<v-container fluid>
<v-checkbox
v-for="(groupItem, index) in groupItems"
:key="index"
:label="groupItem.display"
:value="groupItem.value"
#change="onCheckboxUpdated(index)"
></v-checkbox>
</v-container>
</template>
<script>
export default {
props: {
groupItems: {
type: Array,
required: true
}
},
methods: {
onCheckboxUpdated: function(index) {
this.groupItems[index].value = !this.groupItems[index].value;
this.$emit("checkboxGroupUpdated", this.groupItems);
}
}
};
</script>
This component should render a specific amount of checkboxes and fire an event with all the updated values.
When I pass in these values
values: [
{
display: "Read permissions",
value: true
},
{
display: "Write permissions",
value: false
},
{
display: "Delete permissions",
value: false
}
]
the first generated checkbox renders a false state although the DOM element is set to true
When toggling the checkbox it will work fine the next time.
I created an example to reproduce the problem
https://codesandbox.io/s/checkboxgroup-l8gcg
If you want to expand the v-model of a v-checkbox you'll need to use input-value as the prop rather than value.
:input-value="groupItem.value"
See:
https://github.com/vuetifyjs/vuetify/blob/27d5fdd32dc7c8a9af38f823d1574d92b211d405/packages/vuetify/src/mixins/selectable.js#L14
That's a mixin that's included in v-checkbox.
While input-value is documented, https://vuetifyjs.com/en/components/selection-controls#api, you do have to dig a bit to find it.
Unrelated to your problem, your current implementation of a checkbox group violates one-way data flow. You are mutating an object that has been passed via a prop.
https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
I'll leave it to you to decide whether you care.
It is because Vue.js ignores the value of inputs. You need to use v-model instead.
<v-checkbox
v-for="(groupItem, index) in groupItems"
:key="index"
:label="groupItem.display"
v-model="groupItem.value"
#change="onCheckboxUpdated(index)"
></v-checkbox>
From the documentation https://v2.vuejs.org/v2/guide/forms.html
v-model will ignore the initial value, checked or selected attributes found on any form elements. It will always treat the Vue instance data as the source of truth. You should declare the initial value on the JavaScript side, inside the data option of your component.

How am I suppose to get the value of a checkbox?

So I have the following element:
<input v-on:click="$emit('addPartId', $event)" v-bind:value="13209" name="add_13209" type="checkbox">
Which then calls the following method:
methods: {
....
addPartId(evnt) {
console.log(evnt);
}
},
In the parent container and is passed to the child:
<table-body
v-bind:items="items"
v-bind:columns="columns"
v-bind:sort-column="sortColumn"
v-bind:direction="direction"
#sort="sort"
#addPartId="addPartId"
>
</table-body>
The question I have, that I can't find on stack, is how do I register a click event so that when the checkbox is clicked I get the event object (I want the value, from v-bind:value, of the checkbox.
You should use event name which is the kebab-cased version, check Vue Guide: Custom Event,
As the guide says:
Unlike components and props, event names will never be used as
variable or property names in JavaScript, so there’s no reason to use
camelCase or PascalCase. Additionally, v-on event listeners inside DOM
templates will be automatically transformed to lowercase (due to
HTML’s case-insensitivity), so v-on:myEvent would become v-on:myevent
– making myEvent impossible to listen to.
For these reasons, we recommend you always use kebab-case for event
names.
Vue.component('my-checkbox', {
template: `
<input v-on:click="$emit('add-part-id', {'whole': $event, 'value':13209})" v-bind:value="13209" name="add_13209" type="checkbox">
`
})
Vue.component('my-another-checkbox', {
template: `
<input v-on:click="$emit('add-part-id', $event)" v-bind:value="13209" name="add_13209" type="checkbox">
`
})
new Vue({
el: '#emit-example-simple',
methods: {
getChecked1: function (ev) {
console.log('checkbox1', ev.value)
console.log('checkbox1', ev.whole.target.value)
},
getChecked2: function (ev) {
console.log('checkbox2', ev.target.value)
}
}
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="emit-example-simple">
First Example: <my-checkbox #add-part-id="getChecked1"></my-checkbox>
Second Example: <my-another-checkbox #add-part-id="getChecked2"></my-another-checkbox>
</div>

Modify props.value from within child component

I am new to Vue and trying to build a "dropdown" component. I want to use it from a parent component like this:
<my-dropdown v-model="selection"></my-dropdown>
where selection is stored as data on the parent, and should be updated to reflect the user's selection. To do this I believe my dropdown component needs a value prop and it needs to emit input events when the selection changes.
However, I also want to modify the value from within the child itself, because I want to be able to use the dropdown component on its own (I need to modify it because otherwise the UI will not update to reflect the newly selected value if the component is used on its own).
Is there a way I can bind with v-model as above, but also modify the value from within the child (it seems I can't, because value is a prop and the child can't modify its own props).
You need to have a computed property proxy for a local value that handles the input/value values.
props: {
value: {
required: true,
}
},
computed: {
mySelection: {
get() {
return this.value;
},
set(v) {
this.$emit('input', v)
}
}
}
Now you can set your template to use the mySelection value for managing your data inside this component and as it changes, the data is emitted correctly and is always in sync with the v-model (selected) when you use it in the parent.
Vue's philosophy is: "props down, events up". It even says this in the documentation: Composing Components.
Components are meant to be used together, most commonly in
parent-child relationships: component A may use component B in its own
template. They inevitably need to communicate to one another: the
parent may need to pass data down to the child, and the child may need
to inform the parent of something that happened in the child. However,
it is also very important to keep the parent and the child as
decoupled as possible via a clearly-defined interface. This ensures
each component’s code can be written and reasoned about in relative
isolation, thus making them more maintainable and potentially easier
to reuse.
In Vue, the parent-child component relationship can be summarized as
props down, events up. The parent passes data down to the child via
props, and the child sends messages to the parent via events. Let’s
see how they work next.
Don't try to modify the value from within the child component. Tell the parent component about something and, if the parent cares, it can do something about it. Having the child component change things gives too much responsibility to the child.
You could use the following pattern:
Accept 1 input prop
Have another variable inside your data
On creation, white the incoming prop into your data variable
Using a watcher, watch the incoming prop for changes
On a change inside your component, send the change away
Demo:
'use strict';
Vue.component('prop-test', {
template: "#prop-test-template",
props: {
value: { // value is the default prop used by v-model
required: true,
type: String,
},
},
data() {
return {
dataObject: undefined,
// For testing purposes:
receiveData: true,
sendData: true,
};
},
created() {
this.dataObject = this.value;
},
watch: {
value() {
// `If` is only here for testing purposes
if(this.receiveData)
this.dataObject = this.value;
},
dataObject() {
// `If` is only here for testing purposes
if(this.sendData)
this.$emit('input', this.dataObject);
},
},
});
var app = new Vue({
el: '#app',
data: {
test: 'c',
},
});
<script src="https://unpkg.com/vue#2.0.1/dist/vue.js"></script>
<script type="text/x-template" id="prop-test-template">
<fieldset>
<select v-model="dataObject">
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
<option value="d">d</option>
<option value="e">e</option>
<option value="f">f</option>
<option value="g">g</option>
<option value="h">h</option>
<option value="i">i</option>
<option value="j">j</option>
</select>
<!-- For testing purposed only: -->
<br>
<label>
<input type="checkbox" v-model="receiveData">
Receive updates
</label>
<br>
<label>
<input type="checkbox" v-model="sendData">
Send updates
</label>
<!--/ For testing purposed only: -->
</fieldset>
</script>
<div id="app">
<prop-test v-model="test"></prop-test>
<prop-test v-model="test"></prop-test>
<prop-test v-model="test"></prop-test>
</div>
Notice that this demo has a feature that you can turn off the propagation of the events per select box, so you can test if the values are properly updated locally, this is of course not needed for production.
If you want to modify a prop inside a component, I recommend passing a "default value" prop to your component. Here is how I would do that
<MyDropdownComponent
:default="defaultValue"
:options="options"
v-model="defaultValue"
/>
And then there are 2 options to how I would go from there -
Option 1 - custom dropdown element
As you're using custom HTML, you won't be able to set a selected attribute. So you'll need to be creative about your methods.
Inside your component, you can set the default value prop to a data attribute on component creation. You may want to do this differently, and use a watcher instead. I've added that to the example below.
export default {
...
data() {
return {
selected: '',
};
},
created() {
this.selected = this.default;
},
methods: {
// This would be fired on change of your dropdown.
// You'll have to pass the `option` as a param here,
// So that you can send that data back somehow
myChangeMethod(option) {
this.$emit('input', option);
},
},
watch: {
default() {
this.selected = this.default;
},
},
};
The $emit will pass the data back to the parent component, which won't have been modified. You won't need to do this if you're using a standard select element.
Option 2 - Standard select
<template>
<select
v-if="options.length > 1"
v-model="value"
#change="myChangeMethod"
>
<option
v-for="(option, index) of options"
:key="option.name"
:value="option"
:selected="option === default"
>{{ option.name }}</option>
</select>
</template>
<script>
export default {
...
data() {
return {
value: '',
};
},
methods: {
// This would be fired on change of your dropdown
myChangeMethod() {
this.$emit('input', this.value);
},
},
};
</script>
This method is definitely the easiest, and means you need to use the default select element.
You could use a custom form input component
Form Input Components using Custom Events
Basically your custom component should accept a value prop and emit input event when value changes

Vue.js Input value gets deleted on first char only

Based on the official example Custom Input Component example:
HTML:
<div id="app" >
<div :class="{'has-value' : hasValue}">
<input type="text"
ref="input"
v-bind:value="value"
#input="onInput" >
</div>
</div>
JS:
new Vue({
el: '#app',
data: function() {
return {
hasValue: false
}
},
props: ['value'],
methods:{
onInput: function(){
val= this.$refs.input.value
if(val && val.length > 0){
this.hasValue = true
} else {
this.hasValue = false
}
}
}
})
JS Fiddle to play
Expected:
Dynamically set hasValue data for input and bind a Css Class to this data
Unexpected:
Only after initialization and typing the first Character, the typed character gets deleted from the input. Press JS-Fiddle->Run to see it again.
After first character everything works as expected.
If I remove v-bind:value="value" or comment \\this.hasValue = true it works as expected. Binding value prop is recommended for custom Input component.
Is this intended and why? Or is this a bug I should report?
This is expected behaviour:
When the first character is inserted, hasValue is changed from false to true, causing the component to re-render
During re-render, Vue sees that the input has a value (the character you just typed in), but the property you have bound to it (the value prop) is empty.
Therefore, Vue updates the input to match the bound prop - and thus, it empties the input.
After that, hasValue doesn't change anymore, so there's no re-rendering happening, and thus, Vue doesn't reset the input field's value anymore.
So how to fix this?
First you have to understand that Vue is data-driven - the data determines the HTML, not the other way around. So if you want your input to have a value, you have to reflect that value in the property bound to it.
This would be trivial if you worked with a local data property:
https://jsfiddle.net/Linusborg/jwok2jsx/2/
But since you used a prop, and I assume you want to continue using it, the sittuation is a bit different - Vue follows a pattern of "data down - events up". You can't change props from within a child, you have to tell the parent from which you got it that you want to change it with an event. The responsibility to change it would be the parent's. Here's your example with a proper parent-child relationship:
https://jsfiddle.net/Linusborg/jwok2jsx/4/
I'm guessing that when you modified hasValue, Vue re-renders the component (to apply the has-value class to the div) which causes the v-bind:value to be re-evaluated. value is a prop which is unassigned and never changes, so the value of the input element gets cleared.
You really should use v-model on the input element and control has-value based on the value in the model, rather than interacting directly with the DOM. (You won't be able to use v-model with value though since value is a prop and cannot be assigned from within the component.)
I'm also guessing you want to make a custom input component which applies a certain style when it has a value. Try this:
Vue.component('my-input', {
props: ['value'],
template: `<input class="my-input" :class="{ 'has-value': value }" type="text" :value="value" #input="$emit('input', $event.target.value)">`,
});
new Vue({
el: '#app',
data: {
value: '',
},
});
.my-input.has-value {
background-color: yellow;
}
<script src="https://unpkg.com/vue#2.5.6/dist/vue.js"></script>
<div id="app">
<my-input v-model="value"></my-input>
</div>
Are you trying to achieve something like this?
new Vue({
el: '#app',
data: {
input: ''
},
computed: {
hasValue () {
return this.input.length ? true : false
}
}
})
.has-value {
background-color: red;
}
<div id="app">
<div :class="{'has-value' : hasValue}">
<input type="text" v-model="input">
</div>
</div>
<script src="https://unpkg.com/vue"></script>

how to select first radio button without knowing the value using vue.js (and v-model)

In my use case the radio buttons is initially dynamically rendered by server. Then in client side I want to use v-model to handle the value change and related logic. The problem is that I want to pre-select the first button but in client side I don't know the value of it. What is the recommended way to do this in vue.js? If necessary the server can generate some "hint" for it.
I've been searching for a while but every working solution I saw assume the data is already there. One method I can think of is to make an api to return the buttons' value but this feels overkill.
JSFiddle
You can set the initial value of r in the mounted hook of the component to the value of the first radio button:
mounted() {
this.r = this.$el.querySelector('input[type="radio"]').value;
}
But it might be better to use a fixed template for the component that isn't rendered by the server in the first place. Instead of rendering HTML, you could render JavaScript containing the data for the items you want and then bind that data to the template.
Vue works best when the template is generated from the data instead of the other way around.
<script>
// Rendered by the server external to the Vue component,
// otherwise just render directly into the component's data
window.items = [
{ label: 'Apple', value: 'apple' },
{ label: 'Orange', value: 'orange' },
];
</script>
<label>
<input v-model="selected" v-for="item of items" type="radio" :value="item.value">
{{ item.label }}
</label>
new Vue({
el: "#app",
data: {
items: window.items,
selected: window.items[0].value,
},
});