Vue prevent event propagation to children conditionally - javascript

This is the example of ColorButtonGroup Vue component (template only) that serves as group of checkbox/toggle buttons and has limit of max. options selected (in this case 4 colors).
It uses ToggleButton component which works as simple toggle selection with styles apply and it's one of common components we have to use in projects.
<template>
<div
class="color-button-group"
:class="[typeClass, variationClass]">
<ToggleButton
v-for="(item, index) in items"
:key="index"
:color="item.color"
:type="type"
#click.native="validateClick"
#change="onChange(item.id)" />
</div>
</template>
I've implemented all logic needed through methods and event handlers and everything works fine but it's also possible to toggle buttons visually after max. selection reached.
Current behavior:
Desired behavior:
How to prevent event propagation to children element(s) conditionally?
stopPropagation and preventDefault as bubbling up and default action prevention were not helpful.
When max. colors selected, level bellow toggle button shouldn't be triggered (disabled state is not allowed for use).

Bottom ToggleButton component has to be modified by adding the new prop toggleable which is true by default and upper ColorButtonGroup controls when it will become false.
<template>
<div
class="color-button-group"
:class="[typeClass, variationClass]">
<ToggleButton
v-for="(item, index) in items"
:key="index"
:color="item.color"
:type="type"
:toggleable="isValidSelection()"
#change="onChange(item.id)" />
</div>
</template>
<script>
export default {
data() {
return {
selected: [],
};
},
methods: {
isValidSelection() {
if (this.selected.length < this.maxSelect)
return true;
return false;
},
onChange(item) {
if (this.isSelected(item)) {
this.selected = this.selected.filter(val => val !== item);
} else if (this.isValidSelection()) {
this.selected.push(item);
}
},
},
};
</script>

Related

How to hide an element from a list if clicked twice without toggling in a vue nuxt application

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

How to implement communication between two arbitrary elements in Vue?

I'm currently building an app using the Vue framework and came across a strange issue that I was unable to find a great solution for so far:
What I'm trying to do is add a class to a parent container in case a specific element inside the container (input, select, textarea etc.) gets focus. Here's the example code:
<div class="form-group placeholder-label">
<label for="desc"><span>Description</span></label>
<div class="input">
<input id="desc" type="text" />
</div>
</div>
In Vanilla JS of course, this is easily done:
const parent = document.querySelector('.placeholder-label');
const input = parent.querySelector('input');
input.addEventListener('focus', (e) => {
parent.classList.add('active');
});
In the same way, you could loop through all .placeholder-label elements and add the event to their child inputs/selects etc. to add this basic functionality. There are two moving parts here:
You don't know the type of the parent element, just that it has .placeholder-label on it.
You don't know the type of the child element, just that it is some sort of HTML form element inside the parent element.
Can I build a Vue component that toggles a class on a given parent element based on focus/blur of a given child element? The best I could come up with is use slots for the child elements, but then I still need to build a component for each parent. Even when using mixins for the reused parts it's still quite a mess compared to the five lines of code I need to write in pure JS.
My template:
<template>
<div
class="form-group"
:class="{ 'active': active }"
>
<label :for="inputID"><span>{{ inputLabel }}</span></label>
<slot
name="input"
:focusFunc="makeActive"
:blurFunc="makeInactive"
:inputLabel="inputLabel"
:inputID="inputID"
/>
</div>
</template>
<script>
export default {
name: 'TestInput',
props: {
inputLabel: {
type: String,
default: '',
},
inputID: {
type: String,
required: true,
},
},
// data could be put into a mixin
data() {
return {
active: false,
};
},
// methods could be put into a mixin
methods: {
makeActive() {
this.active = true;
},
makeInactive() {
this.active = false;
},
},
};
</script>
Usage:
<test-input
:input-i-d="'input-2'"
:input-label="'Description'"
>
<template v-slot:input="scopeVars">
<!-- this is a bootstrap vue input component -->
<b-form-input
:id="scopeVars.inputID"
:state="false"
:placeholder="scopeVars.inputLabel"
#blur="scopeVars.blurFunc"
#focus="scopeVars.focusFunc"
/>
</template>
</test-input>
I guess I'm simply missing something or is this a problem that Vue just can't solve elegantly?
Edit: In case you're looking for an approach to bubble events, here you go. I don't think this works with slots however, which is necessary to solve my issue with components.
For those wondering here are two solutions. Seems like I did overthink the issue a bit with slots and everything. Initially I felt like building a component for a given element that receives a class based on a given child element's focus was a bit too much. Turns out it indeed is and you can easily solve this within the template or css.
CSS: Thanks to #Davide Castellini for bringing up the :focus-within pseudo-selector. I haven't heard of that one before. It works on newer browsers and has a polyfill available.
TEMPLATE I wrote a small custom directive that can be applied to the child element and handles everything.
Usage:
v-toggle-parent-class="{ selector: '.placeholder-label', className: 'active' }"
Directive:
directives: {
toggleParentClass: {
inserted(el, { value }) {
const parent = el.closest(value.selector);
if (parent !== null) {
el.addEventListener('focus', () => {
parent.classList.add(value.className);
});
el.addEventListener('blur', () => {
parent.classList.remove(value.className);
});
}
},
},
},
try using $emit
child:
<input v-on:keyup="emitToParent" />
-------
methods: {
emitToParent (event) {
this.$emit('childToParent', this.childMessage)
}
}
Parent:
<child v-on:childToParent="onChildClick">
--------
methods: {
// Triggered when `childToParent` event is emitted by the child.
onChildClick (value) {
this.fromChild = value
}
}
use this pattern to set a property that you use to change the class
hope this helps. let me know if I misunderstood or need to better explain!

Element Ui component is not rerendering on vue component prop change

I have a parent component and multiple child components, which use the same prop. This prop is an array of keys for a dropdown menu in element.js.
When the children render the first time, they contain no data. However, once the keys from arrive using vuefire the children get the dropdown menu items. However, the element dropdown menu is not rerendered as it should have been.
However using the vue dev tools, I can see that the dropdown menu entries have been passed down as a key. When vue does a hot reload, because of a file change, the keys will load.
Once the entries are loaded, I can select the entry and everything works as expected.
I also had the same results using the vuetify dropdown and the HTML dropdown. Both have the same issue.
parent
<template>
<div class="setup">
<h1>Setup</h1>
<div class="selectIngredients" v-for="number in 6">
<setupSelection :bottle="number" :ingredients="options" />
</div>
</div>
</template>
<script>
import {db} from "#/firebaseConfig"
import setupSelection from '#/components/setupSelection';
export default {
components: {
setupSelection,
},
firestore: {
options: db.collection('ingredients'),
},
};
</script>
child
<template>
<div class="ingredientSelector">
<h3>Select for Pump <span>{{bottle}}</span></h3>
<el-select v-model="selected" clearable placeholder="Select" >
<el-option
v-for="ingredient in ingredients"
v-bind:key="ingredient.text"
v-bind:label="ingredient.text"
v-bind:value="ingredient">
</el-option>
</el-select>
<!-- <v-select
v-model="selected"
:items="ingredients"
label="Select a favorite activity or create a new one"
></v-select> -->
<!-- <select v-model="selected" v-for="ingredient in ingredients">
<option :value="ingredient.value">{{ingredient.text}}</option>
</select> -->
</div>
</template>
<script>
import {db} from "#/firebaseConfig";
export default {
props: {
ingredients: { required: true },
bottle: { type: Number, required: true },
},
data() {
return {
selected: ''
}
},
},
};
</script>
I expected the dropdown menu to update once the client received them.
Thank you!
I haven't used Vuefire myself but I read the following in the documentation:
Make sure to create any property added to firestore in data as well
https://github.com/vuejs/vuefire/tree/master/packages/vuefire#firestore-option
Similar advice is given here:
https://vuefire.vuejs.org/vuefire/binding-subscriptions.html#declarative-binding
In your example you don't have options in the parent's data. This would, presumably, leave it non-reactive, leading to the symptoms you describe.
Use a data property for your items, and set them after the options are loaded.
data() {
return {
options: []
}
},
created() {
db.collection('ingredients').then(data=> this.options = data}
}
The promise returned from db.collection('ingredients') is not reactive.
Even better approach would be to set options: null, and show a loading indicator until it is an array.

v-on:click not working bootstrap

Initially I thought this was an issue with how I was using the #click directive according to this question. I added the .native to the directive and my method is still not getting invoked.
I know this is bootstrap as if I use a normal <button> then the method is invoked as expected.
There are no errors in the logs so it is just as if the element is not registering the directive?
UpcomingBirthdays.vue
<template>
<div>
<h1>{{ section_title }}</h1>
<b-card style="max-width: 20rem;"
v-for="birthday in birthdays.birthdays"
:key="birthday.name"
:title="birthday.name"
:sub-title="birthday.birthday">
<b-button href="#"
#click.native="toWatch(birthday, $event)"
variant="primary">Watch
</b-button>
</b-card>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name: "UpcomingBirthdays",
data: () => {
return {
section_title: "Upcoming Birthdays",
};
},
methods: {
toWatch: (birthday, event) => {
event.preventDefault();
console.log("watched called");
console.log(birthday.name);
console.log(`BEFORE: ${birthday.watch}`);
birthday.watch = !birthday.watch;
console.log(`AFTER: ${birthday.watch}`);
}
},
computed: mapState([
"birthdays",
]),
};
</script>
<style>
</style>
EDIT
Worth mentioning that when using HTML5 <button>, I do not have to append the .native property to the #click directive.
EDIT 2
Here is my codesandbox I created to replicate this error. I would expect an error here to say BirthdaysApi is not defined but I am not getting anything when the button is clicked.
Just remove the href="#" from your buttons (this makes the Bootstrap b-button component render your buttons as anchors) and it's working as expected:
https://codesandbox.io/s/w0yj3vwll7
Edit:
Apparently this is intentional behaviour from the authors, a decision I disagree upon. What they are doing is apparently executing event.stopImmediatePropagation() so any additional listener isn't triggered.
https://github.com/bootstrap-vue/bootstrap-vue/issues/1146

Vue.js checkbox component multiple instances

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

Categories

Resources