Display dropdowns dynamically in one component - javascript

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>

Related

Click Button in Parent, get data from Child to Parent and use it in a method (Vue 3)

I'm working with Vue3 and Bootstrap 5.
MY PROBLEM: I want to click a button in my parent.vue. And after clicking this I want to have the data from my child.vue inside of the method in my parent.vue - method .
But my data is always empty, except I need another ```setTimeout"-function. But actually I don't want to use it.
I think there is a better solution for the props Boolean as well..
If there are any question left regarding my problem, please ask me!
Thanks for trying helping me out!
PARENT:
<template>
<Child :triggerFunc="triggerFunc" #childData="childData"/>
<button type="button" class="btn btn-success" #click="get_data()">Get Data</button>
</template>
<script>
export default {
data() {
return {
triggerFunc: false,
c_data: [],
}
},
methods: {
childData(data) {
this.c_data = data;
},
get_data() {
this.triggerFunc = true;
setTimeout(() => {
this.triggerFunc = false;
}, 50);
console.log(this.c_data);
//HERE I WANT TO USE "C_DATA" BUT OF COURSE IT's EMPTY. WITH ANOTHER SET_TIMEOUT IT WOULD WORK
//BUT I DON'T WANT TO USE IT. BUT WITHOUT IT'S EMPTY.
//LIKE THIS IT WOULD WORK BUT I DON'T WANT IT LIKE THAT
setTimeout(() => {
console.log(this.c_data);
}, 50);
}
},
}
</script>
CHILD:
<template>
<!-- SOME BUTTONS, INPUTS, ETC. IN HERE -->
</template>
<script>
export default {
data() {
return {
input1: "",
input2: "",
}
},
props: {
triggerFunc: Boolean,
},
triggerFunc(triggerFunc) {
if(triggerFunc == true) {
this.save_data()
}
}
methods: {
save_data() {
var data = [
{
Input1: this.input1,
Input2: this.input2
},
]
this.$emit("childData", data);
},
},
}
</script>
Parent can very well hold/own the data of it's children. In that case, the children only render/display the data. Children need to send events up to the parent to update that data. (Here parent is the Key component and child is a Helper for the parent.)
So, here parent always has the master copy of the child's data in its own data variables.
Also, you are using # for binding properties, which is wrong. # is for event binding. For data binding use ':' which is a shorthand for v-bind:
You can just say :childData=c_data
PS: You seem to be getting few of the basics wrong. Vue is reactive and automatically binds the data to the variables. So, you don't have to do this much work. Please look at some basic Vue examples.
Refer: https://sky790312.medium.com/about-vue-2-parent-to-child-props-af3b5bb59829
Edited code:
PARENT:
<template>
<Child #saveClick="saveChildData" :childData="c_data" />
</template>
<script>
export default {
data() {
return {
c_data: [{Input1:"", Input2:""}]
}
},
methods: {
saveChildData(incomingData) {
//Either set the new value, or copy all elements.
this.c_data = incomingData;
}
},
}
</script>
CHILD:
<template>
<!-- SOME BUTTONS, INPUTS, ETC. IN HERE -->
<!-- Vue will sync data to input1, input2. On button click we can send data to parent. -->
<button #click.prevent="sendData" />
</template>
<script>
export default {
props:['childData'],
data() {
return {
input1: "",
input2: "",
}
},
methods: {
sendData() {
var data = [
{
Input1: this.input1,
Input2: this.input2
},
]
this.$emit("saveClick", data); //Event is "saveClick" event.
},
},
beforeMount(){
//Make a local copy
this.input1 = this.childData[0].Input1;
this.input2 = this.childData[0].Input2;
}
}
</script>

Prevent click on the html element according to the variable value

I have this clickable div in a component which fires a todo function when the div is clicked:
<div #click="todo()"></div>
Also, I have a global variable in the component which is called price.
I need to make the div above clickable, or the todo function firable only if the price value is greater than 100.
How can I achieve this behavior in Vue?
I guess you can simply write a wrapper function like so:
<template>
<div #click="onClick"></div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
price: 0
}
},
computed: {
isTodoCallAllowed() {
return this.price > 100;
}
},
methods: {
onClick() {
if(this.isTodoCallAllowed) {
this.todo();
}
},
todo() {
//
}
}
}
</script>
In general, making divs clickable is usually bad practice, since it can make your interface really unclear and confusing to users.
With this in mind, I think it's easier if you replace your div with a button element, since buttons allow you to use the disabled property to prevent clicks:
new Vue({
el: '#app',
data() {
return {
price: 100
}
},
computed: {
priceIsOver() {
return this.price > 100;
}
},
methods: {
todo() {
console.log('doing stuff');
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<div>
Price: {{price}} <button #click="price++">+</button> <button #click="price--">-</button>
</div>
<br>
<button :disabled="!priceIsOver" #click="todo">TODO</button>
</div>

Check / Uncheck a Checkbox in a child component with vuejs

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

Vue - loading div when data is re-rendered

I'm doing a list component with v-for and a function. Every time I change filters it re-renders the list. I did a div for loading message every time its changing, but I'm not sure exactly how to handle it.
This is an uncompleted code, but its easier to you read:
<template>
<div>
<Filters
:country-selected="countrySelected" :countries="countries"
#updateCountry="countrySelected = $event"
></Filters>
<section class="teams container-wrapper">
<!-- Loading Placeholder -->
<div class="loading-msg" :class="{'is-open': loading}">
<p>Loading...</p>
</div>
<!-- List -->
<div class="teams-list" :class="{'is-loading': loading}">
<team-list class="teams"
v-for="(team, index) in loadingTeams()" :key="index"
:lorem=team.lorem
></team-list>
</div>
</section>
</div>
</template>
<script>
export default {
data() {
return {
teams: json.teams,
countries: json.countries,
countrySelected: "",
teamList: undefined, //to watch
loading: true //variable to toggle
}
},
watch: {
teamList: function() {
this.loading = true // => THIS LINE EXECUTES AN INFINTE LOOP
setTimeout(() => this.loading = false, 1000)
}
},
methods: {
teamFilters: function() {
return this.teams.filter(
team => team.country = this.countrySelected
)
},
loadingTeams: function() {
this.teamList = this.teamFilters()
return this.teamFilters()
}
}
}
</script>
<style>
.is-open {
opacity: 1;
}
.is-loading {
opacity: 0;
}
</style>
Probably there is an easy way to handle list update and toogle loading div (i would like to know your opinions), but I'm not understading why the function in watch 'teamList' is doing a infinite loop. It should work, right?

(VueJS) Update parent data from child component

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!

Categories

Resources