Nested Vue.Draggable list breaks when dragging cross-level - vue.js

I want to have a draggable, nested list with Vue and used the Vue.Draggable component for it. I'm however stuck with updating nested lists.
The rendering is fine, the dragging is fine when you stay inside the same level. But dragging cross-level doesn't seem to do it (error in VueComponent.onDragStart).
html
<template>
<div class="fluid container">
<div class="col-md-6">
<draggable class="list-group" element="ul" v-model="list" :options="dragOptions" :move="onMove" #start="isDragging=true" #end="isDragging=false">
<transition-group type="transition" :name="'flip-list'">
<li class="list-group-item" v-for="element in list" :key="element.order">
<i :class="element.fixed? 'fa fa-anchor' : 'glyphicon glyphicon-pushpin'" #click=" element.fixed=! element.fixed" aria-hidden="true"></i>
{{element.name}}
<span class="badge">{{element.order}}</span>
<draggable v-if="element.notes" class="list-group" element="ul" :options="dragOptions" :move="onMove">
<transition-group class="list-group" :name="'flip-list'">
<li class="list-group-item" v-for="elementDeep in element.notes" :key="elementDeep.order">
{{elementDeep.name}} <span class="badge">{{ elementDeep.order }}</span>
</li>
</transition-group>
</draggable>
</li>
</transition-group>
</draggable>
</div>
<div class="list-group col-md-6">
<pre>{{listString}}</pre>
</div>
</div>
</template>
Vue js
<script>
import draggable from 'vuedraggable'
var folderOneReady = [
{
"name":"LOREM IPSUM",
"order":1,
"fixed":false
},
{
"name":"MAGNA ALIQUA",
"order":2,
"fixed":false
},
{
"name": "DOLOREM LAUDANTIUM",
"notes": [
{
"name": "Note level deep One",
"order":31,
"fixed":false
},
{
"name": "Note level deep two",
"order":32,
"fixed":false
},
{
"name": "Note level deep deep three",
"order":33,
"fixed":false
}
],
"order":3,
"fixed":false
},
{
"name":"SIT AMET",
"order":4,
"fixed":false
},
{
"name":"NEMO",
"order":5,
"fixed":false
},
{
"name":"ACCUSANTIUM",
"order":6,
"fixed":false
},
{
"name":"ESSE",
"order":7,
"fixed":false
},
{
"name":"DOLORES",
"order":8,
"fixed":false
}
];
export default {
name: 'hello',
components: {
draggable,
},
data () {
return {
list: folderOneReady,
editable:true,
isDragging: false,
delayedDragging:false
}
},
methods:{
onMove ({relatedContext, draggedContext}) {
const relatedElement = relatedContext.element;
const draggedElement = draggedContext.element;
return (!relatedElement || !relatedElement.fixed) && !draggedElement.fixed;
}
},
computed: {
dragOptions () {
return {
animation: 1,
group: 'description',
disabled: !this.editable,
ghostClass: 'ghost'
};
},
listString(){
return JSON.stringify(this.list, null, 2);
}
},
watch: {
isDragging (newValue) {
if (newValue){
this.delayedDragging= true
return
}
this.$nextTick( () =>{
this.delayedDragging =false
})
}
}
}
</script>
Anyone can direct me in the right direction?

It may not be the only problem but you need to set the list prop or use v-model for the nested draggable
Try something like:
<draggable v-if="element.notes" class="list-group" element="ul" :options="dragOptions" :move="onMove" :list="element.notes">

Related

Nesting in VueDraggable is not not working

here's my problem :
I'm using the VueDraggable library in order to drag and drop elements between a DragBoard.vue and a DropBoard.vue, and a specific type of element should allow to be nested when it is in the DropBoard.
I'm going to take this element as an example :
"Grouped Items"
To do that I've followed this example : https://github.com/SortableJS/vue.draggable.next/blob/master/example/components/nested-example.vue
And this is what I get when I drop "Grouped Items" into the DropBoard.vue :
IMG
As you can see, the DropBoard appears a second time inside Grouped items for whatever reason. I've supposed that the nested-draggable tag also loop what is out of the draggable tag and I've no idea how to resolve that...
πŸ“„ dragItems.JSON (used in DragBoard.vue) :
1st object is a common element
2nd object is a nestable element
[
{
"type": "Simple list",
"title": "Simple list",
"id": 1,
"properties": "this is an item property"
},
...
{
"type": "Grouped items",
"title": "Grouped items",
"id": 10,
"properties": "this is an item property",
"tasks": []
},
...
]
🚩 DropBoard.vue template:
<template>
<div class="board">
<div class="head">Mock</div>
<div class="dd-container">
<draggable
:list="tasks"
v-model="dropItems"
item-key="title"
:group="{ name: 'items', put: true }"
#change="log"
>
<template #item="{ element }">
<div
class="item"
:key="element"
>
<div>
{{ element.title }}
</div>
<nested-draggable
v-if="element.tasks"
:tasks="element.tasks"
class="group-container"
/>
<div class="trashico" :key="index">
<i class="fas fa-trash" #click="deleteItem(index)"></i>
</div>
</div>
</template>
</draggable>
</div>
</div>
</template>
🚩 DropBoard.vue script
<script>
import draggable from "vuedraggable";
export default {
name: "nested-draggable",
components: {
draggable,
},
props: {
dropItems: {
type: Array,
required: true,
},
tasks: {
required: true,
type: Array,
},
},
data() {
return {
dropItems: [],
};
},
methods: {
deleteItem(id) {
this.dropItems.splice(id, 1);
},
},
};
</script>
Here is what I found while using the Vue DevTools, it's quit explicit about the problem.
See image: IMG

VueJS- Display multiple nested properties in objects

I have a multi-level navigation component in VueJS. I can retrieve and display the first 2 levels but I also need the nested property called name of the children array to be displayed. How can I get this nested property to print out in my for loop?
Here is my code:
<template>
<div class="navigation__main-menu-wrapper">
<ul>
<li
v-for="(item, index) in navItems"
:key="index">
<div class="navigation__main-menu-list-link">
<a class="mainnav-anchor"
:href="item.url">{{ item.name }}</a>
</div>
<ul
class="navigation__submenu">
<li
v-for="(subItem, index) in item.items"
:key="index">
<a
:href="subItem.url"
:title="subItem.name">
<div>
<span>{{subItem.name}}</span>
</div>
<div
v-for="(subItemChild, index) in items.children"
:key="index">
<span class="navigation__submenu-name">{{subItemChild.name}}</span>
</div>
</a>
</li>
</ul>
</li>
</ul>
</div>
</template>
<script>
export default {
data: function () {
return {
navItems: []
};
},
mounted: function () {
this.onLoadMainNavigation();
},
methods: {
onLoadMainNavigation: function () {
this.$helpers
.getApiCall("/api/", {
type: "mainnavigation",
})
.then((response) => {
const items = [];
Object.values(response.data.data).forEach((item) => {
const subItems = [];
//Check if there is a submenu
if (item.subMainNavigation) {
item.subMainNavigation.forEach((subItem, subItemChild) => {
subItems.push({
name: subItem.name,
class: subItem.class,
children: [{
name: subItemChild.name
}]
});
});
}
items.push({
name: item.name,
url: item.url,
items: subItems
});
});
this.navItems = items;
})
}
}
};
</script>
This is an exmplae of the data that is outputted
[
{
"name":"Charging solutions"
"items":[
{
"name":"By industry",
"class":"by-industry",
"children":[
{
"id":6671,
"name":"Workplaces",
"class":"workplaces"
},
{
"id":6672,
"name":"Retail & hospitality",
"class":"retail"
},
{
"id":6673,
"name":"Commercial parking",
"class":"parking"
},
{
"id":6674,
"name":"Fuel retailers",
"class":"fuel"
},
]
},
{
"name":"Products",
"class":"products",
"children":[
{
"id":204,
"name":"Public chargers",
"class":"public"
},
{
"id":206,
"name":"Accessories",
"class":"accessories"
},
{
"id":4889,
"name":"Smart charging",
"class":"smart"
}
]
}
]
}
]
Just replace items.children by subItem.children in the last nested loop :
<div v-for="(subItemChild, _index) in subItem.children" :key="_index">
<span class="navigation__submenu-name">{{subItemChild.name}}</span>
</div>
I eventually solved this by simply doing
subItems.push(subItem);

Vue state variable not updating

I have this code but when the selectCategory method gets called i don’t see product.categories being updated in the vue dev console.
<li class="border hover:bg-blue-100" v-bind:class=""
v-on:click="selectCategory($event, category.id)"
>
{{category.name}}
</li>
export default {
data () {
return {
product: {
categories: []
},
}
},
methods: {
selectCategory(event, id){
this.product.categories.push(id);
},
}
This might help you as referance.
<template>
<div>
<li v-for="category in Available.categories"
class="border hover:bg-blue-100"
v-bind:class=""
v-on:click="selectCategory($event, category.id)"
>
{{category.name}}
</li>
</div>
</template>
<script>
export default{
data () {
return {
Available: {
categories: [{
"name":"Fruites",
"id":1,
},
{
"name":"Veges.",
"id":2,
}]
},
product: {
categories: []
},
}
},
methods: {
selectCategory(event, id){
this.product.categories.push(id);
console.log(this.product.categories)
},
}
}
</script>
https://codesandbox.io/s/vue-template-v2zss?fontsize=14

Vue Component Communication (Parent > Children)

I have a parent component (todo-list) with a child component inside (todo-item). I am trying to create a checkbox(check all todos) in the parent so that when all todos will be checked with one click.
With the checkAll() in the parent component, it change the props of the child but it does not change the data of the child.
This is the parent component todo-list
<template>
<div class="todo-list-container">
<todo-input #addTodo="addTodo"></todo-input>
<todo-item v-for="(todo, index) in todos"
:key="todo.id"
:todo="todo"
:index="index"
:completed="todo.completed"
#removeTodo="removeTodo"
#changedCompleted="changedCompleted"
></todo-item>
<div class="flex-container">
<div class="button-aux-div"></div>
<a href="#" class="todo-button">
<input type="checkbox" :checked="!anyRemaining" #change="checkAll">
</a>
</div>
</div>
</template>
<script>
import Vue from 'vue'
import TodoItem from './TodoItem'
import TodoInput from './TodoInput'
export default {
name: 'todo-list',
components: {
TodoItem,
TodoInput,
},
data () {
return {
idForTodo: 3,
todos: [
{
'id': '1',
'title': 'title1',
'body': 'body1',
'completed': false,
},
{
'id': '2',
'title': 'title2',
'body': 'body2',
'completed': false,
},
],
allChecked: false,
}
},
computed: {
remaining() {
return this.todos.filter(todo => !todo.completed).length
},
anyRemaining() {
return this.remaining != 0
}
},
methods: {
addTodo(todoMessage) {
this.todos.push({
id: this.idForTodo,
title: 'title' + this.idForTodo,
body: todoMessage,
completed: false,
})
this.idForTodo++;
},
removeTodo(data) {
this.todos.splice(data.index, 1);
this.idForTodo--;
},
changedCompleted(data) {
this.todos.splice(data.index, 1, data.todo)
},
checkAll() {
this.todos.forEach((todo) => todo.completed = event.target.checked)
},
},
}
</script>
This is the child componenet todo-item
<template>
<div class="todo-item-container">
<div class="todo-title-container">
<div class="todo-id-container">
<div id="todo-id">
<h2>{{ id }}</h2>
</div>
</div>
<div id="todo-title"><h2>{{ title }}</h2></div>
<div class="todo-completed-container">
<a href="#" class="todo-button">
<input type="checkbox" v-model="completed" #change="changedCompleted">
</a>
</div>
<div class="todo-delete-container">
Γ—
</div>
</div>
<hr>
<div class="todo-body-container">
<p id="todo-body">{{ body }}</p>
</div>
</div>
</template>
<script>
export default {
name: 'TodoItem',
props: {
todo : {
type: Object,
required: true,
},
index : {
type: Number,
required: true,
},
},
data () {
return {
'id': this.todo.id,
'title': this.todo.title,
'body': this.todo.body,
'completed': this.todo.completed,
}
},
methods: {
deletedTodo() {
this.$emit('removeTodo', {
'index': this.index,
'todo': {
'id': this.id,
'title': this.title,
'body': this.body,
'completed': this.completed,
}
})
},
changedCompleted() {
this.$emit('changedCompleted', {
'index': this.index,
'todo': {
'id': this.id,
'title': this.title,
'body': this.body,
'completed': this.completed,
}
})
},
},
}
</script>
Instead of 'completed': this.todo.completed, .
Use
computed: {
completed () {
return this.todo.completed
}
}

Change component state form another component rendered in a v-for

I have a list of components rendered in a a v-for. I want to set the "show" Boolean property as false in the other components when one of them is set to true:
To simplify I am only adding two components
Main component code:
<template>
<aside class="main-sidebar">
<section class="sidebar">
<ul class="sidebar-menu" data-widget="tree">
<nav-bar-user-profile-item></nav-bar-user-profile-item>
<nav-bar-item></nav-bar-item>
<nav-bar-item></nav-bar-item>
</ul>
</section>
</aside>
</template>
<script>
import NavBarUserProfileItem from '#/components/NavBar/NavBarUserProfileItem';
import NavBarItem from '#/components/NavBar/NavBarItem';
export default {
name: 'NavBar',
components: {
NavBarUserProfileItem,
NavBarItem
},
methods: {
MenuHasBeenToggled(event) {
console.log(event);
}
}
}
NavBarItemComponent
<template>
<li class="treeview2 item" :class="{'menu-open': isOpen, 'active': menu.active}" #click="ToggleState">
<a href="#">
<i class="fa fa-th"></i>
<span>{{ menu.title }}</span>
<span class="pull-right-container">
<i class="fa fa-angle-right pull-right"></i>
</span>
</a>
<collapse-transition>
<ul class="treeview-menu" v-show="isOpen">
<li v-for="submenu in menu.submenus" :key="submenu.title" :class="{'active': (('active' in submenu) ? submenu.active : false)}">
<b-link :href="submenu.link">
<i class="fa fa-circle-thin"></i>
{{ submenu.title }}
</b-link>
</li>
</ul>
</collapse-transition>
</li>
</template>
<script>
export default {
name: 'NavBarItem',
data: function () {
return {
isOpen: false
}
},
computed: {
},
methods: {
ToggleState() {
this.isOpen = !this.isOpen;
this.$emit("toggle-state");
}
},
props: {
menu: {
type: Object,
default: function() {
return {
link: "#",
title: "Main menu",
active: true,
submenus: [
{
link: "#",
title: "Submenu 1",
},
{
link: "#",
title: "Submenu 2",
active: true
},
{
link: "#",
title: "Submenu 3",
},
]
}
}
}
}
}
</script>
<style scoped>
</style>
The goal is to click on one of the and show the menu contents while at the same time collapse the other components.
I thought about using an array of variables and bind it to the "show" prop and with an event listen to it and set every variable to false except the one form the component that sent the event.
How can I know which component sent the event?
Any better idea on how to accomplish this task?
I think, the best way to do it is to add a uniuque identifier property to each NavBarItem and a property for a selected NavBarItem. Then in the main component you can on click on NavBarItem set selected NavBarItem and in NavBarItem make the isOpen computed on the basis if current NavBarItem identifier equals the clicked NavBarItem. Something like this:
<template>
<aside class="main-sidebar">
<section class="sidebar">
<ul class="sidebar-menu" data-widget="tree">
<nav-bar-user-profile-item></nav-bar-user-profile-item>
<nav-bar-item item-id="1" :selected-item-id="selectedNavbarItemId" #click="selectedNavBarItemId = 1"></nav-bar-item>
<nav-bar-item item-id="2" :selected-item-id="selectedNavbarItemId" #click="selectedNavBarItemId = 2"></nav-bar-item>
</ul>
</section>
</aside>
</template>
<script>
import NavBarUserProfileItem from '#/components/NavBar/NavBarUserProfileItem';
import NavBarItem from '#/components/NavBar/NavBarItem';
export default {
name: 'NavBar',
components: {
NavBarUserProfileItem,
NavBarItem
},
data: function(){
return {
selectedNavBarItemId: 0
}
},
methods: {
MenuHasBeenToggled(event) {
console.log(event);
}
}
}
And in NavBarItem
<template>
<li class="treeview2 item" :class="{'menu-open': isOpen, 'active': menu.active}" #click="ToggleState">
<a href="#">
<i class="fa fa-th"></i>
<span>{{ menu.title }}</span>
<span class="pull-right-container">
<i class="fa fa-angle-right pull-right"></i>
</span>
</a>
<collapse-transition>
<ul class="treeview-menu" v-show="isOpen">
<li v-for="submenu in menu.submenus" :key="submenu.title" :class="{'active': (('active' in submenu) ? submenu.active : false)}">
<b-link :href="submenu.link">
<i class="fa fa-circle-thin"></i>
{{ submenu.title }}
</b-link>
</li>
</ul>
</collapse-transition>
</li>
</template>
<script>
export default {
name: 'NavBarItem',
data: function () {
return {
}
},
computed: {
isOpen:function(){
return itemId == selectedItemId;
}
},
methods: {
},
props: {
itemId:Number,
selectedItemId:Number,
menu: {
type: Object,
default: function() {
return {
link: "#",
title: "Main menu",
active: true,
submenus: [
{
link: "#",
title: "Submenu 1",
},
{
link: "#",
title: "Submenu 2",
active: true
},
{
link: "#",
title: "Submenu 3",
},
]
}
}
}
}
}
</script>
<style scoped>
</style>