Context :
I am using VueJS in a project for the first time so I'm potentially not using it the right way.
I have a parent and a child component :
Parent : ResearchProducts.vue : Displays a list of products that can be filtred by their categories. The filters are checkboxes related to the category of the products.
Child : CategoryDisplay.vue : This is the component that handle the view and the methods of a category row.
My Question :
When I click on a category checkbox inside of my child component and if the checkbox is checked, the category is added to a list of filters in the parent. This works.
When I uncheck the checkbox, the category is removed from this list. This works too.
Now I've showed all my categories as buttons in the parent component to make the filters visible more easily for the user. And my problem is that I want this button to uncheck the related checkbox inside the child component when I click on it.
Here is my actual code :
ResearchProducts.vue :
<template>
<div>
<template v-for="category in categories">
<category-display
:category="category"
:key="'category_'+category.id"
:checked="checked"
#categorySelected="categorySelected"
></category-display>
</template>
<button
v-for="filter in listFilters"
:key="'filter_'+filter.slug"
class="btn btn-light btn-sm mr-2 mb-2"
#click="removeFilter(filter)"
>
{{ filter.name }}
</button>
</div>
</template>
<script>
export default {
data() {
return {
categories: { // HERE A COLLECTION CONTAINING ALL MY CATEGORIES },
selectedCategories: [],
listFilters: []
};
},
methods: {
categorySelected(category) {
var newCategory = {
type: "category",
slug: category.slug,
name: category.name
};
if (category.checked) {
if (!this.selectedCategories.includes(category.slug)) {
this.selectedCategories.push(category.slug);
this.listFilters.push(newCategory);
}
} else {
if (this.selectedCategories.includes(category.slug)) {
const index = this.selectedCategories.indexOf(category.slug);
if (index > -1) {
this.selectedCategories.splice(index, 1);
}
}
this.listFilters = this.listFilters.filter(function(item) {
for (var key in newCategory) {
if (item[key] === undefined || item[key] == newCategory[key])
return false;
}
return true;
});
}
},
removeFilter(filter) {
// THERE, I NEED TO UNCHECK THE RELATED CHECKBOX IN CHILD COMPONENT
this.listFilters = this.listFilters.filter(function(item) {
for (var key in filter) {
if (item[key] === undefined || item[key] == filter[key]) return false;
}
return true;
});
}
}
};
</script>
CategoryDisplay.vue :
<template>
<b-form-checkbox-group class="w-100">
<b-form-checkbox :value="category.slug" class="w-100" #input="selection" v-model="selected" ref="checked">
{{ category.name }}
<span class="badge badge-secondary float-right">{{ category.products_count }}</span>
</b-form-checkbox>
</b-form-checkbox-group>
</template>
<script>
export default {
props: {
category: {
type: Object,
required: true
}
},
data() {
return {
selected: false
}
},
methods: {
selection() {
var response = false;
if(this.selected.length !== 0){
response = true;
}
this.$emit('categorySelected', {slug: this.category.slug, name: this.category.name, checked: response});
}
}
};
</script>
Here is a simplified sample for your reference. You can use the sync modifier to achieve a two-way-binding between the parent and child. That together with a computed property in the child with a setter and getter
So passing all categories to child, and sync the selected categories:
<HelloWorld :cats.sync="selectedCategories" :categories="categories"/>
Child component takes the categories, iterates and shows checkboxes. Here we use the computed property, and when a checkbox is clicked, the setter emits the change to the parent:
<label v-for="c in categories" :key="c.id">
<input v-model="temp" type="checkbox" :value="c">
{{ c.name }}
</label>
script:
computed: {
temp: {
get: function() {
return this.cats;
},
set: function(newValue) {
this.$emit("update:cats", newValue);
}
}
}
And the parent just simply iterates the selectedCategories and as you wish, when a change happens in parent for selectedCategories, child will be automatically aware of when an item is deleted.
Here's a full sample for your reference: SANDBOX
Related
I have got a clickable list in a Vue/Nuxt application. When one item is selected, a little tick mark appears. I would like to be able to unselect an item (the tick mark to disappear) if the item is clicked again. If I click on another item, I would like this item to be selected and the previously selected item to unselect (only one item can be selected). So far, if I try to select another item, I need to click twice because the first click will only unselect the first selected item and the second click will select the new item. Any idea ??
<template>
<div
v-for="(item, itemIndex) in list"
:key="itemIndex"
#click="onClick(itemIndex)"
>
<div>
<div v-if="activeIndex == itemIndex && selected === true">
<TickMark />
</div>
<Item />
</div>
</div>
</template>
<script>
export default {
props: {
questionModules: {
required: true,
type: Array,
},
},
data() {
return {
activeIndex: null,
selected: false,
}
},
methods: {
onClick (index) {
this.activeIndex = index
this.selected = !this.selected
},
},
}
</script>
because you don't need to change positions or sort the list - keeping the selected index is just fine, do it like this:
<template>
<section
class="items-list">
<template v-for="(item, itemIndex) in list"
:key="itemIndex" >
<TickMark v-if="activeIndex === itemIndex
#click="selectItem(itemIndex)" /> // by clicking on the mark - it will toggle the selection
<Item />
</template>
</section>
</template>
<script>
export default {
props: {
questionModules: {
required: true,
type: Array,
},
},
data() {
return {
activeIndex: null
}
},
methods: {
selectItem (index) {
this.activeIndex = index
},
},
}
</script>
I've changed the architecture of the DOM so it will be without all the un-necessary elements
I want to have multiple dropdowns in one component using one variable to display or not and also clicking away from their div to close them:
<div class="dropdown">
<button #click.prevent="isOpen = !isOpen"></button>
<div v-show="isOpen">Content</div>
</div>
// second dropdown in same component
<div class="dropdown">
<button #click.prevent="isOpen = !isOpen"></button>
<div v-show="isOpen">Content</div>
</div>
data() {
return {
isOpen: false
}
},
watch: {
isOpen(isOpen) {
if(isOpen) {
document.addEventListener('click', this.closeIfClickedOutside)
}
}
},
methods: {
closeIfClickedOutside(event){
if(! event.target.closest('.dropdown')){
this.isOpen = false;
}
}
}
But now when I click one dropdown menu it displays both of them. I am kinda new to vue and cant find way to solve this
To use just one variable for this, the variable needs to identify which dropdown is open, so it can't be a Boolean. I suggest storing the index (e.g., a number) in the variable, and conditionally render the selected dropdown by the index:
Declare a data property to store the selected index:
export default {
data() {
return {
selectedIndex: null
}
}
}
Update closeIfClickedOutside() to clear the selected index, thereby closing the dropdowns:
export default {
methods: {
closeIfClickedOutside() {
this.selectedIndex = null
}
}
}
In the template, update the click-handlers to set the selected index:
<button #click.stop="selectedIndex = 1">Open 1</button>
<button #click.stop="selectedIndex = 2">Open 2</button>
Also, update the v-show condition to render based on the index:
<div v-show="selectedIndex === 1">Content 1</div>
<div v-show="selectedIndex === 2">Content 2</div>
Also, don't use a watcher to install a click-handler on the document because we want to know about the outside-clicks when this component is rendered. It would be more appropriate to add the handler in the mounted hook, and then remove in the beforeDestroy hook:
export default {
mounted() {
document.addEventListener('click', this.closeIfClickedOutside)
},
beforeDestroy() {
document.removeEventListener('click', this.closeIfClickedOutside)
},
}
demo
Make an array and loop through it, much easier that way.
<template>
<div id="app">
<div class="dropdown" v-for="(drop, index) in dropData" :key="index">
<button #click="openDropdown(index);">{{ drop.title }}</button>
<div v-show="isOpen === index">{{ drop.content }}</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isOpen: null,
dropData: [
{
title: "Hey",
content: "Hey it's content 1"
},
{
title: "Hey 2",
content: "Hey it's content 2"
},
{
title: "Hey 3",
content: "Hey it's content 3"
},
]
};
},
methods: {
openDropdown(idx){
if (this.isOpen === idx) {
this.isOpen = null;
} else {
this.isOpen = idx;
}
}
}
};
</script>
I have a parent who's passing props to a child and the child emits events to the parent. However this is not fully working and I am not sure why. Any suggestions?
Parent:
<template>
<div class="natural-language">
<div class="natural-language-content">
<p class="natural-language-results">
<msm-select :options="payments" :model="isShowingUpdate" />
</p>
</div>
</div>
</template>
<script>
import msmSelect from '../form/dropdown.vue'
export default {
components: {
'msm-select': msmSelect
},
data() {
return {
isShowingUpdate: true,
payments: [
{'value': 'paying anuualy', 'text': 'paying anuualy'},
{'value': 'paying monthly', 'text': 'paying monthly'}
],
}
}
}
</script>
Child:
<template>
<div class="form-pseudo-select">
<select :model="flagValue" #change="onChange($event.target.value)">
<option disabled value='Please select'>Please select</option>
<option v-for="(option, index) in options" :value="option.value">{{ option.text }}</option>
</select>
</div>
</template>
<script>
export default {
props: {
options: {
elType: Array
},
isShowingUpdate: {
type: Boolean
}
},
data() {
return {
selected: '',
flagValue: false
}
},
methods: {
onChange(value) {
if (this.selected !== value) {
this.flagValue = true;
} else {
this.flagValue = false;
}
}
},
watch: {
'flagValue': function() {
console.log('it changed');
this.$emit('select', this.flagValue);
}
},
created() {
console.log(this.flagValue);
this.flagValue = this.isShowingUpdate;
}
}
</script>
Basically, when the option in the select box changes, the boolean flag should be updated. However, in my child I am getting undefined for isShowingUpdate. What am I missing?
There isn't the relation that you said between the two components.
The component that you called parent is in reality the child... and the child is parent.
The parent component is always the one that calls the other, in your case:
//Parent component
<template>
...
<msm-select :options="policies" :model="isShowingUpdate" /> << the child
...
</template>
You should change the props/events between the two components.
Edit:
You can edit the:
onChange(value) {
if (this.selected !== value) {
this.flagValue = true;
} else {
this.flagValue = false;
}
}
To a new one like the following:
On the children:
onChange(value) {
if (this.selected !== value) {
this.flagValue = true;
} else {
this.flagValue = false;
}
this.$emit('flagChanged', this.flagValue)
}
On the parent use the emit event to capture and call some other method:
//HTML part:
<msm-select :options="payments" :model="isShowingUpdate" v-on:flagChanged="actionFlagChanged" />
//JS part:
methods: {
actionFlagChanged () {
//what you want
}
}
Can I give you some tips?
It's not a (very) good name of a function the one called: onChange
inside a onChange event... try something like: updateFlag (more
semantic).
I think that you can delete the watch part and do it in the onChange
event
Try to find a good documentation/tutorial (i.e the official documentation) to learn more about parent/child communication.
Remember to add the event-bus import:
import { EventBus } from './event-bus'
Hope it helps!
I have a list of filters using checkboxes. I'm trying to make each checkbox it's own components. So I loop through my list of filters adding a checkbox component for each filter. The Vue.js documentation says that if I have multiple checkboxes that use the same model that array will get updated with the value of the checkboxes. I see that working if the group of checkboxes is part of the parent component. But if I make the checkbox a component and add each checkbox component in a loop then the model doesn't update as expected.
How can I have a checkbox component that updates an array on the parent? I know I can do this with emitting an event for a method on the component that updates the array but the Vue documentation makes it seems like the framework does this for you.
Here is a code sample I've been playing around with https://www.webpackbin.com/bins/-KwGZ5eSofU5IojAbqU3
Here is a working version.
<template>
<div class="filter-wrapper">
<input type="checkbox" v-model="internalValue" :value="value">
<label>{{label}}</label>
</div>
</template>
<script>
export default {
props: ['checked','value', 'label'],
model: {
prop: "checked"
},
computed:{
internalValue: {
get(){return this.checked},
set(v){this.$emit("input", v) }
}
}
}
</script>
Updated bin.
The answer given by #Bert is right. I just want to complete the picture with the list of components and how thay are integrated. As this is a useful pattern.
Also including Select All functionality
ListItem.vue
<template>
<div class="item">
<input type="checkbox" v-model="internalChecked" :value="item.id" />
... other stuff
</div>
</template>
<script>
export default {
// Through this we get the initial state (or if the parent changes the state)
props: ['value'],
computed:{
internalChecked: {
get() { return this.value; },
// We let the parent know if it is checked or not, by sending the ID
set(selectedId) { this.$emit("input", selectedId) }
}
}
}
</script>
List.vue
<template>
<div class="list">
<label><input type="checkbox" v-model="checkedAll" /> All</label>
<list-item
v-for="item in items"
v-bind:key="item.id"
v-bind:item="item"
v-model="checked"
</list-item>
... other stuff
</div>
</template>
<script>
import ListItem from './ListItem';
export default {
data: function() {
return: {
// The list of items we need to do operation on
items: [],
// The list of IDs of checked items
areChecked: []
}
},
computed: {
// Boolean for checked all items functionality
checkedAll: {
get: function() {
return this.items.length === this.areChecked.length;
},
set: function(value) {
if (value) {
// We've checked the All checkbox
// Starting with an empty list
this.areChecked = [];
// Adding all the items IDs
this.invoices.forEach(item => { this.areChecked.push(item.id); });
} else {
// We've unchecked the All checkbox
this.areChecked = [];
}
}
}
},
components: {
ListItem
}
}
</script>
Once boxes get checked we have in checked the list of IDS [1, 5] which we can use to do operation on the items with those IDs
I have the following problem: I have parent component with a list of checkboxes and two inputs. So when the any of those two inputs has been changed I need to uncheck all checkboxes. I would appreciate if you can help me to solve this.
I wanted to change checkedItem to trigger watch in child and then update all children, but it doesn't work.
parent.vue
<template>
<div class="filter-item">
<div class="filter-checkbox" v-for="item in filter.items">
<checkbox :item="item" v-model="checkedItem"> {{ item }} </checkbox>
</div>
<div class="filter-range">
<input v-model.number="valueFrom">
<input v-model.number="valueTo">
</div>
</div>
</template>
<script>
import checkbox from '../checkbox.vue'
export default {
props: ['filter'],
data() {
return {
checkedItem: false,
checkedItems: [],
valueFrom: '',
valueTo: '',
}
},
watch: {
'checkedItem': function () {},
'valueFrom': function () {},
'valueTo': function () {}
},
components: {checkbox}
}
</script>
child.vue
<template>
<label>
<input :value="value" #input="updateValue($event.target.value)" v-model="checked" class="checkbox"
type="checkbox">
<span class="checkbox-faux"></span>
<slot></slot>
</label>
</template>
<script>
export default {
data() {
return {
checked: ''
}
},
methods: {
updateValue: function (value) {
let item = this.item
let checked = this.checked
this.$emit('input', {item, checked})
}
},
watch: {
'checked': function () {
this.updateValue()
},
},
created: function () {
this.checked = this.value
},
props: ['checkedItem', 'item']
}
</script>
When your v-for renders in the parent component, all the rendered filter.items are bound to the same checkedItem v-model.
To correct this, you would need to do something like this:
<div class="filter-checkbox" v-for="(item, index) in filter.items">
<checkbox :item="item" v-model="item[index]> {{ item }} </checkbox>
</div>
To address your other issue, updating the child component list is as easy as updating filter.items.
You don't even need a watcher if you dont want to use one. Here is an alternative:
<input v-model.number="valueFrom" #keypress="updateFilterItems()">
And then:
methods: {
updateFilterItems () {
// Use map() or loop through the items
// and uncheck them all.
}
}
Always ask yourself twice if watch is necessary. It can create complexity unnecessarily.