Vue bind multiple v-models to elements in v-for loop - javascript

I have a v-for loop that creates sections, all of which have a switch component that is set to ON. It is set to on because it is part of this group. The idea is that when one gets switched to OFF it will make an API call to set the new state and be removed from the group.
The trouble that I'm having is that right now I'm binding the switch with v-model and a computed property and when the sections get built they are connected to the SAME property. So if one gets switched to OFF, they all do. I am not sure how to separate these so that when one is clicked it only affects that one. I will also need data associated with the switch that is clicked to make the API call. PS, a click method on the switch element DOES NOT WORK.
HTML
<div class="col-md-6 col-sm-12" v-for="person in people">
<switcher size="lg" color="green" open-name="ON" close-name="OFF" v-model="toggle"></switcher>
</div>
VUE
computed: {
people() { return this.$store.getters.peopleMonitoring },
toggle: {
get() {
return true;
},
set() {
let dto = {
reportToken: this.report.reportToken,
version: this.report.version
}
this.$store.dispatch('TOGGLE_MONITORING', dto).then(response => {
}, error => {
console.log("Failed.")
});
}
}
}
}

You can change your toggle to an array:
computed: {
people() { return this.$store.getters.peopleMonitoring },
toggle: {
get() {
return Array(this.people.length).fill(true);
},
set() {
let dto = {
reportToken: this.report.reportToken,
version: this.report.version
}
this.$store.dispatch('TOGGLE_MONITORING', dto).then(response => {
}, error => {
console.log("Failed.")
});
}
}
}
}
And your HTML:
<div class="col-md-6 col-sm-12" v-for="(person, index) in people">
<switcher size="lg" color="green" open-name="ON" close-name="OFF" v-model="toggle[index]"></switcher>
</div>

Related

Enable loading state of an input field on a computed property

EDITED: code pen was added at the end of the post
How to eanable (change to true) the loading of an input field in a computed property?
In the example for demonstration that follows I get the 'error' unexpected side effect in "inputFilterParentsAndChilds" computed property.
The search field and the list
<template>
<q-input
label="Search"
v-model="searchValue"
placeholder="Minimum 3 characters"
:loading="loadingState"
/>
<q-list
v-for="pater in inputFilterParentsAndChilds"
:key="pater.id_parent"
>
<q-item>
<q-item-section>
<q-item-label>
{{ pater.name }}
</q-item-label>
</q-item-section>
</q-item>
<q-list
v-for="filius in pater.allfilius"
:key="filius.id_filius"
>
<q-item>
<q-item-section>
<q-item-label>
{{ filius.title }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-list>
</template>
Computed propriety
<script>
export default {
data() {
return {
loadingState: false,
searchValue: ''
};
}
}
computed: {
//filter the parent list with nested data (childs)
inputFilterParentsAndChilds() {
if (this.searchValue.length > 2) {
this.loadingState = true; // error!! unexpected side effect in "inputFilterParentsAndChilds" computed property
const filteredPaterArr = this.records.map((rows) => {
const filteredParent = rows.filius.filter(
({ name }) =>
name.toLowerCase().match(this.searchValue.toLowerCase())
);
const filteredChilds = rows.filius.filter(
({ title }) =>
title.toLowerCase().match(this.searchValue.toLowerCase())
);
if (filteredChilds.length) {
this.loadingState = false;
return { ...rows, filius: filteredParent };
} else {
this.loadingState = false;
return { ...rows, filius: filteredChilds };
}
});
return filteredPaterArr.filter((obj) => obj.filius.length);
} else {
return this.records;
}
}
}
</script>
About nested v-for list Filter nested v-for list
EDITED
CODEPEN https://codepen.io/ijose/pen/BaYMVrO
Why is loading important in my case?
If the list has hundreds of records the filter is slow and appears (wrongly) to have frozen, requiring a loading to inform the user that the filtering operation is still in progress
You don't, computed properties simply can't change state.
You can achieve similar effects by watching searchValue.
data () {
return {
searchValue: '',
isLoading: false,
filteredElements: []
}
},
watch: {
searchValue: {
immediate: true, // tells Vue to run handler function right after constructing the component
handler () {
// this function will be triggered when searchValue changes
this.isLoading = true
// give Vue chance to render the loader before doing heavy computation in 'filterElements' function
this.$nextTick(() => {
this.filteredElements = this.filterElements()
this.isLoading = false
})
}
}
Docs on nextTick
Docs on watchers
Edit:
Have you measured that it's really computing that makes your drop-down slow and not rendering? Take a look at the documentation on performance, especially "Virtualizing large lists"

check props value in child component if available

I'm working currently with BootstrapVue.
I have a b-dropdown in my parent.vue where I can select a object of a JSON-File and convert it into an array because I need the length of this json object. This works fine!!
My problem is that I need to check in my parent.vue if something was selected - so if this.arrayLength is higher than 0 (until this point it works all well!). If this is true, it should use and show addElementsNotClickable() in my child.vue where no elements can be added (count of the inputs are equal to length of array) - otherwise it should use and show my button addElement() where multiple elements can be added manually.
But I'm not able to check in my child.vue if arrayLenght > 0... AND i don't know what to use on second button e.g #change(??) How can I solve that?
Many thanks! I've tried to be as detailed as I can!
Additional Info: I get no error codes!!
my parent.vue:
methods: {
inputedValue(input, index) {
var array = [];
const item= this.json.find((i) => i.Number === input);
for (let key in item.ID) {
array.push(item.ID[key]);
}
if(array.length > 0) {
this.getIndex = index;
this.getDataArray = array;
this.getLengthArray = array.length;
}
}
}
my child.vue (template)
<div class="mt-4 mb-5 ml-3 mr-3">
<b-button v-if="!hide" #click="addElement" variant="block">Add Element</b-button>
<b-button v-if="hide" #???="addElementNotClickable" variant="block">Not clickable ! </b-button>
</div>
my child.vue (script)
methods: {
addElementsNotClickable() {
for(let i = 1; i < this.arrayLength; i++) {
this.inputs.push({})
}
},
addElement() {
this.inputs.push({})
},
}
data() {
return {
inputs: [{}]
arrayLength: this.getLengthArray,
arrayIndex: this.getIndex,
hide: false,
}
props: [
"getLengthArray",
"getIndex"
],
You are misunderstanding how components should work in Vue. In short you can understand them by:
parent send props down and child send events up
What you are looking for is that whenever your arrayLength updates, you send an event to the parent. Then, it is the responsibility of the parent to handle that event. In this case, the parent would receive the event and store the length of the array.
Parent.vue
<template>
<div>
<child #arrayLenght:update="onArrayLenghtUpdate"/>
</div>
</template>
<script>
export default {
data: () => {
arrayLength: 0,
},
methods: {
onArrayLenghtUpdate(length) {
this.arrayLength = length;
}
}
}
</script>
Child.vue
<template> ... </template>
<script>
export default {
data: () => ({
arrayLength: 0,
}),
watch: {
arrayLenghth: function (newLenght) {
this.$emit('arrayLenght:update', newLenght);
}
}
}
</script>
This is the standard way and extremely useful if your Parent and Child aren't highly coupled together. If they are dependent on each other (you won't use Child.vue anywhere else in the app. Just as direct child of Parent.vue) then you can use the inject/provide approach. This approach is beyond the scope of this answer, feel free to read an article on it

Vue / axios filter types of same type as single item

I have an app where I want to show a list of items. When you click on a single item, you're sent to it's "page", where its info is displayed.
These items have a type.
Beneath the single item info, I want to display all items with the same type, filtered from the list of all items. However I have no idea what to return in my filterItems()-method. Since the axios-calls are done with asyncData() I don't have access to singleitem.type, do I?
HTML:
<template>
<div>
<reusable-component v-for="item in singleitem" :key="item.id" />
<reusable-component v-for="item in filterItems(type)" :key="item.id" />
</div>
</template>
JS:
export default {
data() {
return {
singleitem: [],
allitems: []
}
},
asyncData() {
// Grab single item from ID supplied
return axios.get(`https://url/to/GetItemById${params.id}`)
.then((result) => {
return { singleitem: result.data }
})
// Grab all items
return axios.get('https://url/to/GetAllItems')
.then((result) => {
return { allitems: result.data }
})
},
methods: {
filterItems() {
// Filter items from all items that has same type as the singleitem
return allitems.filter(function(type) {
// Help!
})
}
}
}
I think this is a use case of computed properties [https://v2.vuejs.org/v2/guide/computed.html]
May be something like this:
computed: {
filterItems: function() {
return this.allitems.filter(item => item.type != singleitem.type);
}
}
So whenever the data changed. filterItems get (re)computed.

vuejs passing array of checkboxes to parent template is only passing one value

I looked at potential dupes of this, such as this one and it doesn't necessarily solve my issue.
My scenario is that I have a component of orgs with label and checkbox attached to a v-model. That component will be used in combination of other form components. Currently, it works - but it only passes back one value to the parent even when both checkboxes are click.
Form page:
<template>
<section>
<h1>Hello</h1>
<list-orgs v-model="selectedOrgs"></list-orgs>
<button type="submit" v-on:click="submit">Submit</button>
</section>
</template>
<script>
// eslint-disable-next-line
import Database from '#/database.js'
import ListOrgs from '#/components/controls/list-orgs'
export default {
name: 'CreateDb',
data: function () {
return {
selectedOrgs: []
}
},
components: {
'list-orgs': ListOrgs,
},
methods: {
submit: function () {
console.log(this.$data)
}
}
}
</script>
Select Orgs Component
<template>
<ul>
<li v-for="org in orgs" :key="org.id">
<input type="checkbox" :value="org.id" name="selectedOrgs[]" v-on:input="$emit('input', $event.target.value)" />
{{org.name}}
</li>
</ul>
</template>
<script>
import {db} from '#/database'
export default {
name: 'ListOrgs',
data: () => {
return {
orgs: []
}
},
methods: {
populateOrgs: async function (vueObj) {
await db.orgs.toCollection().toArray(function (orgs) {
orgs.forEach(org => {
vueObj.$data.orgs.push(org)
})
})
}
},
mounted () {
this.populateOrgs(this)
}
}
</script>
Currently there are two fake orgs in the database with an ID of 1 and 2. Upon clicking both checkboxes and clicking the submit button, the selectedOrgs array only contains 2 as though the second click actually over-wrote the first. I have verified this by only checking one box and hitting submit and the value of 1 or 2 is passed. It seems that the array method works at the component level but not on the component to parent level.
Any help is appreciated.
UPDATE
Thanks to the comment from puelo I switched my orgListing component to emit the array that is attached to the v-model like so:
export default {
name: 'ListOrgs',
data: () => {
return {
orgs: [],
selectedOrgs: []
}
},
methods: {
populateOrgs: async function (vueObj) {
await db.orgs.toCollection().toArray(function (orgs) {
orgs.forEach(org => {
vueObj.$data.orgs.push(org)
})
})
},
updateOrgs: function () {
this.$emit('updateOrgs', this.$data.selectedOrgs)
}
},
mounted () {
this.populateOrgs(this)
}
}
Then on the other end I am merely console.log() the return. This "works" but has one downside, it seems that the $emit is being fired before the value of selectedOrgs has been updated so it's always one "check" behind. Effectively,I want the emit to wait until the $data object has been updated, is this possible?
Thank you so much to #puelo for the help, it helped clarify some things but didn't necessarily solve my problem. As what I wanted was the simplicity of v-model on the checkboxes populating an array and then to pass that up to the parent all while keeping encapsulation.
So, I made a small change:
Select Orgs Component
<template>
<ul>
<li v-for="org in orgs" :key="org.id">
<input type="checkbox" :value="org.id" v-model="selectedOrgs" name="selectedOrgs[]" v-on:change="updateOrgs" />
{{org.name}}
</li>
</ul>
</template>
<script>
import {db} from '#/database'
export default {
name: 'ListOrgs',
data: () => {
return {
orgs: []
}
},
methods: {
populateOrgs: async function (vueObj) {
await db.orgs.toCollection().toArray(function (orgs) {
orgs.forEach(org => {
vueObj.$data.orgs.push(org)
})
})
},
updateOrgs: function () {
this.$emit('updateOrgs', this.$data.selectedOrgs)
}
},
mounted () {
this.populateOrgs(this)
}
}
</script>
Form Page
<template>
<section>
<h1>Hello</h1>
<list-orgs v-model="selectedOrgs" v-on:updateOrgs="updateSelectedOrgs"></list-orgs>
<button type="submit" v-on:click="submit">Submit</button>
</section>
</template>
<script>
// eslint-disable-next-line
import Database from '#/database.js'
import ListOrgs from '#/components/controls/list-orgs'
export default {
name: 'CreateDb',
data: function () {
return {
selectedOrgs: []
}
},
components: {
'list-orgs': ListOrgs
},
methods: {
updateSelectedOrgs: function (org) {
console.log(org)
},
submit: function () {
console.log(this.$data)
}
}
}
</script>
What the primary change here is I now fire a method of updateOrgs when the checkbox is clicked and I pass the entire selectedOrgs array via the this.$emit('updateOrgs', this.$data.selectedOrgs)`
This takes advantage of v-model maintaining the array of whether they're checked or not. Then on the forms page I simply listen for this event on the component using v-on:updateOrgs="updateSelectedOrgs" which contains the populated array and maintains encapsulation.
The documentation for v-model in form binding still applies to custom components, as in:
v-model is essentially syntax sugar for updating data on user input
events...
https://v2.vuejs.org/v2/guide/forms.html#Basic-Usage and
https://v2.vuejs.org/v2/guide/components-custom-events.html#Customizing-Component-v-model
So in your code
<list-orgs v-model="selectedOrgs"></list-orgs>
gets translated to:
<list-orgs :value="selectedOrgs" #input="selectedOrgs = $event.target.value"></list-orgs>
This means that each emit inside v-on:input="$emit('input', $event.target.value) is actually overwriting the array with only a single value: the state of the checkbox.
EDIT to address the comment:
Maybe don't use v-model at all and only listen to one event like #orgSelectionChange="onOrgSelectionChanged".
Then you can emit an object with the state of the checkbox and the id of the org (to prevent duplicates):
v-on:input="$emit('orgSelectionChanged', {id: org.id, state: $event.target.value})"
And finally the method on the other end check for duplicates:
onOrgSelectionChanged: function (orgState) {
const index = selectedOrgs.findIndex((org) => { return org.id === orgState.id })
if (index >= 0) selectedOrgs.splice(index, 1, orgState)
else selectedOrgs.push(orgState)
}
This is very basic and not tested, but should give you an idea of how to maybe solve this.

binding a ref does not work in vue.js?

When I v-bind a element-ref with :ref="testThis" it stops working it seems. Compare this version which works:
<template>
<div>
<q-btn round big color='red' #click="IconClick">
YES
</q-btn>
<div>
<input
ref="file0"
multiple
type="file"
accept=".gif,.jpg,.jpeg,.png,.bmp,.JPG"
#change="testMe"
style='opacity:0'
>
</div>
</div>
</template>
<script>
import { QBtn } from 'quasar-framework'
export default {
name: 'hello',
components: {
QBtn
},
data () {
return {
file10: 'file0'
}
},
methods: {
IconClick () {
this.$refs['file0'].click()
},
testMe () {
console.log('continue other stuff')
}
}
}
</script>
With this one which DOES NOT work:
<template>
<div>
<q-btn round big color='red' #click="IconClick">
YES
</q-btn>
<div>
<input
:ref="testThis"
multiple
type="file"
accept=".gif,.jpg,.jpeg,.png,.bmp,.JPG"
#change="testMe"
style='opacity:0'
>
</div>
</div>
</template>
<script>
import { QBtn } from 'quasar-framework'
export default {
name: 'hello',
components: {
QBtn
},
data () {
return {
file10: 'file0'
}
},
methods: {
IconClick () {
this.$refs['file0'].click()
},
testThis () {
return 'file0'
},
testMe () {
console.log('continue other stuff')
}
}
}
</script>
The first one works. The second one throws an error:
TypeError: Cannot read property 'click' of undefined
at VueComponent.IconClick
As I would like to vary the ref based on a list-index (not shown here, but it explains my requirement to have a binded ref) I need the binding. Why is it not working/ throwing the error?
In the vue docs I find that a ref is non-reactive: "$refs is also non-reactive, therefore you should not attempt to use it in templates for data-binding."
I think that matches my case.
My actual problem 'how to reference an item of a v-for list' is NOT easily solved not using a binded ref as vue puts all similar item-refs in an array, BUT it loses (v-for index) order.
I have another rather elaborate single file component which works fine using this piece of code:
:ref="'file' + parentIndex.toString()"
in an input element. The only difference from my question example is that parentIndex is a component property.
All in all it currently is kind of confusing as from this it looks like binding ref was allowed in earlier vue version.
EDIT:
Triggering the method with testThis() does work.
If you want to use a method, you will need to use the invocation parentheses in the binding to let Vue know you want it to bind the result of the call and not the function itself.
:ref="testThis()"
I think the snippet below works as you expect it to. I use a computed rather than a method.
new Vue({
el: '#app',
data() {
return {
file10: 'file0'
}
},
computed: {
testThis() {
return 'file0';
}
},
methods: {
IconClick() {
this.$refs['file0'].click()
},
testMe() {
console.log('continue other stuff')
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<q-btn round big color='red' #click="IconClick">
YES
</q-btn>
<div>
<input :ref="testThis" multiple type="file" accept=".gif,.jpg,.jpeg,.png,.bmp,.JPG" #change="testMe" style='opacity:0'>
</div>
</div>

Categories

Resources