I am currently doing a website builder, where user can drag and drop to add element.
The drag and drop works well, but what i want is, how can i disable/hide the drop placeholder in the target container ?
As show in the image, whenever I hover on a container, it will show a copy of my dragging element by default, which I don't want.
Here is my code :
<template>
<div style="display : flex;">
<div id="dragArea">
<draggable
class="dragArea list-group"
:list="list1"
:group="{ name: 'item', pull: 'clone', put: false }"
:clone="cloneItem"
#change="log"
>
<div class="list-group-item" v-for="element in list1" :key="element.id">{{ element.name }}</div>
</draggable>
</div>
<div id="dropArea">
<draggable class="dragArea list-group" :list="list2" group="item" #change="log">
<div class="list-group-item" v-for="element in list2" :key="element.id">{{ element.name }}</div>
</draggable>
</div>
</div>
</template>
Script :
<script>
import draggable from "vuedraggable";
let idGlobal = 8;
export default {
name: "custom-clone",
display: "Custom Clone",
order: 3,
components: {
draggable,
},
data() {
return {
hover : false,
list1: [
{ name: "cloned 1", id: 1 },
{ name: "cloned 2", id: 2 },
],
list2: [
]
};
},
methods: {
log: function(evt) {
window.console.log(evt);
},
cloneItem({ name, id }) {
return {
id: idGlobal++,
name: name
};
},
},
};
</script>
On each of your <draggable> components within your <template>, you can set the ghost-class prop to a CSS class that hides the drop placeholder (ie. "ghost", or "dragging element" as you called it) using display: none; or visibility: hidden;.
For example:
In your <template>:
<draggable ghost-class="hidden-ghost">
and in the <style> section of your Vue Single File Component, or in the corresponding stylesheet:
.hidden-ghost {
display: none;
}
Working Fiddle
The ghost-class prop internally sets the SortableJS ghostClass option (see all the options here). The ability to modify these SortableJS options as Vue.Draggable props is available as of Vue.Draggable v2.19.1.
Related
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
I have a BaseMenuItem component that has normal button elements as well as special news elements.
I have added a ticker effect to the news type els and want to stop the ticker on that element when it's clicked. Currently the click event stops the ticker effect on the whole group.
How can I target a single element from that group?
There are two methods, openNews one showing the specific news article that the element is linked to.
And clearItemType that clears the itemType upon recieving the emitted event from the BaseMenuItem component.
I'm just not sure which element to target to change it's itemType.
Does Vuejs have a way to make an unique data value for dynamically generated elements?
If you need anymore information please let me know!
Cheers!
BaseMenuItem
<template>
<q-btn align="left" dense flat class="main-menu-item" v-on="$listeners">
<div class="flex no-wrap items-center full-width">
<iconz v-if="iconz" :name="iconz" type="pop" color="black" class="mr-md" />
<q-icon v-if="menuIcon" :name="menuIcon" class="text-black mr-md" />
<div #click="$emit('stop-ticker')" v-if="itemType === 'news'" class="ellipsis _ticker">
<div class="ellipsis _ticker-item">{{ title }}</div>
</div>
<div v-else>
<div class="ellipsis">{{ title }}</div>
</div>
<slot>
<div class="ml-auto"></div>
<div class="_subtitle mr-md" v-if="subtitle">{{ subtitle }}</div>
<q-icon name="keyboard_arrow_right" class="_right-side" />
<ComingSoon v-if="comingSoonShow" />
</slot>
</div>
</q-btn>
</template>
<style lang="sass" scoped>
// $
.main-menu-item
display: block
font-size: 15px
position: relative
width: 100%
border-bottom: 1px solid #F5F5F5
+py(10px)
._left-side
color: #000000
._subtitle
margin-left: auto
opacity: 0.7
._ticker
position: absolute
font-weight: bold
margin-left: 2em
width: 82%
&-item
display: inline-block
padding-left: 100%
animation: ticker 8s linear infinite
#keyframes ticker
to
transform: translateX(-100%)
</style>
<script>
import { iconz } from 'vue-iconz'
export default {
name: 'MainMenuItem',
components: { iconz },
props: {
comingSoonShow: { type: Boolean, default: false },
title: { type: String, default: 'menu' },
subtitle: { type: String, default: '' },
menuIcon: { type: String, default: '' },
iconz: { type: String, default: '' },
itemType: { type: String, default: '' },
}
}
</script>
MainMenuPage
<template>
<div class="eachMenuGroup" v-if="newsList.length">
<MainMenuItem
v-for="news in newsList"
:key="news.id"
#click="openNews(news)"
:title="news.title"
:itemType="itemType"
:class="{ readLink: readNewsList[news.id] }"
menuIcon="contactless"
#stop-ticker="clearItemType"
></MainMenuItem>
</div>
</template>
<style lang="sass" scoped>
.readLink
font-weight: 500
</style>
<script>
methods: {
openNews(postInfo) {
dbAuthUser().merge({ seenNewsPosts: { [postInfo.id]: true } })
Browser.open({ url: postInfo.url, presentationStyle: 'popover' })
},
clearItemType() {
this.itemType = ''
return
},
</script>
EDIT: I edited since your latest comment. See below
To target the exact element within your v-for that fired the event, you can use $refs by using an index :
<MainMenuItem v-for="(item, index) in items" :ref="`menuItem--${index}`" #stop-ticker="clearItemType(index)" />
In your method:
clearItemType(index){
console.log(this.$refs[`menuItem--${index}`])
// this is the DOM el that fired the event
}
Edited version after your comments:
If you pass the exact same prop to each el of your v-for you wont be able to modify its value just for one child in the parent context.
Instead, keep an array of distinctive values in your parent. Each child will receive one specific prop that you can change accordingly in the parent either by listening to an child emitted event or by passing index as click event as I've done below for simplicity.
See snippet below
Vue.config.productionTip = false;
const Item = {
name: 'Item',
props: ['name', 'isActive'],
template: `<div>I am clicked {{ isActive }}!</div>`,
};
const App = new Vue({
el: '#root',
components: {
Item
},
data: {
items: ["item1", "item2"],
activeItemTypes: []
},
methods: {
toggle(index) {
this.activeItemTypes = this.activeItemTypes.includes(index) ? this.activeItemTypes.filter(i => i !== index) : [...this.activeItemTypes, index]
}
},
template: `
<div>
<Item v-for="(item, i) in items" :name="item" #click.native="toggle(i)" :isActive="activeItemTypes.includes(i)"/>
</div>
`,
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="root"></div>
I am working on a basic notepad app, for now the functionality is simple, create a note, when done click on the note from a list of previous created notes to view its details. I am not able to click on the note and see the details, instead I see the details of the component ShowNote.vue on the bottom of the notepad template, and in order to see the details I have to make the v-if="noteIsOpen to false". I am also not able to see the data from the data bind in ShowNote.vue file. Also when you click on the plus button the details from the note populate the page the button generates when clicked. I will paste screen shots of my code below. Please help me figure this out. I have tried to fix the props, and when I did I was able to see the note details finally.
App.vue
<template>
<div class="body">
<div class="notepad-container h-75 w-75">
<header class="header d-flex justify-content-center align-items-center">
<h4>Light Notepad v1</h4>
</header>
<section class="notepad-content" v-if="editorIsOpen === false">
<note-list
v-for="note in notes"
:key="note.id"
:note="note"
></note-list>
<add-note-button #open-editor="openNewEditor"></add-note-button>
</section>
<section class="notepad-editor" v-if="editorIsOpen === true">
<save-button></save-button>
</section>
<section class="notepad-content" v-if="noteIsOpen === true">
<show-note
:note="notes"
#open-note="readNote"
/>
</section>
<section class="notepad-content" v-if="noteIsOpen === false">
<show-note
:note="notes"
#open-note="openNote"
/>
</section>
</div>
</div>
</template>
<script>
import AddNoteButton from "./components/AddNoteButton.vue";
import NoteList from "./components/NoteList.vue";
import SaveButton from "./components/SaveButton.vue";
import ShowNote from "./components/ShowNote.vue";
export default {
components: {
NoteList,
AddNoteButton,
SaveButton,
ShowNote,
},
data() {
return {
editorIsOpen: false,
noteIsOpen: false,
notes: [
{
id: 1,
title: "1st Note",
body: "This is a note",
date: "10/17/20",
},
{
id: 2,
title: "2nd Note",
body: "This is a note",
date: "11/17/20",
},
],
};
},
methods: {
openNewEditor() {
this.editorIsOpen = !this.editorIsOpen;
},
readNote() {
this.noteIsOpen = !this.noteIsOpen;
},
},
};
</script>
AddNoteButton.vue
<template>
<div class="add-note-container" #click="openEditor">
<b-icon-plus-circle></b-icon-plus-circle>
</div>
</template>
<script>
import {BIconPlusCircle} from 'bootstrap-vue';
export default {
emits: ['open-editor'],
components: {
BIconPlusCircle
},
methods: {
openEditor() {
console.log('hello');
this.$emit('open-editor');
}
}
}
</script>
NoteList.vue
<template>
<div>
<b-list-group>
<b-list-group-item button #click="openNote()"
>{{ note.title }} - {{ note.date }}</b-list-group-item
>
</b-list-group>
</div>
</template>
<script>
export default {
emits: ['open-note'],
props: {
note: {
required: true,
},
},
methods: {
openNote() {
this.$emit('open-note');
console.log("clicked from NoteList");
},
},
};
</script>
ShowNote.vue
<template>
<div>
note details:
Note ID: {{ note.id }}, Date: {{ note.date }},
Title: {{ note.title }}, Body: {{ note.body }}
</div>
</template>
<script>
export default {
name: 'showNote',
props: {
note: {
required: true,
}
},
};
</script>
In your NoteList.vue you are emitting the event "open-note". But this event is never catched in your parent component named App.vue. You have to bind this event in order to get notified when ever you clicked on a note entry. Something like #open-note="openNote".
I figured out how to get the details to show when clicking on the note, for now I created a button in the notepad-content section:
<button class="readNoteButton" #click="readNote">view note one</button>
and changed the section with the show note component to:
<section v-if="readingNote === true" class="">
<show-note
#open-note="openNote"
v-for="note in notes"
:key="note.id"
:note="note"
></show-note>
</section>
issue I have now is figuring out how to get the details to show separately pertaining to each individual button
I have a hierarchical list component where child items have checkboxes. Checkbox actions(check/uncheck) must keep the parent component in sync with the checkbox's changed state. I cannot figure out how to achieve this using v-bind.sync recursively. My code is as below:
Menu.vue
This component holds the hierarchical list. (Only relevant code included)
HierarchicalCheckboxList is the component that displays the hierarchical list
Property 'value' holds the check/uncheck value (true/false)
Property 'children' contains the child list items
How do I define the .sync attribute on HierarchicalCheckboxList and with what parameter?
<template>
<div>
<HierarchicalCheckboxList
v-for="link in links"
#checked="primaryCheckChanged"
:key="link.id"
v-bind="link">
</HierarchicalCheckboxList>
</div>
</template>
<script>
import HierarchicalCheckboxList from 'components/HierarchicalCheckboxList'
data () {
return {
links: [{
id: 1,
title: 'Home',
caption: 'Feeds, Dashboard & more',
icon: 'account_box',
level: 0,
children: [{
id: 2,
title: 'Feeds',
icon: 'feeds',value: true,
level: 1,
children: [{
id: '3',
title: 'Dashboard',
icon: 'settings',
value: true,
level: 1
}]
}]
}]
}
},
methods: {
primaryCheckChanged (d) {
// A child's checked state is propogated till here
console.log(d)
}
}
</script>
HierarchicalCheckboxList.vue
This component calls itself recursively:
<template>
<div>
<div v-if="children != undefined && children.length == 0">
<!--/admin/user/user-->
<q-item clickable v-ripple :inset-level="level" :to="goto">
<q-item-section>
{{title}}
</q-item-section>
</q-item>
</div>
<div v-else>
<div v-if="children != undefined && children.length > 0">
<!-- {{children}} -->
<q-expansion-item
expand-separator
:icon="icon"
:label="title"
:caption="caption"
:header-inset-level="level"
default-closed>
<template v-slot:header>
<q-item-section>
{{ title }}
</q-item-section>
<q-item-section side>
<div class="row items-center">
<q-btn icon="add" dense flat color="secondary"></q-btn>
</div>
</q-item-section>
</template>
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
#checked="primaryCheckChanged"
v-bind="child">
</HierarchicalCheckboxList>
</q-expansion-item>
</div>
<!-- to="/admin/user/user" -->
<div v-else>
<q-item clickable v-ripple :inset-level="level">
<q-item-section>
<q-checkbox :label="title" v-model="selection" />
</q-item-section>
</q-item>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HierarchicalCheckboxList',
props: {
id: { type: String, required: true },
title: { type: String, required: false },
caption: { type: String, default: '' },
icon: { type: String, default: '' },
value: { type: Boolean, default: false },
level: { type: Number, default: 0 },
children: { type: Array }
},
data () {
return {
localValue: this.$props.value
}
},
computed: {
selection: {
get: function () {
return this.localValue
},
set: function (newvalue) {
this.localValue = newvalue
this.$emit('checked', this.localValue)
// or this.$emit('checked', {id: this.$props.id, value: this.localValue })
}
}
},
methods: {
primaryCheckChanged (d) {
this.$emit('checked', d)
}
}
}
</script>
What works so far
As a work-around I am able to get the checkbox state emitted with $emit('checked'), which I use to send it to the next process. But the parent's state is not updated until I refresh it back from the database.
How do I update the parent component's state using v-bind.sync recursively?
Appreciate any help!!
UI
Figured out how to do it after I broke the code down from the whole 2000 line code to a separate 'trial-n-error' code of 20 lines and then things became simple and clear.
Menu.vue
A few changes in the parent component in the HierarchicalCheckboxList declaration:
Note the sync property
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="link.value"
v-bind="child">
</HierarchicalCheckboxList>
HierarchicalCheckboxList.vue
Change the same line of code in the child component (as its recursive)
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="child.value"
v-bind="child">
</HierarchicalCheckboxList>
And in the computed set property, emit as below:
this.$emit('update:u', this.localValue)
That's it - parent n children components now stay in snyc.
In my project, I use vue.js.
I want to display content of list with nested loop。 In parent page, i have defined:
<template>
<div>
<detail-header></detail-header>
......
<detail-list></detail-list>
</div>
</template>
The component of detail-list is :
<template>
<div>
<div v-for="(item, index) of list" :key="index">
<div class="item-title border-bottom">
<span class="item-title-icon"></span>
{{item.title}}
</div>
<div v-if="item.children" class="item-children">
<detail-list :list="item.children"></detail-list>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DetailList',
props: {
list: Array
},
data () {
return {
list: [{
title: 'adult',
children: [{title: 'threePeople',children: [{ title: 'threePeople-w'}]}, {title: 'fivePeople'}]
}, {
title: 'student'
}, {
title: 'child'
}, {
title: 'offer'
}]
}
}
}
</script>
unlucky, I got a error message:
Duplicated key 'list' of list: [{ in detail-list
who can help me ?
If you want this to work, keep the list in props (and remove it from DetailList's data) and define in your parent page's data.
So the first DetailList and its children will have the list as a prop.
So you'll have in the parent page :
<template>
<div>
<detail-header></detail-header>
......
<detail-list :list="list"></detail-list>
</div>
</template>
<script>
export default {
name: 'Parent',
data () {
return {
list: [{ ... the list ... }]
}
}