I want to create a drag and drop application using the Vue JS framework. Here is an example of my complete code.
The problem is with the id properties inside the children arrays.
For example, when I drag an object named 'AAA' to another place, everything works fine for me, but when I drag it back, I get an error like - Duplicate keys detected: '0'. This may cause an update error.
I'm pretty sure the problem is inside the oneDrop function
onDrop(e, categoryId) {
const itemId = parseInt(e.dataTransfer.getData('itemId'))
this.categories.map(item => {
item.children = item.children.filter(child => {
if (child.id == itemId) {
child.categoryId = categoryId;
this.categories[categoryId].children.push(child);
}
return child
})
})
}
Of course, I understand that when dragging using the push method, the old object remains and is not deleted, so I get this error, but how to deal with this problem? (Full code at the beginning of the question)
You need to filter list from and add item to list to:
new Vue({
el: "#demo",
data() {
return {
categories: [
{id: 0, title: "This is parent block", children: [{ id: 0, title: "AAA", categoryId: 0 }, { id: 1, title: "BBB", categoryId: 0 },],},
{id: 1, title: "This is parent block", children: [{ id: 2, title: "CCC", categoryId: 1 }, { id: 3, title: "DDD", categoryId: 1 },],},
],
};
},
methods: {
onDrop(e, categoryId) {
const itemId = parseInt(e.dataTransfer.getData("itemId"));
const id = categoryId === 0 ? 1 : 0
const child = this.categories[id].children.find(c => c.id === itemId)
child.categoryId = categoryId;
this.removeFromList(id, itemId)
this.addToList(categoryId, child)
},
addToList(categoryId, child) {
this.categories[categoryId].children = [...this.categories[categoryId].children, child];
},
removeFromList(id, itemId) {
this.categories[id].children = this.categories[id].children.filter(c => c.id !== itemId);
},
onDragStart(e, item) {
e.dataTransfer.dropEffect = "move";
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("itemId", item.id.toString());
},
},
})
.droppable {
padding: 15px;
border-radius: 5px;
background: #2c3e50;
margin-bottom: 10px;
}
.droppable h4 {
color: white;
}
.draggable {
background: white;
padding: 5px;
border-radius: 5px;
margin-bottom: 5px;
}
.draggable h5 {
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<div
v-for="category in categories"
:key="category.id"
#drop="onDrop($event, category.id)"
class="droppable"
#dragover.prevent
#dragenter.prevent
>
<h4>{{ category.title }}</h4>
<div class="child">
<div
v-for="item in category.children.filter(
(x) => x.categoryId === category.id
)"
:key="item.id"
#dragstart="onDragStart($event, item)"
class="draggable"
draggable="true"
>
<h5>{{ item.title }}</h5>
</div>
</div>
</div>
{{categories}}
</div>
Related
I made a really simple file which have a reactive Array of objects. ⇒ all objects have a property called checked, which is a boolean and toggles based on a checkbox.
I'm iterating with a v-for="" the array of employees and rendering them on a <ul/ li.
I'm trying to make a :class just for the ones who got checked, but it's throwing me a syntax error, and I'm not sure where I'm wrong and which would be the best approach. Every comment, advice will be appreciated, here's the code:
<template>
<div class="contaier">
<h1>Employees view</h1>
<ul class="list">
<li
class="li-item"
v-for="employee in employees" :key="employee.id"
:class="{employee.checked: isChecked}"
>
<input class="checkbox" v-model="employee.checked" type="checkbox" #click="checkEmployee">
{{ employee.name }}
</li>
</ul>
</div>
</template>
<script>
import { reactive, ref } from 'vue'
export default {
setup() {
const checked = ref(false)
const employees = reactive([
{id: 1,
name:'Terry Lawrence',
username:'TerryLaw',
email: 'TerryLaw#gmail.com',
address: 'whateverStreet 258',
checked: checked.value
},
{id: 2,
name:'MartyClFly',
username:'MartyMac',
email: 'MartyMac#gmail.com',
address: 'George Junior 300',
checked: checked.value
},
{id: 3,
name:'Nancy Pelosi',
username:'Drunk ho',
email: 'drunkHo#gmail.com',
address: 'Velbedere 400',
checked: checked.value
},
{id: 4,
name:'Jonh Doe',
username:'Jonny',
email: 'JonhDoe#gmail.com',
address: 'NobodyKnows 129',
checked: checked.value
},
{id: 5,
name:'Candace Owens',
username:'the greate black hope',
email: 'Candace#gmail.com',
address: 'Washington Str 777',
checked: checked.value
},
{id: 6,
name:'Charles Dickens',
username:'Charlie',
email: 'CharlieDick#gmail.com',
address: 'chickenNutso 678',
checked: checked.value
}
])
const checkEmployee = (event) => {
try {
for (const employee of employees) {
if (event.target.id !== employee.id) {
checked.value = true
}}
} catch (error) {
console.error(error)
}
}
return {
employees,
checkEmployee,
}
}}
</script>
<style scoped>
.list {
width: 60%;
margin-inline: auto;
padding: 1em;
list-style: none;
}
.li-item {
padding: .5em;
border: 1px solid black;
}
.checkbox {
float: left;
}
.isChecked {
background-color: rgb(191, 236, 122);
}
</style>
the error is here exactly ⇒ <li / element:
Replace
<li class="li-item"
v-for="employee in employees" :key="employee.id"
:class="{employee.checked: isChecked}">
with
<li class="li-item"
v-for="employee in employees" :key="employee.id"
:class="{isChecked: employee.checked}">
So, i tried to dynamically toggle className, based on computed property, but it looks like pug doesn't have access to computed properties. I tried to manually set true to a className, then it's working.
I tried to reassign computed property to pug variable, but it still doesn't work
When using pure html, classes dynamically toggle correctly
Pug:
main#app.container
- var isPinkDefinitely = isPink ? 'pink' : 'gray';
div.section(
v-bind:class=[
'custom-section',
{
'custom-section--pink': isPink
}
]
v-bind:style=[
{
'background-color': isPinkDefinitely
}
]
) {{ isPink }}
form(#submit.prevent="addItem")
label.label Add another
div.field.has-addons
div.control
input.input(required, autofocus, v-model="newItem", placeholder="Remake this in React")
button(type="submit").button.is-info
i.fa.fa-plus
span Add
transition(name="slide")
div(v-show="items.length > 0")
hr
ul
transition-group(name="slide")
li(v-for="(item, index) in items", :key="item.id")
button(#click="removeItem(index)").button.is-danger
i.fa.fa-trash
span(v-text="item.desc")
hr
span(v-text="'Total: ' + items.length")
JS:
new Vue({
el: '#app',
data: {
items: [
{id: 1, desc: "Lorem"},
{id: 2, desc: "Ipsum"},
{id: 3, desc: "Dolor"},
],
newItem: '',
},
computed: {
isPink() {
return true;
}
},
methods: {
addItem () {
const id = this.items.length + 1
this.items.push({id, desc: this.newItem})
this.newItem = ''
},
removeItem (index) {
this.items.splice(index, 1)
},
},
})
https://codepen.io/itprogressuz/pen/qBoePob?editors=1010
UPD:
SO the basically solution was to just write all classes in one line inside just an object. see solution of #aykut
I just tried to solve your problem and I think i successed. You could use variable like me. If you want change it in computed function, it will change dynamically. You could also change it in methods functions when get users events. Here, my solution.
main#app.container
div.section(
class="default-style"
:class="{'bg-pink': isPink }"
) {{ setIsPink }}
form(#submit.prevent="addItem")
label.label Add another
div.field.has-addons
div.control
input.input(required, autofocus, v-model="newItem", placeholder="Remake
this in React")
button(type="submit").button.is-info
i.fa.fa-plus
span Add
transition(name="slide")
div(v-show="items.length > 0")
hr
ul
transition-group(name="slide")
li(v-for="(item, index) in items", :key="item.id")
button(#click="removeItem(index)").button.is-danger
i.fa.fa-trash
span(v-text="item.desc")
hr
span(v-text="'Total: ' + items.length")
// css file
.default-style{
background-color: gray;
}
.bg-pink{
background-color: pink;
}
// js file
new Vue({
el: '#app',
data: {
isPink: false,
items: [
{id: 1, desc: "Lorem"},
{id: 2, desc: "Ipsum"},
{id: 3, desc: "Dolor"},
],
newItem: '',
},
computed: {
setIsPink() {
this.isPink = !this.isPink;
return this.isPink;
}
},
methods: {
addItem () {
const id = this.items.length + 1
this.items.push({id, desc: this.newItem})
this.newItem = ''
},
removeItem (index) {
this.items.splice(index, 1)
},
},
})
hi everyone I have data given below by using this data I just want to create a cart click on this link to check what kind of cart I want to design from this data
const result = [
{
name: 'shanu',
label: ['ak', 'pk', 'plk', 'k'],
value: [1, 2, 3, 5],
},
];
// what i did
{result.map((el) => {
return (
<div>
<h1>{el.name}</h1>
<div className="vart">
<div>
{el.label.map((e) => {
return <p>{e}</p>;
})}
</div>
<div>
{el.value.map((e) => {
return <p>{e}</p>;
})}
</div>
</div>
</div>
);
})}
.vart {
display: flex;
flex-direction: row;
}
You can access the value according to the index of the label as below. You can use a CSS grid system to show a two-column view.
export default function App() {
const result = [
{
name: "shanu",
label: ["ak", "pk", "plk", "k"],
value: [1, 2, 3, 5]
}
];
return result.map((el) => {
return (
<div>
<div className="header">{el.name}</div>
<div className="grid-container">
{el.label.map((e, index) => {
return (
<div
className="grid-item"
style={{ textAlign: index % 2 === 0 ? "left" : "right" }}
>
{e} : {el.value[index]}
</div>
);
})}
</div>
</div>
);
});
}
Following styles will organise the grid with right aligning the second column.
.header {
color: #ffffff;
background-color: #4473c4;
padding: 10px 20px;
}
.grid-container {
display: grid;
grid-template-columns: auto auto;
}
.grid-item {
padding: 10px 20px;
color: #ffffff;
background-color: #91cf50;
}
HTML Output
Code Sandbox
If you want to link value and label this way:
ak => 1
pk => 2
plk => 3
k => 5
It would be better practice to change your data structure and move them aside. It avoids running in cases where label[x] is defined, but value[x] is not:
export default function App() {
const result = [
{
name: "shanu",
items: [
{ label: "ak", value: 1 },
{ label: "pk", value: 2 },
{ label: "plk", value: 3 },
{ label: "k", value: 5 },
],
}
];
return result.map((el) => {
return (
<div>
<h1>{el.name}</h1>
<div className="vart">
<div>
{el.items.map((e, index) => {
return (
<p>
{e.label} : {e.value}
</p>
);
})}
</div>
</div>
</div>
);
});
}
I'm trying to display the OfferName upon user clicking on a list of images that each have their own data (an array of objects) using jQuery. I've achieved this using plain JS but I want to know how to achieve this with jQuery.
How can I make it so that upon user clicking on the image, the individual and correct OfferName appears in the console? This one confused me a lot even though I tried reverse engineering the plain JS :(.
We can assume the array of objects looks like this below:
var offers = [ {
id: 5933,
desc: "game",
OfferName: "blitz",
Template: "nonfeatured",
SortIndex: 0
}, {
id: 5934,
desc: "game",
OfferName: "blitz - 2",
Template: "featured",
SortIndex: 0
}, {
id: 5935,
desc: "game",
OfferName: "blitz - 3",
Template: "special",
SortIndex: 0
}];
$(".items").on("click", function () {
var offerName = "";
for(var i = 0; i < offers.length; i++) {
offerName = offers[i].OfferName;
$(this).on("click", function (offerName) {
console.log("user clicked " + offerName);
});
}
}
Not sure how you're building your HTML, but you can accomplish your goal using data-attributes and event delegation
Here's an example
var offers = [{
id: 5933,
desc: "game",
OfferName: "blitz",
Template: "nonfeatured",
SortIndex: 0
}, {
id: 5934,
desc: "game",
OfferName: "blitz - 2",
Template: "featured",
SortIndex: 0
}, {
id: 5935,
desc: "game",
OfferName: "blitz - 3",
Template: "special",
SortIndex: 0
}];
offers.forEach(o => {
let t = `<div class='tpl item' data-offername='${o.OfferName}' data-id='${o.id}'>Click on this if you're interested in ${o.OfferName}</div>`;
$('.container').append(t);
})
$(document).on("click", ".item", function() {
console.log(`user clicked: ${$(this).data('offername')}, id: ${$(this).data('id')}`);
})
.item {
padding: 5px;
margin-bottom: 5px;
cursor: pointer;
border: 1px solid #ccc;
background: #f0f0f0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='container'></div>
In Vue.js, in order to add a property/array item to something already in the virtual DOM, you have to use the $set function.
Here are the wrong ways:
Object: this.myObject.newProperty = "value";
Array: this.myArray[3] = object;
Here's the right way:
Object: this.$set(this.myObject, "newProperty", "value");
Array: this.$set(this.myArray, 3, object);
My question is how do you $set a property of all objects in an array?
Here's the wrong way:
for (var i = 0; i < this.myArray.length; i++) {
this.myArray[i].newProperty = "value";
}
So, what's the method for me to use $set to do this?
A bit tweaked code of yours works:
new Vue({
el: "#app",
data: {
todos: [{
text: "Learn JavaScript",
done: false
},
{
text: "Learn Vue",
done: false
},
{
text: "Play around in JSFiddle",
done: true
},
{
text: "Build something awesome",
done: true
}
]
},
methods: {
toggle: function(todo) {
todo.done = !todo.done
},
changeProperty1() {
const val = 'A'
// this is your code a bit modified
// defining length (and using it in the comparison) is a
// bit of optimization, not required
for (var i = 0, length = this.todos.length; i < length; i++) {
this.$set(this.todos[i], 'property1', val);
}
},
changeProperty1Again() {
for (todo of this.todos) {
if (todo.property1) {
todo.property1 = 'B'
}
}
}
},
created() {
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
del {
color: rgba(0, 0, 0, 0.3);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h2>Todos:</h2>
<ol>
<li v-for="todo in todos">
<label>
<input type="checkbox"
v-on:change="toggle(todo)"
v-bind:checked="todo.done">
<del v-if="todo.done">
{{ todo.text }}
</del>
<span v-else>
{{ todo.text }}
</span>
<span>
{{ todo.property1 }}
</span>
</label>
</li>
</ol>
<button #click="changeProperty1">Click this first</button>
<button #click="changeProperty1Again">Click this second</button>
</div>
Sorry for the lengthy snippet, I just copied it over from JSFiddle :)
You keep doing this.$set(this.myArray, 3, object); in a loop using the index.
Something like this after modifying your object.
var newObject = Object.assign({}, this.myArray[i], {newProperty: 'value'} ); // Immutable object created
this.$set(this.myArray, i, newObject);
This will be inefficient as it will call $set for each iteration.
so you can do a map on your array and return a new Object from inside.
const newArray = myArray.map(object => {
return Object.assign({}, object, {newProperty: 'value'} );
//or by ES6 spread operator
return {...object, newProperty: 'value'};
});
Then set your array for Vuejs to re-render.
Hope, this will give the idea. Although context(this) may vary depending on how you're implementing!
You simply just wanna add a new property to objects in an array. Not set a new value to the array by their index. You can do the following:
new Vue({
el: '#demo',
data: {
myArray: [
{id: 1},
{id: 2},
{id: 3},
{id: 4},
{id: 5}
]
},
methods: {
addProperties() {
for (var i = 0; i < this.myArray.length; i++) {
this.$set(this.myArray[i], 'newProperty', 5 - i)
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<div v-for="item in myArray" :key="item.id">
<span>{{item.id}}: </span>
<span v-if="item.newProperty">{{item.newProperty}}</span>
</div>
<button #click="addProperties">Add Properties</button>
</div>
Not really a vue thing - just plain JS:
arr.map(obj => { return {...obj, newProperty: "sameValueAsOthers"}});