Updating Vuex following Drag and Drop using vuedraggable - vue.js

I have created a Vue application that uses Vuetify, Vuex and vuedraggable. The component functions as expected with drag and drop of v-card containing the associated information but I have not been able to find a way to identify the destination so that I can update the Vuex Store
<template>
<v-container>
<v-row justify='center'>
<v-col v-for="stage in stages" :key="stage.value">
<span class="card-text-bold">{{ stage.heading }}</span>
<draggable :list="buckets[stage.name]" group="openTasks" :move="handleStatus">
<v-card color="commentCard" class="list-group mt-3" v-for="task in buckets[stage.name]" :key="task._id">
<v-card-text>
<span class="card-text-bold">{{ task.title}}</span>
</v-card-text>
<v-card-text>
<span class="card-text"> {{ task.description}} </span>
</v-card-text>
<v-card-text>
{{ task.assignee}}<v-icon right #click="goToTask(task._id)">edit</v-icon>
</v-card-text>
</v-card>
</draggable>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { mapGetters } from 'vuex';
import draggable from 'vuedraggable';
import moment from 'moment';
export default {
name: 'Tasks',
components: {
draggable
},
data() {
return {
stages: [
{ heading: 'Created', name: 'created' },
{ heading: 'Assigned', name: 'assigned' },
{ heading: 'In Progress', name: 'in progress' },
{ heading: 'On Hold', name: 'on hold' },
{ heading: 'Complete', name: 'complete' },
{ heading: 'Closed', name: 'closed' }
]
};
},
created() {
this.handleGetTasks();
},
computed: {
...mapGetters(['user', 'loading', 'tasks', 'buckets'])
},
methods: {
getTimeFromNow(time) {
return moment(new Date(time)).fromNow();
},
goToTask(taskId) {
this.$router.push(`/task/${taskId}`);
},
handleGetTasks() {
const userRole = localStorage.getItem('role');
const fullname = localStorage.getItem('fullname');
switch (userRole) {
case 'Manager': {
this.$store.dispatch('getAllTasks');
break;
}
case 'Requester': {
this.$store.dispatch('getRequestTasks', {
fullname: fullname
});
break;
}
case 'Assignee': {
this.$store.dispatch('getAssignTasks', {
fullname: fullname
});
break;
}
}
},
stateTasks(target) {
console.log('state', target);
if (this.buckets[target] > 0) return this.buckets[target];
else return [];
},
handleStatus(evt) {
const movedId = evt.draggedContext.element._id;
var targetStage;
console.log('source', movedId, 'target', Object.keys(evt.relatedContext.list));
}
}
};
</script>
The source list for data used here is the 'buckets' object which contains an array of tasks for each of the task stages. Drag and Drop moves the task cards from the source to the target but I have been able to find a way to identify the target list.
Please provide advice about how I can determine which of the lists in the 'buckets' object should be updated following the move.
Thanks
Des

If you take a look at the documentation, you'll see that vuedraggable has a #end event which will trigger after moving an element. When logging that event you'll notice quite a lot of useful information - such as oldIndex, newIndex and more..
<draggable :list="list" #end="onEnd">
Source: https://github.com/SortableJS/Vue.Draggable#events

I was able to solve the problem by parsing the HTML element of the move event.
The header for each of the stage columns can then be used as the identifier of the target list.
Here is the code
<template>
<v-container>
<v-row justify='center'>
<v-col v-for="(stage, index) in stages" :key='index'>
<draggable :list="buckets[stage]" group="openTasks" :move='handleMove'>
<span slot="header" :tag="stage" class="card-text-bold">{{ stage }}</span>
<v-card color="commentCard" class="list-group mt-3" v-for="task in buckets[stage]" :key="task._id">
<v-card-text>
<span class="card-text-bold">{{ task.title}}</span>
</v-card-text>
<v-card-text>
<span class="card-text"> {{ task.description}} </span>
</v-card-text>
<v-card-text>
{{ task.assignee}}<v-icon right #click="goToTask(task._id)">edit</v-icon>
</v-card-text>
</v-card>
</draggable>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { mapGetters } from 'vuex';
import draggable from 'vuedraggable';
import moment from 'moment';
export default {
name: 'Tasks',
components: {
draggable
},
data() {
return {
stages: ['created', 'assigned', 'in progress', 'on hold', 'complete', 'archive']
};
},
created() {
this.handleGetTasks();
},
computed: {
...mapGetters(['user', 'loading', 'tasks', 'buckets'])
},
methods: {
getTimeFromNow(time) {
return moment(new Date(time)).fromNow();
},
goToTask(taskId) {
this.$router.push(`/task/${taskId}`);
},
handleGetTasks() {
const userRole = localStorage.getItem('role');
const fullname = localStorage.getItem('fullname');
switch (userRole) {
case 'Manager': {
this.$store.dispatch('getAllTasks');
break;
}
case 'Requester': {
this.$store.dispatch('getRequestTasks', {
fullname: fullname
});
break;
}
case 'Assignee': {
this.$store.dispatch('getAssignTasks', {
fullname: fullname
});
break;
}
}
},
handleMove(evt) {
const movedId = evt.draggedContext.element._id;
var stageTag = evt.relatedContext.component.rootContainer.firstElementChild.innerText;
if (this.stages.includes(stageTag)) {
this.$store.dispatch('changeStatus', {
taskId: movedId,
status: stageTag
});
}
}
}
};
Decoding the component HTML is done with this
evt.relatedContext.component.rootContainer.firstElementChild.innerText;

Related

Vuejs Error: Property or method "cart" is not defined on the instance but referenced during render. Property"cartItems" is not defined

I have an error trying to get data from my vuex store.
Property or method "cart" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
Property or method "cartItems" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property
pages/cart.vue
<template>
<div>
<v-container>
<section>
<h2 class="my-4">Your Cart</h2>
<v-divider class="my-4"></v-divider>
<h3>Total Amount: ${{ cart }}</h3>
<CartItem
v-for="item in cartItems"
:key="item.productId"
:prod-id="item.productId"
:name="item.name"
:image="item.image"
:price="item.price"
:qty="item.qty"
></CartItem>
</section>
</v-container>
</div>
</template>
<script>
import CartItem from '../components/CartItem'
import { mapGetters } from 'vuex'
export default {
components: {
CartItem,
computed: {
...mapGetters('cart',['totalSum'])
},
cartItems(){
return this.$store.getters['cart/products'];
}
}
}
</script>
<style scoped>
h2{
text-align: center;
}
h3{
text-align: center;
}
</style>
./components/CartItem.vue
<template>
<v-container class="my-5">
<v-row>
<v-col
sm="6"
md="4"
>
<v-card outlined>
<v-img :src="image" height="200px" />
<v-card-title> {{ name}} </v-card-title>
<v-card-subtitle> ${{ price }}</v-card-subtitle>
<v-card-subtitle> Quantity <strong>{{ qty }}</strong> </v-card-subtitle>
<v-card-actions>
<div>Total: ${{ itemTotal }}</div>
<v-btn #click="remove()" color="success" outlined >
<v-icon small left> add </v-icon>
Remove
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
props: ['prodId', 'name', 'image', 'price', 'qty'],
computed: {
itemTotal() {
return( this.price * this.qty).toFixed(2);
}
},
methods: {
remove(){
this.$store.dispatch('cart/removeFromCart', {productId: this.prodId})
}
}
}
</script>
store/modules/cart.js
export default {
namespaced: true,
state(){
return{
items: [],
total: 0,
qty: 0
}
},
mutations: {
addProductToCart(state, payload) {
const productData = payload;
const productInCartIndex = state.items.findIndex(
(ci) => ci.productId === productData.id
);
if (productInCartIndex >= 0) {
state.items[productInCartIndex].qty++;
} else {
const newItem = {
productId: productData.id,
title: productData.title,
image: productData.image,
price: productData.price,
qty: 1,
};
state.items.push(newItem);
}
state.qty++;
state.total += productData.price;
},
removeProductFromCart(state, payload) {
const prodId = payload.productId;
const productInCartIndex = state.items.findIndex(
(cartItem) => cartItem.productId === prodId
);
const prodData = state.items[productInCartIndex];
state.items.splice(productInCartIndex, 1);
state.qty -= prodData.qty;
state.total -= prodData.price * prodData.qty;
},
},
actions: {
addToCart(context, payload) {
const prodId = payload.id;
const products = context.rootGetters['prods/products'];
const product = products.find(prod => prod.id === prodId);
context.commit('addProductToCart', product);
},
removeFromCart(context, payload) {
context.commit('removeProductFromCart', payload);
}
},
getters: {
products(state) {
return state.items;
},
totalSum(state) {
return state.total;
},
quantity(state) {
return state.qty;
}
}
}
The property cartItems should be added to the computed option like :
export default {
components: {
CartItem,
},
computed: {
...mapGetters('cart',['totalSum']),
cartItems(){
return this.$store.getters['cart/products'];
}
}
}

Vuetify v-dialog not showing the second time

I'm trying to to conditionally show a dialog. The dialog shows up the first time just fine.
But when second time I try to remount the component I can't see the dialog.
The dialog also fetches some data when it is mounted. I can see the logs that the dialog is being mounted and unmounted and also the network request on chrome devtools network tab but I can't see the dialog.
index.vue
<v-list-item-title #click="changeDialogState">
<TestDialog
v-if="showEditDialog"
:id="item.id"
#editDone="changeDialogState"/>
Edit
</v-list-item-title>
------------------------
//This is defined inside methods
changeDialogState() {
this.showEditDialog = !this.showEditDialog // this.showEditDialog is just a boolean value define inside data()
},
Testdialog.vue
<template>
<v-dialog v-model="dialog" width="700">
<v-progress-circular
v-if="fetching"
:size="70"
:width="7"
color="purple"
indeterminate
></v-progress-circular>
<v-card v-else>
<v-card-title> New Customer </v-card-title>
<v-divider></v-divider>
<v-card-text>
<customer-form
v-model="customer"
:errors="errors"
:initial-customer="customer"
#submit="editCustomer"
>
<template v-slot:footer>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text #click="$emit('editDone')"> Cancel </v-btn>
<v-btn color="primary" type="submit" class="ma-1">
Save Customer
</v-btn>
</v-card-actions>
</template>
</customer-form>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import { formHandling, paginatedResponse } from '~/mixins'
export default {
mixins: [formHandling, paginatedResponse],
props: {
value: {
type: Object,
default: () => ({
//some stuff here
}),
},
error: {
type: Array,
required: true,
default: () => {
return []
},
},
id: {
type: String,
required: true,
},
},
async fetch() {
this.fetching = true
this.customer = await this.$axios.$get(`/customer/${this.id}/`)
this.fetching = false
},
data() {
return {
dialog: true,
customer: {
...
},
errors: {
...
},
fetching: true,
}
},
mounted() {
console.log('TestDialog - mounted')
},
beforeDestroy() {
console.log('TestDialog - unmounted')
},
methods: {
async editCustomer(customer) {
try {
await this.$axios.$put(`/customer/${this.id}/`, customer)
this.dialog = false
} catch (err) {
this.setErrors(err)
}
},
},
}
</script>

want to use vuetify snackbar as a global custom component in vuejs

i used snackbar to show success messages in vuejs. i want to make a global custom snackbar component.
<template>
<div name="snackbars">
<v-snackbar
v-model="snackbar"
:color="color"
:timeout="timeout"
:top="'top'"
>
{{ text }}
<template v-slot:action="{ attrs }">
<v-btn dark text v-bind="attrs" #click="snackbar = false">
Close
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
export default {
props: {
snackbar: {
type: Boolean,
required: true,
},
color: {
type: String,
required: false,
default: "success",
},
timeout: {
type: Number,
required: false,
default: 3000,
},
text: {
type: String,
required: true,
},
},
};
</script>
then i import this as a component in my every form like this.
<SnackBar :snackbar="snackbar" :color="color" :text="text" />
but my issue is i can't use snackbar as a prop in my child component. it shows me this error.
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "snackbar"
how can i fix this issue. can anyone help me?
I realize this is old, but thanks to google, I am going to add my solution.
I use this, because I don't see the point of using vuex for a snackbar. It's more work then needed.
Create a vue component named vtoast
<template>
<v-snackbar
:color="color"
:timeout="timer"
v-model="showSnackbar"
bottom
right
>
<v-icon left>{{icon}}</v-icon>{{message}}
</v-snackbar>
</template>
<script>
export default {
name: "vtoast",
data() {
return{
showSnackbar: false,
message: '',
color: 'success',
icon: 'mdi-check',
timer: 3000
}
},
methods:{
show(data) {
this.message = data.message || 'missing "message".'
this.color = data.color || 'success'
this.timer = data.timer || 3000
this.icon = data.icon || 'mdi-check'
this.showSnackbar = true
}
}
}
</script>
Somewhere in the root of your main app, add the following. (I usually put mine in App.vue)
<template>
...
<!-- toast -->
<vtoast ref="vtoast"/>
...
</template>
<script>
import vtoast from '#/your/vtoast/directory/vtoast'
export default{
name: 'App', //or whatever your root is
components:{
vtoast
},
mounted() {
this.$root.vtoast = this.$refs.vtoast
},
}
</script>
And access it like so...
this.$root.vtoast.show()
this.$root.vtoast.show({message: 'Ahoy there!'})
i found a way to fix my solution using vuex.
<template>
<div name="snackbars">
<v-snackbar v-model="show" :color="color" :timeout="timeout" :top="'top'">
{{ text }}
<template v-slot:action="{ attrs }">
<v-btn dark text v-bind="attrs" #click="show = false">
Close
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
export default {
created() {
this.$store.subscribe((mutation, state) => {
if (mutation.type === "snackbar/SHOW_MESSAGE") {
this.text = state.snackbar.text;
this.color = state.snackbar.color;
this.timeout = state.snackbar.timeout;
this.show = true;
}
});
},
data() {
return {
show: false,
color: "",
text: "",
timeout: 0,
};
},
};
</script>
in my vuex module i wrote like this
export default {
namespaced: true,
state: {
text: "",
color: "",
timeout: "",
},
mutations: {
SHOW_MESSAGE(state, payload) {
state.text = payload.text;
state.color = payload.color;
state.timeout = payload.timeout;
},
},
actions: {
showSnack({ commit }, payload) {
commit("SHOW_MESSAGE", payload);
},
},
};
then i import snackbar child component into my parent component and send data like this.
...mapActions("snackbar", ["showSnack"]),
saveDetails() {
this.showSnack({
text: "Successfully Saved!",
color: "success",
timeout: 3500,
});
}
Another solution is to use a computed value with getter and setter.
Using options api
<template>
<v-snackbar v-model="show" :color="color">
{{ message }}
<template v-slot:action="{ attrs }">
<v-btn text v-bind="attrs" #click="show = false">Close</v-btn>
</template>
</v-snackbar>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters({
message: 'snackbar/message',
color: 'snackbar/color'
}),
show: {
get() {
return this.$store.state.snackbar.show
},
set(v) {
this.$store.commit('snackbar/SET_SHOW', v)
}
}
}
}
</script>
Using composition api plugin
<template>
<v-snackbar v-model="show" :color="color">
{{ message }}
<template v-slot:action="{ attrs }">
<v-btn text v-bind="attrs" #click="show = false">Close</v-btn>
</template>
</v-snackbar>
</template>
<script>
import { defineComponent, computed } from '#vue/composition-api';
export default defineComponent({
setup(_props, { root }) {
const show = computed({
get: () => root.$store.state.snackbar.show,
set: (v) => root.$store.commit('snackbar/SET_SHOW', v),
});
const message = computed(() => root.$store.state.snackbar.message);
const color = computed(() => root.$store.state.snackbar.color);
return {
show,
message,
color,
};
},
});
</script>
A better implementation using composables here https://gist.github.com/wobsoriano/2f3f0480f24298e150be0c13f93bac20
You are having a prop and the same in data.
remove snackbar from data() as it is available from prop.
<script>
export default {
props: {
snackbar: {
type: Boolean,
required: true,
},
color: {
type: String,
required: false,
default: "success",
},
timeout: {
type: Number,
required: false,
default: 3000,
},
text: {
type: String,
required: true,
},
}
};
</script>
This is what I did with Options API with mere props and events;
Here is the Snackbar.vue component
<template>
<div class="text-center">
<v-snackbar
transition="true"
bottom
right
v-model="show"
:color="snackbar.color"
:timeout="snackbar.timeout"
class="snackbar-shadow"
>
<div class="d-flex align-start alert-notify">
<v-icon size="24" class="text-white mr-5">{{ snackbar.icon }}</v-icon>
<p class="mb-0">
<span class="font-size-root font-weight-600">{{
snackbar.title
}}</span>
<br />
{{ snackbar.message }}
</p>
</div>
<template v-slot:action="{ attrs }">
<v-btn
icon
elevation="0"
max-width="136"
:ripple="false"
height="43"
class="font-weight-600 text-capitalize py-3 px-6 rounded-sm"
color="rgba(255,255,255, .85)"
text
v-bind="attrs"
#click="show = false"
>
<v-icon size="13">fas fa-times</v-icon>
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
export default {
name: "snackbar",
props: {
snackbar: Object,
},
computed: {
show: {
get() {
return this.snackbar.visible;
},
set(value) {
this.$emit("closeSnackbar", value);
},
},
},
};
</script>
Here is the App.vue component
<template>
<!-- Snackbar -->
<snackbar :snackbar="snackbar" #closeSnackbar="SnackbarClose"></snackbar>
</template>
<script>
export default {
name: "app",
data() {
return {
snackbar: {
visible: false,
timeout: 2000,
color: "#11cdef",
title: "Hello",
message: null,
icon: "fas fa-bell",
},
};
},
created: { this.SnackbarShow(); }
methods: {
SnackbarShow() {
this.snackbar.visible = true;
this.snackbar.message = "Hola!👋 I'm a snackbar";
},
SnackbarClose() {
this.snackbar.visible = false;
},
},
};
</script>

Vuejs - Same component with different data

I am trying to re-use the same component, but load the components with different data. I thought simply providing unique id's would do the trick, but no luck. I switched from a Vuex store for this data, to using a dataShare This is what I'm doing:
The components:
<logoHeader :panel=0 title="Add your logo / header" id="top" topPadding="pt-10" />
<logoHeader :panel=1 title="Add your main image" id="main" topPadding="pt-0"/>
So its the exact same component, with some different props and different ids
This is the logoHeader component:
<template>
<v-row
:class="topPadding"
align="center"
justify="center"
>
<v-col
align="center"
justify="center"
cols="12"
>
<v-hover v-slot:default="{ hover }">
<v-card
:elevation="hover ? 12 : 0"
class="mx-auto"
max-width="600"
>
<v-img
v-if="showImage"
:src="imageUrl"
max-width="600px"
class="pointer"
#click="panel = 0"
>
</v-img>
<v-icon
v-if="!showImage"
class="my_dark_purple_text"
size="100"
#click="sendToPanel"
>add_box</v-icon>
<p class="my_dark_purple_text">{{ title }}</p>
<p>URL {{ imageUrl }}</p>
<p>Show image? {{ showImage }}</p>
</v-card>
</v-hover>
</v-col>
</v-row>
</template>
<script>
import {mapGetters} from 'vuex';
import {mapActions} from 'vuex';
import {dataShare} from '../../../packs/fresh-credit.js';
export default {
props: ['panel', 'title', 'topPadding'],
data() {
return {
imageUrl: "",
showImage: false,
}
},
created() {
dataShare.$on('imageUrl', (data) => {
this.imageUrl = data;
});
dataShare.$on('showImage', (data) => {
this.showImage = data;
});
},
computed: {
...mapGetters('emailPanel', [
'returnPanel'
]),
},
methods: {
...mapActions('emailPanel', [
'updatePanel'
]),
sendToPanel() {
this.updatePanel(this.panel);
},
},
}
</script>
And then finally this is where the data enters the system:
<template>
<v-expansion-panel-content>
<h1 class="subtitle-1 font-weight-bold">Only images files accepted</h1>
<v-file-input
v-model="image"
accept="image/*"
label="Image Upload"
prepend-icon="add_a_photo"
color='#68007d'
></v-file-input>
<v-btn
:disabled="disableUpload"
color="#68007d"
class="white--text"
#click="sendImage"
>Submit</v-btn>
</v-expansion-panel-content>
</template>
<script>
import axios from 'axios';
import {dataShare} from '../../../../packs/fresh-credit.js';
export default {
data() {
return {
image: null,
disableUpload: true,
}
},
watch: {
image: function() {
if(this.image.size > 0){
this.disableUpload = false;
}
else{
this.disableUpload = true;
}
}
},
computed: {
},
methods: {
sendImage() {
let formData = new FormData();
formData.append('file', this.image);
axios.post('/admin/features/images', formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
).then(response => {
dataShare.$emit('imageUrl', response.data);
dataShare.$emit('showImage', true);
});
}
},
}
</script>
Where did I go astray?
Add the key property to the components and set it to different values (for example 1 and 2). If the key has different values Vue will differentiate them when rendering. Here is a basic explanation.

VueJS - How to pass function to global component

I have a confirm dialog, which should be shown when users perform delete action. I need to make it works globally (Many pages can use this component by passing confirm message and delete function to it). However, I haven't found a way to pass a function to this component.
Thanks in advance!
ConfirmDialog component:
<template>
<v-dialog
v-model="show"
persistent
max-width="350"
>
<v-card>
<v-card-text class="text-xs-center headline lighten-2" primary-title>
{{ message }}
</v-card-text>
<v-card-actions class="justify-center">
<v-btn color="back" dark #click="close">キャンセル</v-btn>
<v-btn color="primary" dark>削除</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
data () {
return {
show: false,
message: ''
}
},
created: function () {
this.$store.watch(state => state.confirmDialog.show, () => {
const msg = this.$store.state.confirmDialog.message
if (msg !== '') {
this.show = true
this.message = this.$store.state.confirmDialog.message
} else {
this.show = false
this.message = ''
}
})
},
methods: {
close () {
this.$store.commit('closeDialog')
}
}
}
</script>
ConfirmDialog store:
export default {
state: {
show: false,
message: '',
submitFunction: {}
},
getters: {
},
mutations: {
showDialog (state, { message, submitFunction }) {
state.show = true
state.message = message
state.submitFunction = submitFunction
},
closeDialog (state) {
state.show = false
state.message = ''
}
}
}
you can get and set states easily.
try getting the value of show with ...mapState
ConfirmDialog.vue :
<template>
<v-dialog
v-if="show"
persistent
max-width="350"
>
<v-card>
<v-card-text class="text-xs-center headline lighten-2" primary-title>
{{ message }}
</v-card-text>
<v-card-actions class="justify-center">
<v-btn color="back" dark #click="close">キャンセル</v-btn>
<v-btn color="primary" dark>削除</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { mapState } from 'vuex';
export default {
data () {
return {
show: false,
message: ''
}
},
methods: {
close () {
this.$store.commit('closeDialog')
}
},
computed: {
...mapState({
show: 'show'
})
}
}
</script>
The store is, as the name says, a store. You have a centralized tree where you save data, not functionalities. Another reason is that functions are not serializable.
You could create this component in a global way by injecting the function as prop or by using emit and handling the functionality in the parent.