Vue JS toggle class on individual items rendered with v-for - javascript

I am using the v-for directive to render a list .
<li v-for="group in groupList" :key="group.id" #dragenter="toggleClass ...."#dragleave="toggleClass ...." >
Content
</li>
What I want is to add a class to the li on which the dragenter event is fired ?
How can I accomplish this ?
How do I even get a reference to the item (the item,not the data property of the parent component)in the first place inside the event handle?and even If I get the reference how to toggle the class from there?
Thanks.
I know vue is data-driven , change the data to reflect on the DOM but I would like a concise solution to this rather than index/Id on the data-model based solutions.Thanks

You can access the li being dragged in the dragenter-callback by accessing event.currentTarget (or even event.target would work in this case), where event is the callback's parameter.
new Vue({
el: '#app',
data() {
return {
grouplist: [
{ id: 1, text: 'a' },
{ id: 2, text: 'b' },
{ id: 3, text: 'c' },
]
}
},
methods: {
onDragEnter(e) {
e.currentTarget.classList.add('drag-enter');
},
onDragLeave(e) {
e.currentTarget.classList.remove('drag-enter');
}
}
})
.drag-enter {
background: #eee;
}
<script src="https://unpkg.com/vue#2.5.16"></script>
<div id="app">
<p draggable>Drag this text over the list items below</p>
<ul>
<li v-for="group in grouplist"
:key="group.id"
#dragenter="onDragEnter"
#dragleave="onDragLeave">{{group.text}}</li>
</ul>
</div>

Related

Vue3 can't get dynamic styling to only apply to specific buttons

I am in the process of learning vue and I'm stumped on how to get these buttons to dynamically style separately when clicked. These are filters for a list of products and I would like the apply one style when the filter is 'on' and a different style when the filter is 'off'. I can get the styles to update dynamically, but all of the buttons change style when any of them are clicked. The actual filter functionality is working as expected (the products are being filtered out when the button for that product is clicked).
In the code snippet, mode is passed to the BaseButton component, which is then applied as the class.
<template>
<ul>
<li v-for="genus of genusList" :key="genus.label">
<BaseButton #click="filterGenus(genus.label)" :mode="genusClicked.clicked ? 'outline' :''">
{{ genus.label }}
</BaseButton>
</li>
<BaseButton #click="clearFilter()" mode="flat">Clear Filter</BaseButton>
</ul>
</template>
methods: {
filterGenus(selectedGenus) {
this.clickedGenus = selectedGenus
this.clicked = !this.clicked
this.$emit('filter-genus', selectedGenus)
},
clearFilter() {
this.$emit('clear-filter')
}
},
I have tried making a computed value to add a .clicked value to the genusList object but that didn't seem to help.
Maybe something like following snippet (if you need more buttons to be styled at once save selected in array, if only one just save selected):
const app = Vue.createApp({
data() {
return {
genusList: [{label: 1}, {label: 2}, {label: 3}],
selGenus: [],
};
},
methods: {
isSelected(selectedGenus) {
return this.selGenus.includes(selectedGenus)
},
filterGenus(selectedGenus) {
if (this.isSelected(selectedGenus)) {
this.selGenus = this.selGenus.filter(s => s !== selectedGenus)
} else {
this.selGenus = [...this.selGenus, selectedGenus]
}
this.$emit('filter-genus', selectedGenus)
},
clearFilter() {
this.selGenus = []
this.$emit('clear-filter')
}
},
})
app.component('baseButton', {
template: `<button :class="mode"><slot /></button>`,
props: ['mode']
})
app.mount('#demo')
.outline {
outline: 2px solid red;
}
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<ul>
<li v-for="genus of genusList" :key="genus.label">
<base-button #click="filterGenus(genus.label)"
:mode="isSelected(genus.label) ? 'outline' :''">
{{ genus.label }}
</base-button>
</li>
<base-button #click="clearFilter()" mode="flat">
Clear Filter
</base-button>
</ul>
</div>

Update the inital array of a recursive treeview in VueJS

I was folling this tutorial for my own tree view with a recursive component in vuejs.
So the input array looks like this:
let tree = {
label: 'root',
nodes: [
{
label: 'item1',
nodes: [
{
label: 'item1.1'
},
{
label: 'item1.2',
nodes: [
{
label: 'item1.2.1'
}
]
}
]
},
{
label: 'item2'
}
]
}
<template>
<div>
...
<tree-menu
v-for="node in nodes"
:nodes="node.nodes"
:label="node.label" />
...
</div>
<template
<script>
export default {
props: [ 'label', 'nodes' ],
name: 'tree-menu'
}
</script>
So basically a label and a subarray of nodes is passed to a child node. Now I want to update or delete a node (e.g. item1.1), but reflect this change in the outmost array (here tree), because I want to send this updated structure to the server. How can I achive this? If I change the label of a node, this will be rendered in the DOM, but the tree array is not updated.
Here's how you can use the .sync modifier to update recursively:
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('tree-node', {
template: `
<div style="margin-left: 5px;">
<input :value="label"
type="text"
#input="$emit('update:label', $event.target.value)" />
<tree-node v-for="(node, key) in nodes"
:key="key"
v-bind.sync="node" />
</div>
`,
props: ['label', 'nodes']
});
let tree = {
label: 'root',
nodes: [{
label: 'item 1',
nodes: [
{ label: 'item 1.1' },
{ label: 'item 1.2',
nodes: [
{ label: 'item 1.2.1' }
]
}
]
},
{ label: 'item 2' }
]
};
new Vue({
el: '#app',
data: () => ({
tree
})
})
#app {
display: flex;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<div id="app">
<div>
<tree-node v-bind.sync="tree" />
</div>
<pre v-html="tree" />
</div>
v-bind.sync="node" is shorthand for :label.sync="node.label" :nodes.sync="node.nodes". v-bind unwraps all object members as attributes of the tag, resulting in props for the component.
The other half of the solution is replacing v-model on the input with :value + an $emit('update:propName', $event.target.value) call on #input which updates the .sync-ed property in the parent. To conceptualize it, it's a DIY v-model exposed by Vue so it could be customized (you decide when to call the update and what to update with). You can replace the <input> with any other type of input, depending on what you're binding/modifying (checkboxes, textarea, select, or any fancier input wrapper your framework might feature). Depending on type of input you'll want to customize the listener: #change, #someCustomEvent, etc...
.sync makes everything reactive at each individual level. Since everything is :key-ed, no re-rendering actually happens (Vue only re-renders DOM elements which actually changed). If that wasn't the case, the input would lose focus upon re-rendering.
The update principle is: instead of making the change at child level you update the parent property which, through v-bind, sends it back to the child.
It's the same exact principle used by Vuex. Rather than changing some local prop you call a store mutation which comes back through getters and modifies the local value but it happens for any component using that store data, not just for current one.

Creating treeview in vue js - unknown depth of the tree - Modifying the DOM

I'm trying to create a vue js application where a treeview is displayed to the user. The elements inside the treeview can contain other elements, that can contain other elements etc ... With no limit, which means that the depth of the treeview is not known. If I'm right, it means I can't simply use the v-for directive (because it implies to know the depth right ?)
So i'm basically looping going through a json array and creating <ul> and <li> tags to append them to some other tag in the DOM, but if I do this, they don't get the styles of their class applied to them.
I suppose it's because Vue doesn't like the fact that we modify the DOM without having vue doing it for us.
Also, We don't want to use components libraries like vuetify, we want to do it with vue only and simple javascript.
Thank you !
This is actually pretty straight forward in Vue.js.
What you have to do is simply create a component that invokes itself but changing the v-for to use the current tree branch's children.
An important step for making this work in Vue is to apply the name key to the component. Otherwise, the component can not invoke itself.
I have provided a simple example below using HTML's neat details element.
// Your recursive branch component "branch.vue"
const branch = {
name: 'branch',
props: {
branch: {
type: Object,
default: () => ({}),
},
},
template: `
<details>
<summary>{{ branch.title }}</summary>
<branch
v-for="branch in branch.children"
:key="branch.title"
:branch="branch"
/>
</details>
`,
}
// Your page view(component) where you want to display the tree
new Vue({
el: '#app',
name: 'tree',
components: {
branch,
},
data() {
return {
tree: [
{
title: 'parent 1',
children: [
{
title: 'child 1',
children: [
{
title: 'child 1-1',
children: [],
},
{
title: 'child 1-2',
children: [],
},
],
},
],
},
],
};
},
})
#app > details {
margin-left: 0;
}
details {
margin-left: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<branch v-for="branch in tree" :key="branch.title" :branch="branch" />
</div>
The solution is to create a recursive component, for example see here and
here

How to reference a child component from the parent in Vue

I'm trying to figure out the Vue-way of referencing children from the parent handler.
Parent
<div>
<MyDropDown ref="dd0" #dd-global-click="ddClicked"></MyDropDown>
<MyDropDown ref="dd1" #dd-global-click="ddClicked"></MyDropDown>
<MyDropDown ref="dd2" #dd-global-click="ddClicked"></MyDropDown>
</div>
export default {
methods: {
ddClicked: function(id) {
console.log("I need to have MyDropDown id here")
}
}
}
Child
<template>
<h1>dropdown</h1>
<Button #click="bclick"></Button>
</template>
export default {
methods: {
bclick: function() {
this.$emit('dd-global-click')
}
}
}
In the parent component I need to see which dropdown was clicked.
What I've tried so far
I tried to set "ref" attribute in the parent. But I can't refer to this prop within the child component. Is there a way to do it? There is nothing like this.ref or this.$ref property.
I tried to use $event.targetElement in the parent, but it looks like I'm mixing Real DOM and Vue Components together. $event.targetElement is a DOM like . So in the parent I have to go over the tree until I find my dropdown. It is ugly I guess.
I set an additional :id property for the dropdown making it the copy of the 'ref' property. In the blick and I called this.$emit('dd-global-click', this.id). Later in the parent I check this.$refs[id]. I kind of works, but I'm not really content with it, because I have to mirror attributes.
Using the _uid property didn't work out either. On top of that, I think, that since it starts with an underscore it is not a recommended way to go.
It seems like a very basic task, so there must be a simplier way to achieve this.
If this custom dropdown element is the top level one (the root element) in the component, you could access the native DOM attributes (like id, class, etc) via this.$el, once it's mounted.
Vue.component('MyDropdown', {
template: '#my-dropdown',
props: {
items: Array
},
methods: {
changed() {
this.$emit('dd-global-click', this.$el.id);
}
}
})
new Vue({
el: '#app',
data: () => ({
items: [
{
id: 'dropdown-1',
options: ['abc', 'def', 'ghi']
},
{
id: 'dropdown-2',
options: ['jkl', 'lmn', 'opq']
},
{
id: 'dropdown-3',
options: ['rst', 'uvw', 'xyz']
}
]
}),
methods: {
ddClicked(id) {
console.log(`Clicked ID: ${id}`);
}
}
})
Vue.config.devtools = false;
Vue.config.productionTip = false;
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.11"></script>
<div id="app">
<my-dropdown
v-for="item of items" :key="item.id"
:id="item.id"
:items="item.options"
#dd-global-click="ddClicked">
</my-dropdown>
</div>
<script id="my-dropdown" type="text/x-template">
<select #input="changed">
<option v-for="item of items" :key="item" :value="item">
{{item}}
</option>
</select>
</script>

Vue2 error when trying to splice last element of object

I have a Vue2 app wit a list of items which I can choose and show, or delete.
When deleting the last element in the list (and only the last one) - I get Vue warn - "[Vue warn]: Error when rendering root instance: "
my HTML:
<body >
<div id="app">
<ul>
<li v-for="(item, index) in list" v-on:click = "selectItem(index)" >
<a>{{ item.name }}</a>
<div v-on:click="deleteItem(index)">X</div>
</li>
</ul>
<div>
<span>{{selectedItem.name}}</span>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
</body>
The JS:
var app = new Vue({
el: '#app',
data: {
index: 0,
selectedItem: {},
list : [
{ id: 1, name: 'org1', desc: "description1"},
{ id: 2, name: 'org2', desc: "description2"},
{ id: 3, name: 'org3', desc: "description3"},
{ id: 4, name: 'org4', desc: "description4"}
]
},
methods: {
deleteItem: function(index) {
this.list.splice(index,1);
},
selectItem: function(index) {
this.selectedItem = this.list[index];
},
}
})
Can you please advise why does this happen and how to solve this issue?
The problem is happening as you have having selectItem bind at li level, so event when you click cross button, selectItem gets executed and that same item gets deleted as well, causing this error.
One way to solve this problem can be moving the selectItem binding inside li as follows
<li v-for="(item, index) in list">
<a v-on:click = "selectItem(index)" >{{ item.name }}</a>
<div v-on:click="deleteItem(index)">X</div>
</li>
See working fiddle.
Another approach can be when printing selectedItem.name in your HTML, you put a null check, whether selectedItem exist or not like following:
<span>{{selectedItem && selectedItem.name}}</span>
See Working fiddle.

Categories

Resources