Vue 2: Best way to use v-model on custom checkboxes - javascript

Say, I have a custom checkbox (input) component, and I plan to use it with v-model in the component that is supposed to use it.
What I'm doing is, to support multiple checkboxes, I create an array state, and upon checking/unchecking, I add/remove the checkbox's value from the array.
Is the best approach. I'm using Vue2.
// CheckBox.vue
<script>
export default {
model: { // Customising the option for model
prop: "checked", // Array of checked values
event: "check" // Firing this event with new array of checked values
},
props: ["name", "label", "value", "checked"],
emits: ["check"]
};
</script>
<template>
<label :for="`input-${name}`">
{{ label }}
<input :id="`input-${name}`" :value="value" type="checkbox" :checked="checked.includes(value)"
#change="$emit('check', $event.target.checked ? [...checked, value] : checked.filter((val) => val !== value))" /> // I'm just appending/removing value from the array state. Is this the best approach?
</label>
</template>
// SomeComponent.vue
<script>
import CheckBox from "../atoms/checkbox.vue";
export default {
data: () => ({
elements: [1, 2, 3, 4, 5, 6, 7, 8, 9],
checked: [], // Storing the checked values here
}),
watch: {
checked: function (newSelected) { console.log(newSelected); }
},
components: {
CheckBox
}
}
</script>
<template>
<ul>
<li v-for="num in elements">
<CheckBox :name="num" :label="num" :value="num" v-model="checked" />
</li>
</ul>
</template>

Related

AlpineJs dynamically changing nested component

What are the options to keep nested components reactive.
I have very little experience with alpineJs and need some advice.
There is a filter in which items are dynamically changed during the search.
I bypass these elements with x-for.
This is where I run into a problem. For example, for the item.range position, I need to use a different library (for example, Ion.RangeSlider), the problem is that initialization occurs only once and, accordingly, with subsequent changes in items, the item.range position that is in x-for will not change and remain static .
I see several options here:
It is trivial to wrap the rendering of this position in the isLoading condition, and on each load, the initialization will occur anew and, accordingly, the actual data will be displayed. But this is clearly not the right decision, for a number of reasons.
<template x-if="item.hasRange && !isLoading" >
<div x-data="item(index)">
<input x-ref="range" type="text" />
<template x-data="range($refs.range)"></template>
</div>
</template>
Implement a wrapper on top of this library with its api methods, and try to pass data already in them.
Below is a slightly truncated version of the code.
document.addEventListener('alpine:init', () => {
Alpine.data('filter', () => ({
items: [],
item_: (index) => {
return {
item: this.items[index];
range: () => {
return {
range: null,
init() {
this.range = range(element) {
$(element).ionRangeSlider({
type: "double",
//...
onFinish: (data) => {},//...
});
}
},
update: () => {
this.range.update(/*...*/);
},
//... other api
};
}
}
}
})
})
Implement an event listener in a component that implements rendering using an external library
I would be very grateful for an example or information on how best to implement such dynamism.
You can try using x-show instead of x-if, with this you can then manually hide or show the component using CSS.
<div x-data="{ items: [], activeIndex: null }">
<template x-for="(item, index) in items">
<div x-show="activeIndex === index" x-bind:key="index">
<input x-ref="range" type="text" x-bind:value="item.range" x-on:input="item.range = $event.target.value" />
<template x-init="initRange($refs.range, item.range)">
<!-- initialization code here -->
</template>
</div>
</template>
</div>
The best way to include an external library is to write a custom directive or a custom magic component. This way we can have an Alpine.js-like API for the external library with two-way data binding and we can reuse our component easily elsewhere.
Here I show a minimal example that implements a double RangeSlider with min/max modifiers as e.g. x-range.min.0.max.10="item.range" custom directive. I added further explanations as inline comments, so see the live example and source code:
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs#3.x.x/dist/cdn.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/css/ion.rangeSlider.min.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/js/ion.rangeSlider.min.js"></script>
<div x-data="{items: [
{id: 1, range: {from: 2, to: 3, updating: false}},
{id: 2, range: {from: 3, to: 4, updating: false}},
{id: 3, range: {from: 4, to: 5, updating: false}},
]}">
<template x-for="item in items" :key="item.id">
<div style="margin-bottom: 20px;">
<input type="text" x-range.min.0.max.10="item.range" />
<div><b x-text="`Item #${item.id} Range: ${item.range.from}-${item.range.to}`"></b>,
Set manually: from: <input type="number" x-model="item.range.from" min="0" max="10" size="1">
to: <input type="number" x-model="item.range.to" min="0" max="10" size="1">
<span x-show="item.range.updating">Changing...</span>
</div>
</div>
</template>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.directive('range', (el, { expression, modifiers }, { evaluate, evaluateLater, effect }) => {
// Set RangeSlider config from directive modifiers
const config = {
type: "double",
onChange(data) {
// Disable Alpine.js->RangeSlider data flow during RangeSlider change event
evaluate(`${expression}.updating = true;`)
// Update Alpine.js data after range change
evaluate(`${expression}.from = ${data.from}; ${expression}.to = ${data.to}`)
},
onFinish(data) {
// Enable Alpine.js->RangeSlider data flow again after a RangeSlider change event
evaluate(`${expression}.updating = false;`)
},
}
// Source: https://github.com/ryangjchandler/alpine-tooltip
const getModifierArgument = (modifier) => {
return modifiers[modifiers.indexOf(modifier) + 1]
}
if (modifiers.includes('min')) {
config.min = getModifierArgument('min')
}
if (modifiers.includes('max')) {
config.max = getModifierArgument('max')
}
if (!el.__x_range) {
// Init RangeSlider on the element
el.__x_range = $(el).ionRangeSlider(config)
// Set RangeSlider data for updating it from Alpine.js
el.__x_range_data = el.__x_range.data("ionRangeSlider")
}
// Set Alpine.js data binding that updates RangeSlider
let rangeData = evaluateLater(expression)
effect(() => {
// Update RangeSlider data from Alpine.js data when RangeSlider is inactive
rangeData(val => {
if (!val.updating) el.__x_range_data.update({ from: val.from, to: val.to })
})
})
})
})
</script>

Bootstrap Vue Checkbox <b-table> <b-form-checkbox>

I am trying to use b-form-checkbox with b-table. Trying to retrieve the selected module Ids.
<b-table
id="module-table"
:items="list.modules"
:fields="fields"
:busy="isBusy">
<template slot="num" slot-scope="row">
{{ row.index + 1 }}
</template>
<template slot="checkbox" slot-scope="row">
<b-form-group>
<b-form-checkbox v-if="!isLoading" v-model="row.item.selected" #change="selection(row.item.moduleId)" variant="outline-secondary" class="mr-1">
</b-form-checkbox>
</b-form-group>
</template>
</b-table>
data: {
fields: [
{ key: 'checkbox', label: '', class: 'd-none d-lg-table-cell' },
{ key: 'num', label: 'Num', class: 'd-none d-lg-table-cell' },
],
selected: []
}
Though I am able to retrieve the selected module Ids but unable to delete them while switching the checkboxes. If anyone can provide an idea on how to track if the checkbox is selected (true) or not selected (false). Thanks in advance.
methods: {
selection(item) {
if (true)
app.selected.push(item);
else
app.selected = app.selected.map(x => x != item);
}
},
The checkbox values are already stored in list.modules[].selected via the v-model, so you could just use a computed prop to get the selected modules instead of using a separate selected array:
Remove the #change="selection(⋯)" from <b-form-checkbox>:
<!--
<b-form-checkbox
v-model="row.item.selected"
#change="selection(row.item.moduleId)" // remove
>
-->
<b-form-checkbox
v-model="row.item.selected"
>
Remove the selection method and selected data property, since they're no longer needed.
export default {
data() {
return {
// selected: [] // remove
}
},
methods: {
// selection() {⋯} // remove
}
}
Add a computed prop that filters list.modules[] for selected modules:
export default {
computed: {
selected() {
return this.list.modules.filter(module => module.selected)
},
},
}
demo

Why is the Vue.js input value not updating?

I have a Vue.js text-input component like the following:
<template>
<input
type="text"
:id="name"
:name="name"
v-model="inputValue"
>
</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: this.value
};
},
watch: {
inputValue: function () {
eventBus.$emit('inputChanged', {
type: 'text',
name: this.name,
value: this.inputValue
});
}
}
};
</script>
And I am using that text-input in another component as follows:
<ul>
<li v-for="row in rows" :key="row.id">
<text-input :name="row.name" :value="row.value">
</text-input>
</li>
</ul>
Then, within the JS of the component using text-input, I have code like the following for removing li rows:
this.rows = this.rows.filter((row, i) => i !== idx);
The filter method is properly removing the row that has an index of idx from the rows array, and in the parent component, I can confirm that the row is indeed gone, however, if I have, for example, two rows, the first with a value of 1 and the second with a value of 2, and then I delete the first row, even though the remaining row has a value of 2, I am still seeing 1 in the text input.
Why? I don't understand why Vue.js is not updating the value of the text input, even though the value of value is clearly changing from 1 to 2, and I can confirm that in the parent component.
Maybe I'm just not understanding how Vue.js and v-model work, but it seems like the value of the text input should update. Any advice/explanation would be greatly appreciated. Thank you.
You cannot mutate values between components like that.
Here is a sample snippet on how to properly pass values back and forth. You will need to use computed setter/getter. Added a button to change the value and reflect it back to the instance. It works for both directions.
<template>
<div>
<input type="text" :id="name" v-model="inputValue" />
<button #click="inputValue='value2'">click</button>
</div>
</template>
<script>
export default {
props: ['name', 'value'],
computed: {
inputValue: {
get() {
return this.value;
},
set(val) {
this.$emit('updated', val);
}
}
}
}
</script>
Notice that the "#updated" event updates back the local variable with the updated value:
<text-input :name="row.name" :value="row.value" #updated="item=>row.value=item"></text-input>
From your code you are trying to listen to changes.. in v-model data..
// Your Vue components
<template>
<input
type="text"
:id="name"
:name="name"
v-model="inputValue"
>
</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: ""
};
},
};
</script>
If You really want to listen for changes..
<ul>
<li v-for="row in rows" :key="row.id">
<text-input #keyup="_keyUp" :name="row.name" :value="row.value">
</text-input>
</li>
</ul>
in your component file
<template>...</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: ""
};
},
methods : {
_keyUp : () => {// handle events here}
};
</script>
check here for events on input here
To bind value from props..
get the props value, then assign it to 'inputValue' variable
it will reflect in tthe input element

Vue.Js, binding a value to a checkbox in a component

I'm making a component which is a wrapper around a checkbox (I've done similar with inputs of type 'text' and 'number') but I cannot get my passed in value to bind correctly.
My component is:
<template>
<div class="field">
<label :for="name" class="label">
{{ label }}
</label>
<div class="control">
<input :id="name" :name="name" type="checkbox" class="control" :checked="value" v-on="listeners" />
</div>
<p v-show="this.hasErrors" class="help has-text-danger">
<ul>
<li v-for="error in errors" :key="error">{{ error }}</li>
</ul>
</p>
</div>
</template>
<script>
export default {
name: 'check-edit',
props: {
value: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
errors: {
type: Array,
default: () => []
}
},
mounted () {
},
computed: {
listeners () {
return {
// Pass all component listeners directly to input
...this.$listeners,
// Override input listener to work with v-model
input: event => this.$emit('input', event.target.value)
}
},
hasErrors () {
return this.errors.length > 0
}
},
}
</script>
I've imported it globally; and am invoking it in another view by doing:
<check-edit name="ShowInCalendar" v-model="model.ShowInCalendar" label="Show in calendar?" :errors="this.errors.ShowInCalendar"></check-edit>
My model is in data and the property ShowInCalendar is a boolean and in my test case is true. So when I view the page the box is checked. Using the Vue tools in firefox I can see the model.ShowInCalendar is true, and the box is checked. However, when I click it the box remains checked and the value of ShowInCalendar changes to 'on', then changes thereafter do not change the value of ShowInCalendar.
I found this example here: https://jsfiddle.net/robertkern/oovb8ym7/ and have tried to implement a local data property for it but the result is not working.
The crux of what I'm trying to do is have the initial checkstate of the checkbox be that of ShowInCalendar (or whatever property is bound via v-model on the component) and then have that property be update (to be true or false) when the checkbox is checked.
Can anyone offer me any advice please?
Thank you.
You should not $emit event.target.value, it's the value of the checkbox, it's not a Boolean value. If you want to detect the checkbox is update(to be true or false) or not, You should $emit event.target.checked just like fstep said.
If v-on is the only listener that will be used it might be easier to use v-model as in the checkbox example from the Vue input docs.
However you can use listeners based on Binding-Native-Events-to-Components docs
<template>
<div class="field">
<label :for="name" class="label">
{{ label }}
</label>
<div class="control">
<input :id="name" :name="name" type="checkbox" class="control" checked="value" v-on="listeners" />
</div>
<p v-show="this.hasErrors" class="help has-text-danger">
<ul>
<li v-for="error in errors" :key="error">{{ error }}</li>
</ul>
</p>
</div>
</template>
<script>
export default {
name: 'check-edit',
props: {
value: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
errors: {
type: Array,
default: () => []
}
},
mounted() {},
computed: {
listeners() {
var vm = this;
// `Object.assign` merges objects together to form a new object
return Object.assign(
{},
// We add all the listeners from the parent
this.$listeners,
// Then we can add custom listeners or override the
// behavior of some listeners.
{
// This ensures that the component works with v-model
input: function(event) {
vm.$emit('input', event.target.checked);
}
}
);
},
hasErrors() {
return this.errors.length > 0;
}
}
};
</script>
Don't change props. Your component, having a v-model, should be emitting input events on change. The parent will handle the actual changing of the value.

Laravel can't get the values from Vue-multiselect

I am using Vue-multiselect with Laravel.
I am using the multiselect component in my form to let the user select multiple countries. The component works fine but when I submit the form and I dd() it, it shows [object Object].
I can't get the value of the multiselect component. I have found similar questions but none of them worked for me.
Here is my code:
The ExampleComponent.vue file:
<template slot-scope="{ option }">
<div>
<label class="typo__label">Restricted country</label>
<multiselect
v-model="internalValue"
tag-placeholder="Add restricted country"
placeholder="Search or add a country"
label="name"
name="selectedcountries[]"
:options="options"
:multiple="true"
track-by="name"
:taggable="true"
#tag="addTag"
>
</multiselect>
<pre class="language-json"><code>{{ internalValue }}</code></pre>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect'
// register globally
Vue.component('multiselect', Multiselect)
export default {
components: {
Multiselect
},
props: ['value'],
data () {
return {
internalValue: this.value,
options: [
{ name: 'Hungary' },
{ name: 'USA' },
{ name: 'China' }
]
}
},
watch: {
internalValue(v){
this.$emit('input', v);
}
},
methods: {
addTag (newTag) {
const tag = {
name: newTag,
code: newTag.substring(0, 2) + Math.floor((Math.random() * 10000000))
}
this.options.push(tag)
this.value.push(tag)
}
},
}
</script>
Here is my register form:
<div id="select">
<example-component v-model="selectedValue"></example-component>
<input type="hidden" name="countriespost" :value="selectedValue">
</div>
<script>
const select = new Vue({
el: '#select',
data: {
selectedValue: null
},
});
</script>
When I submit the form, the countriespost shows me me this: [object Object] instead of the actual value.
It's because you are providing an array of objects as options property:
options: [
{ name: 'Hungary' },
{ name: 'USA' },
{ name: 'China' }
]
so the value emited on input is an object.
Try to change the options to following:
options: [ 'Hungary', 'USA', 'China' ]
If you pass an array of objects to the :options prop of the multiselect component, you should submit the form with javascript so you can extract the object ids or whatever you need on the backend and then send them through.
Add a method like this:
submit: function() {
let data = {
objectIds: _.map(this.selectedOptions, option => option.id), //lodash library used here
// whatever other data you need
}
axios.post('/form-submit-url', data).then(r => {
console.log(r);
});
}
Then trigger it with a #click.stop event on your submit button.

Categories