Vue testing with Vuetify. Cannot read property 'title' of undefined - vue.js

I am trying to test if the dialog created with vuetify will be active or not after I emit a function called closeNoteForm. However, when I try to test the dialog content being hidden or not, I get an error. It seems the problem is within the noteForm wrapper that I created. But it just points to the css and doesn't make much sense to me.
In my NoteForm component
<template>
<v-card class="note-form-container">
<v-text-field
v-model="title"
placeholder="Note Title"
hide-details
class="font-weight-bold text-h5 pa-2"
flat
solo
color="yellow"
>
</v-text-field>
<vue-tags-input
v-model="tag"
:tags="tags"
#tags-changed="(newTags) => (tags = newTags)"
/>
<div class="mt-2">
<input
type="file"
id="uploadImg"
style="display: none"
multiple
accept="image/*"
v-on:change="handleFileUploads"
/>
<label class="text-button pa-2 upload__label ml-3 mt-2" for="uploadImg"
>Upload Image</label
>
</div>
<v-container>
<v-row justify="space-around"
><div v-for="(image, index) in allImages" :key="index">
<div style="position: relative">
<v-img
:src="image"
height="70px"
width="70px"
contain
class="mx-2 uploaded__image"
#click="openImage(image)"
></v-img>
<v-btn
icon
color="pink"
class="close__button"
#click="handleDeleteButton(image)"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
</div>
<v-spacer></v-spacer>
</v-row>
</v-container>
<v-textarea
v-model="text"
clearable
clear-icon="mdi-close-circle"
no-resize
hide-details
solo
flat
></v-textarea>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text #click="closeForm()" class="close__btn"> Close </v-btn>
<v-btn color="secondary darken-2" text #click="saveNote"> Save </v-btn>
</v-card-actions>
<v-dialog v-model="dialog" width="500" dark>
<v-img :src="selectedImage" #click="dialog = false"></v-img>
</v-dialog>
<v-dialog
v-model="imageDeletionDialog"
width="500"
class="image_delete_dialog"
>
<v-img :src="selectedImage"></v-img>
<v-btn color="red darken-1" text #click="deleteImage">
Delete Image
</v-btn>
<v-btn text #click="imageDeletionDialog = false"> Close </v-btn>
</v-dialog>
</v-card>
</template>
<script>
import { EventBus } from "../event-bus";
import VueTagsInput from "#johmun/vue-tags-input";
import { v4 as uuidv4 } from "uuid";
import dbService from "../services/db_service";
export default {
name: "NoteForm",
components: { VueTagsInput },
data: () => {
return {
text: "",
title: "",
tag: "",
tags: [],
currentNoteID: null,
allImages: [],
dialog: false,
selectedImage: "",
imageDeletionDialog: false,
};
},
mounted() {
EventBus.$on("editNote", (noteToEdit) => {
this.fillNoteForm(noteToEdit);
});
},
methods: {
openImage(image) {
this.selectedImage = image;
this.dialog = true;
},
handleDeleteButton(image) {
this.imageDeletionDialog = true;
this.selectedImage = image;
},
deleteImage() {
this.allImages = this.allImages.filter(
(img) => img !== this.selectedImage
);
this.imageDeletionDialog = false;
this.selectedImage = "";
},
handleFileUploads(e) {
const images = e.target.files;
let imageArray = [];
for (let i = 0; i < images.length; i++) {
const reader = new FileReader();
const image = images[i];
reader.onload = () => {
imageArray.push(reader.result);
};
reader.readAsDataURL(image);
}
this.allImages = imageArray;
e.target.value = "";
},
saveNote() {
if (this.title.trim() === "") {
alert("Please enter note title!");
return;
}
if (this.text === null) this.text = "";
if (this.currentNoteID === null) {
this.createNewNote();
} else {
this.updateNote();
}
this.resetForm();
},
createNewNote() {
const tagList = this.tags.map((tag) => {
return tag.text;
});
const uuid = uuidv4();
const newNote = {
title: this.title,
tags: this.tags,
text: this.text,
uuid,
date: new Date().toLocaleString(),
tagList,
allImages: this.allImages,
};
dbService.addNote(newNote);
EventBus.$emit("addNewNote", newNote);
EventBus.$emit("closeNoteForm");
},
updateNote() {
const tagList = this.tags.map((tag) => {
return tag.text;
});
let updatedNote = {
tags: this.tags,
uuid: this.currentNoteID,
text: this.text,
title: this.title,
tagList: tagList,
allImages: this.allImages,
};
dbService.updateNote(updatedNote);
EventBus.$emit("updateNote", updatedNote);
},
resetForm() {
this.tags = [];
this.text = "";
this.title = "";
this.currentNoteID = null;
this.allImages = [];
this.selectedImage = "";
},
closeForm() {
if (this.currentNoteID !== null) {
EventBus.$emit("openNoteView", this.currentNoteID);
}
this.resetForm();
**EventBus.$emit("closeNoteForm");**
},
fillNoteForm(noteToEdit) {
this.text = noteToEdit.text;
this.title = noteToEdit.title;
this.tags = noteToEdit.tags;
this.currentNoteID = noteToEdit.uuid;
this.allImages = noteToEdit.allImages;
},
},
beforeDestroy() {
EventBus.$off("fillNoteForm", this.fillNoteForm);
},
};
</script>
<style lang="scss" >
.uploaded__image {
position: relative;
cursor: pointer;
}
.close__button {
position: absolute;
top: 0;
right: 0;
}
.upload__label {
background-color: gray;
cursor: pointer;
&:hover {
background-color: lightgrey;
}
}
.note-form-container {
scrollbar-width: none;
}
.v-input__control,
.v-input__slot,
.v-text-field__slot {
height: 100% !important;
}
.v-textarea {
height: 350px !important;
}
.vue-tags-input {
max-width: 100% !important;
border: none;
background: transparent !important;
}
.v-dialog {
background-color: rgb(230, 230, 230);
}
.vue-tags-input .ti-tag {
position: relative;
}
.vue-tags-input .ti-input {
padding: 4px 10px;
transition: border-bottom 200ms ease;
border: none;
height: 50px;
overflow: auto;
}
.note-form-container.theme--dark {
.vue-tags-input .ti-tag {
position: relative;
background: white;
color: black !important;
}
.vue-tags-input .ti-new-tag-input {
color: #07c9d2;
}
}
.note-form-container.theme--light {
.vue-tags-input .ti-tag {
position: relative;
background: black;
color: white !important;
}
.vue-tags-input .ti-new-tag-input {
color: #085e62;
}
}
</style>
My Note component
<template>
<v-dialog width="500px" v-model="dialog">
<template v-slot:activator="{ on, attrs }">
<v-card
v-bind="attrs"
v-on="on"
height="200px"
color="primary"
class="note_card"
>
<v-card-title class="text-header font-weight-bold white--text"
>{{ note.title }}
</v-card-title>
<v-card-subtitle
v-if="note.text.length < 150"
class="text-caption white--text note__subtitle"
>
{{ note.text }}
</v-card-subtitle>
<v-card-subtitle v-else class="text-caption note__subtitle">
{{ note.text.substring(0, 150) + ".." }}
</v-card-subtitle>
</v-card>
</template>
<template>
<v-card class="read_only_note">
<v-card-subtitle class="text-h4 black--text font-weight-bold pa-5"
>{{ note.title }}
</v-card-subtitle>
<v-card-subtitle>
<span
v-for="(note, index) in this.note.tagList"
:key="index"
class="tag_span"
>
{{ note }}
</span>
</v-card-subtitle>
<v-container>
<v-row justify="space-around" class="note__images"
><div v-for="(image, index) in note.allImages" :key="index">
<v-img
:src="image"
height="70px"
width="70px"
contain
class="mx-2"
#click="openImage(image)"
></v-img>
</div>
<v-spacer></v-spacer>
</v-row>
</v-container>
<v-card-subtitle class="text__container">
<p v-html="convertedText" #click="detectYoutubeClick"></p>
</v-card-subtitle>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="red darken-1" text #click="deleteDialog = true">
Delete
</v-btn>
<v-btn color="secondary darken-2" text #click="closeNoteView()">
Close
</v-btn>
<v-btn color="secondary darken-2" text #click="editNote">
Edit
</v-btn>
</v-card-actions>
</v-card>
</template>
<v-dialog v-model="imageDialog" width="500">
<v-img :src="selectedImage" #click="imageDialog = false"></v-img>
</v-dialog>
<v-dialog v-model="deleteDialog" width="500">
<h4 style="text-align: center" class="pa-5">
Are you sure you want to delete {{ note.title }}?
</h4>
<v-btn color="red darken-1" text #click="deleteNote()"> Delete </v-btn>
<v-btn color="blue darken-1" text #click="deleteDialog = false">
Close
</v-btn>
</v-dialog>
<v-dialog v-model="youtubeDialog" width="500">
<iframe
id="ytplayer"
type="text/html"
width="500"
height="400"
:src="youtubeSrc"
frameborder="0"
></iframe>
</v-dialog>
</v-dialog>
</template>
<script>
import { EventBus } from "../event-bus";
export default {
props: {
note: Object,
},
data: () => {
return {
dialog: false,
imageDialog: false,
deleteDialog: false,
selectedImage: "",
youtubeDialog: false,
youtubeSrc: "",
};
},
mounted() {
EventBus.$on("openNoteView", (noteID) => {
if (this.note.uuid === noteID) {
return this.openNoteView();
}
});
},
methods: {
openImage(image) {
this.selectedImage = image;
this.imageDialog = true;
},
deleteNote() {
EventBus.$emit("deleteNote", this.note.uuid);
},
editNote() {
EventBus.$emit("openNoteForm");
this.closeNoteView();
// To load data after note form is mounted
setTimeout(() => {
EventBus.$emit("editNote", this.note);
}, 200);
},
openNoteView() {
this.dialog = true;
},
closeNoteView() {
this.dialog = false;
},
detectYoutubeClick(e) {
if (e.target.innerText.includes("youtube")) {
const url = e.target.innerText.replace("watch?v=", "embed/");
this.youtubeSrc = !url.includes("http") ? "https://" + url : url;
this.youtubeDialog = true;
}
},
},
computed: {
convertedText: function () {
const urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g;
return this.note.text.replace(urlRegex, function (url, b, c) {
const url2 = c == "www." ? "http://" + url : url;
if (url2.includes("youtube")) {
return `<span class="youtube_url">${url}</span>`;
} else {
return '' + url + "";
}
});
},
},
watch: {
youtubeDialog: function (newVal) {
if (newVal === false) this.youtubeSrc = "";
},
},
beforeDestroy() {
EventBus.$off("openNoteView", this.openNoteView);
},
};
</script>
<style lang="scss" >
.text__container {
height: 50vh;
p {
width: 100%;
}
}
.note__images {
margin-left: 5px !important;
}
.note_card {
border: 1px solid black !important;
}
.theme--dark.v-card .v-card__title {
color: black !important;
}
.theme--dark.v-card .v-card__subtitle.note__subtitle {
color: black !important;
}
.theme--light.v-card .v-card__subtitle.note__subtitle {
color: white !important;
}
.read_only_note.theme--dark {
.v-card__subtitle {
color: white !important;
display: flex;
flex-wrap: wrap;
}
.tag_span {
background-color: white;
color: black !important;
padding: 2px;
border-radius: 3px;
margin: 5px;
}
}
.read_only_note.theme--light {
.v-card__subtitle {
display: flex;
flex-wrap: wrap;
}
.tag_span {
color: white !important;
background-color: #212121;
padding: 2px;
border-radius: 3px;
margin: 5px;
}
}
.youtube_url {
text-decoration: underline;
&:hover {
cursor: pointer;
}
}
</style>
My spec file
import Vue from 'vue';
Vue.use(Vuetify);
import Vuetify from 'vuetify';
import NoteForm from '#/components/NoteForm';
import Note from '#/components/Note';
import { createLocalVue, mount } from '#vue/test-utils';
describe('NoteForm.vue', () => {
const localVue = createLocalVue();
localVue.use(Vuetify);
document.body.setAttribute('data-app', true);
let vuetify;
beforeEach(() => {
vuetify = new Vuetify();
});
it('should emit an event when the action v-btn is clicked', async () => {
const formWrapper = mount(NoteForm, {
localVue,
vuetify,
});
const noteWrapper = mount(Note, {
localVue,
vuetify,
});
formWrapper.vm.$emit('closeNoteForm');
await formWrapper.vm.$nextTick(); // Wait until $emits have been handled
expect(formWrapper.emitted().closeNoteForm).toBeTruthy();
expect(noteWrapper.find('.read_only_note').isVisible()).toBe(true);
});
});
However im getting the error
[Vue warn]: Error in render: "TypeError: Cannot read property 'title' of undefined"
found in
---> <Anonymous>
<Root>
console.error node_modules/vue/dist/vue.runtime.common.dev.js:1884
TypeError: Cannot read property 'title' of undefined
at Proxy.render (/Users/ozansozuoz/Downloads/vue-notebook/src/components/Note.vue:199:832)
at VueComponent.Vue._render (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:3538:22)
at VueComponent.updateComponent (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4054:21)
at Watcher.get (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4465:25)
at new Watcher (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4454:12)
at mountComponent (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4061:3)
at VueComponent.Object.<anonymous>.Vue.$mount (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:8392:10)
at init (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:3112:13)
at createComponent (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:5958:9)
at createElm (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:5905:9)
at VueComponent.patch [as __patch__] (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:6455:7)
at VueComponent.Vue._update (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:3933:19)
at VueComponent.updateComponent (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4054:10)
at Watcher.get (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4465:25)
at new Watcher (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4454:12)
at mountComponent (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4061:3)
at VueComponent.Object.<anonymous>.Vue.$mount (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:8392:10)
at mount (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/#vue/test-utils/dist/vue-test-utils.js:13977:21)
at Object.<anonymous> (/Users/ozansozuoz/Downloads/vue-notebook/tests/unit/example.spec.js:24:25)
at Object.asyncJestTest (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:102:37)
at /Users/ozansozuoz/Downloads/vue-notebook/node_modules/jest-jasmine2/build/queueRunner.js:43:12
at new Promise (<anonymous>)
at mapper (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
at /Users/ozansozuoz/Downloads/vue-notebook/node_modules/jest-jasmine2/build/queueRunner.js:73:41
at processTicksAndRejections (internal/process/task_queues.js:93:5)
FAIL tests/unit/example.spec.js
NoteForm.vue
✕ should emit an event when the action v-btn is clicked (501ms)
● NoteForm.vue › should emit an event when the action v-btn is clicked
TypeError: Cannot read property 'title' of undefined
197 | }
198 | .read_only_note.theme--dark {
> 199 | .v-card__subtitle {
| ^
200 | color: white !important;
201 | display: flex;
202 | flex-wrap: wrap;
at Proxy.render (src/components/Note.vue:199:832)
at VueComponent.Vue._render (node_modules/vue/dist/vue.runtime.common.dev.js:3538:22)
at VueComponent.updateComponent (node_modules/vue/dist/vue.runtime.common.dev.js:4054:21)
at Watcher.get (node_modules/vue/dist/vue.runtime.common.dev.js:4465:25)
at new Watcher (node_modules/vue/dist/vue.runtime.common.dev.js:4454:12)
at mountComponent (node_modules/vue/dist/vue.runtime.common.dev.js:4061:3)
at VueComponent.Object.<anonymous>.Vue.$mount (node_modules/vue/dist/vue.runtime.common.dev.js:8392:10)
at init (node_modules/vue/dist/vue.runtime.common.dev.js:3112:13)
at createComponent (node_modules/vue/dist/vue.runtime.common.dev.js:5958:9)
at createElm (node_modules/vue/dist/vue.runtime.common.dev.js:5905:9)
at VueComponent.patch [as __patch__] (node_modules/vue/dist/vue.runtime.common.dev.js:6455:7)
at VueComponent.Vue._update (node_modules/vue/dist/vue.runtime.common.dev.js:3933:19)
at VueComponent.updateComponent (node_modules/vue/dist/vue.runtime.common.dev.js:4054:10)
at Watcher.get (node_modules/vue/dist/vue.runtime.common.dev.js:4465:25)
at new Watcher (node_modules/vue/dist/vue.runtime.common.dev.js:4454:12)
at mountComponent (node_modules/vue/dist/vue.runtime.common.dev.js:4061:3)
at VueComponent.Object.<anonymous>.Vue.$mount (node_modules/vue/dist/vue.runtime.common.dev.js:8392:10)
at mount (node_modules/#vue/test-utils/dist/vue-test-utils.js:13977:21)
at Object.<anonymous> (tests/unit/example.spec.js:24:25)
I don't understand why its pointing to my scss and saying undefined?

You don't add props to your wrapper in tests.
Add this code to your test.
const noteWrapper = mount(Note, {
localVue,
vuetify,
propsData: {
note: {
title: '',
}
});

Related

Dynamic row in el-table Vue JS

I'm learning Vue JS. I'm using el-table to display my data as follows:
el-table data
Basically, I'm creating a function for a button's clicking event that contains parameter using properties of the row in the table (scope.row.id) in the code:
<el-table-column align="center" prop="id" label="Chức năng" width="150">
<template slot-scope="scope">
<el-button
type="primary"
icon="el-icon-switch-button"
#click="searchImportForm(scope.row.id)"
></el-button>
</template>
</el-table-column>
Normally it works fine. However, I've recently added a new feature to the table which is the "search" feature. There is a new text box for the user to input a keyword and the table returns all the information containing that keyword.
Now, when I press the button, the function does not work anymore. And my problem is that the error tells "Cannot read property "id" of undefined".
Does anyone how to handle with dynamic data table like this ??
Update: Here is the full code of my problem
<template>
<div class="app-container">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<div class="importListHeader">
<!-- <button #click="getImportData">Lấy dữ liệu</button> -->
<div class="importList-header-box-boxID">
<div>Mã thùng</div>
<el-input v-model="searchByUser.boxID" placeholder="Nhập mã" class="importListBoxID" />
</div>
<div class="importList-header-box-productList">
<div>Tên SP</div>
<div class="importListMultiselect">
<multiselect
placeholder="Chọn sản phẩm"
v-model="searchByUser.productName"
:options="productOptions"
label="name"
></multiselect>
</div>
</div>
<div class="importList-header-box-supplier">
<div>Tên xưởng</div>
<el-input v-model="searchByUser.keyword" placeholder="Nhập tên xưởng" class="importListSupplier" />
</div>
<div class="importList-header-box-fromDate">
<div>Từ ngày</div>
<el-input
id="importListFromDate"
v-model="searchByUser.firstDate"
placeholder="dd/mm/yyyy"
class="inputBox"
/>
</div>
<div class="importList-header-box-toDate">
<div>Đến ngày</div>
<el-input
id="importListToDate"
v-model="searchByUser.lastDate"
placeholder="dd/mm/yyyy"
class="inputBox"
/>
</div>
<div class="importList-header-box">
<el-button
class="buttonImportList"
type="success"
#click="submitSearchImportInfo"
icon="el-icon-search"
></el-button>
</div>
</div>
<el-table ref="singleTable" :data="filteredLists" border fit highlight-current-row>
<el-table-column align="center" label="#" width="55" prop="id"></el-table-column>
<el-table-column align="center" label="Xưởng SX" width="200" prop="supplier"></el-table-column>
<el-table-column align="center" label="Ngày nhập kho" width="150" prop="date"></el-table-column>
<el-table-column align="center" label="Trạng thái" width="150">
<template slot-scope="scope">
<h3
v-if="scope.row.status == 'Chưa hoàn thành' "
style="background-color: #e8e8e8; border-radius: 8px; font-size: 14px"
>{{ scope.row.status }}</h3>
<h3
v-if="scope.row.status == 'Hoàn thành' "
style="background-color: #67c23a; border-radius: 8px; font-size: 14px"
>{{ scope.row.status }}</h3>
</template>
</el-table-column>
<!--
<el-table-column class-name="status-col" label="Status" width="110" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status | statusFilter">{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
-->
<el-table-column align="center" prop="id" label="Chức năng" width="150">
<template slot-scope="scope">
<el-button
type="primary"
icon="el-icon-switch-button"
#click="searchImportForm(scope.row.id)"
></el-button>
</template>
</el-table-column>
<el-table-column align="center" label="Người tạo phiếu" width="180">
<template slot-scope="scope">
<el-tag id="importListCreate">{{ scope.row.create_user }}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" label="Người thực hiện" width="180">\
<template slot-scope="scope">
<el-tag id="importListConfirm">{{ scope.row.confirm_user }}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" label="Ghi chú" prop="description"></el-table-column>
</el-table>
</div>
</template>
<script>
import axios from 'axios'
import Multiselect from 'vue-multiselect'
export default {
filters: {
statusFilter(status) {
const statusMap = {
published: 'success',
draft: 'gray',
deleted: 'danger'
}
return statusMap[status]
}
},
data() {
return {
searchByUser: {
keyword: '',
firstDate: '',
lastDate: '',
boxID: '',
productName: ''
},
lists: [
// {
// id: '',
// supplier: '',
// date: '',
// status: ''
// }
],
productOptions: [{}],
startDate: '00-00-0000',
endDate: '31-12-3000'
}
},
beforeRouteEnter(to, from, next) {
axios
.post('http://192.168.1.93:3000/displayAllInPaper')
// axios.post('http://192.168.1.93:3000/displayAllInPaper')
.then(res => {
console.log(res)
next(vm => {
for (var i = 0; i < res.data.length; i++) {
vm.lists.push({
id: res.data[i].id,
supplier: res.data[i].supplier,
date: res.data[i].created_at,
status:
res.data[i].cur_status == 'p'
? 'Chưa hoàn thành'
: 'Hoàn thành',
description: res.data[i].paper_desc,
create_user: res.data[i].create_user,
confirm_user: res.data[i].confirm_user
})
}
axios.post('http://192.168.1.93:3000/getProductType').then(res => {
console.log(res)
for (var i = 0; i < res.data.length; i++) {
vm.productOptions.push({
name: res.data[i].cur_name
})
}
})
})
})
},
methods: {
submitSearchImportInfo() {
axios
.post('http://192.168.1.93:3000/searchInPaperWithProduct', {
productID: this.searchByUser.boxID,
productName: this.searchByUser.productName
})
.then(res => {
console.log(res)
this.lists.splice(0, this.lists.length)
for (var i = 0; i < res.data.length; i++) {
this.lists.push({
id: res.data[i].id,
supplier: res.data[i].supplier,
date: res.data[i].created_at,
status:
res.data[i].cur_status == 'p' ? 'Chưa hoàn thành' : 'Hoàn thành'
})
}
})
},
setCurrentRow(row) {
this.$refs.singleTable.setCurrentRow(row)
},
searchImportForm(id_par) {
this.$store.state.products.splice(0, this.$store.state.products.length)
this.$store.state.inPaperID = this.filteredLists[id_par - 1].id
axios
.post(
'http://192.168.1.93:3000/getDetailInPaper',
this.filteredLists[id_par - 1]
)
.then(res => {
console.log(res)
for (var i = 0; i < res.data.length; i++) {
this.$store.state.products.push({
productID: res.data[i].id,
cur_name: res.data[i].cur_name,
perbox: res.data[i].perbox,
box_amount: res.data[i].box_amount,
scan_number: res.data[i].scan_number
})
}
})
this.$router.push({ path: '/import/details' })
},
filteredByName(lists) {
return lists.filter(list =>
list.supplier.match(this.searchByUser.keyword)
)
},
localizeDate(date) {
if (!date || !date.includes('/')) return date
const [dd, mm, yyyy] = date.split('/')
return (date = `${yyyy}-${mm}-${dd}`)
},
convertDate(date) {
const [yyyy, mm, dd] = date.split('-')
var newDate = `${yyyy}${mm}${dd}`
var newDate2 = parseInt(newDate)
return (date = newDate2)
},
filteredByDateRange(lists) {
if (this.searchByUser.firstDate != '') {
this.startDate = this.searchByUser.firstDate
}
if (this.searchByUser.lastDate != '') {
this.endDate = this.searchByUser.lastDate
}
return lists.filter(list =>
this.convertDate(list.date) >=
this.convertDate(this.localizeDate(this.startDate)) &&
this.convertDate(list.date) <=
this.convertDate(this.localizeDate(this.endDate))
? list
: ''
)
}
},
computed: {
filteredLists: function() {
// return this.filteredByDateRange(this.lists)
if (
this.searchByUser.keyword == '' &&
this.searchByUser.firstDate == '' &&
this.searchByUser.lastDate == ''
) {
return this.filteredByName(this.lists)
}
if (
this.searchByUser.keyword != '' &&
this.searchByUser.firstDate == '' &&
this.searchByUser.lastDate == ''
) {
return this.filteredByName(this.lists)
}
if (
this.searchByUser.keyword == '' &&
(this.searchByUser.firstDate != '' || this.searchByUser.lastDate != '')
) {
return this.filteredByDateRange(this.lists)
}
if (
this.searchByUser.keyword != '' &&
(this.searchByUser.firstDate != '' || this.searchByUser.lastDate != '')
) {
return this.filteredByDateRange(this.filteredByName(this.lists))
}
}
},
components: {
Multiselect
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style>
.importListHeader {
max-width: 96%;
display: flex;
text-align: left;
justify-content: space-between;
}
.inputBox {
width: 250px;
height: 100px;
}
.el-dropdown-menu {
overflow-y: scroll;
}
.el-dropdown-link {
cursor: pointer;
color: #ffffff;
}
.importListMultiselect .multiselect__tags {
min-height: 40px;
display: block;
padding: 8px 40px 0 8px;
border-radius: 5px;
border: 0.5px solid #e8e8e8;
background: #fff;
font-size: 14px;
width: 320px;
}
.importListMultiselect .multiselect {
-webkit-box-sizing: content-box;
box-sizing: content-box;
display: block;
position: relative;
width: 320px;
min-height: 40px;
text-align: left;
color: #35495e;
}
#importListFromDate {
width: 200px;
}
#importListToDate {
width: 200px;
}
.buttonImportList {
position: absolute;
top: 39px;
}
.el-icon-arrow-down {
font-size: 8px;
}
.el-dropdown {
vertical-align: top;
}
.importListBoxID {
max-width: 210px;
}
.importList-header-box-boxID {
max-width: 200px;
}
.importList-header-box-productList {
max-width: 320px;
}
.importList-header-box-supplier {
max-width: 250px;
}
.importList-header-box-fromDate {
max-width: 200px;
}
.importList-header-box-toDate {
max-width: 200px;
}
#importListCreate {
font-size: 15px;
}
#importListConfirm {
font-size: 15px;
color: #35495e;
background-color: transparent;
border-style: none;
}
</style>

vue clicking on the action button's focus area breaks it

I'm trying to create a floating action button by using vuetify -vspeed dial. I created a logic to style my button whenever it's clicked and it's working perfect, it collapses and expands whenever i click on it. However, if i try to click the focus area of the action buttons, it breaks it and close the buttons. How can i prevent that? When I click on button, it's fine - I use click.stop to make it persistent but if i click to the area right next to button, it closes the buttons which breaks my logic for styling. Here's my code
Test.Vue
<template>
<v-card :class="{create: backgroundColor }">
<v-speed-dial
:bottom="true"
:right="true"
:direction="direction"
:transition="transition"
fixed
>
<template v-slot:activator>
<v-btn
:class="{is_active:isActive}"
color="#C6002B"
fab
dark
#click=toggleButton
x-large
>
<v-icon>{{isActive? 'mdi-close' : 'mdi-account-circle'}}</v-icon><span>{{isActive ? "EXPANDED" : ''}}</span>
</v-btn>
</template>
<v-btn
v-if="finalProp"
:class="{alignLeft:isActive}"
fab
dark
large
#click.stop="$emit('func1')"
color="white" >
<v-icon color="#F0BE85">mdi-pencil</v-icon>
</v-btn>
<v-btn
v-if="thirdProp"
:class="{alignLeft:isActive}"
fab
dark
large
#click.stop="$emit('func2')"
color="white">
>
<v-icon color="purple">mdi-delete</v-icon>
</v-btn>
<v-btn
:class="{alignLeft:isActive}"
v-if="secondProp"
fab
dark
large
#click.stop="$emit('func3')"
color="white">
>
<v-icon color="green">mdi-plus</v-icon>
</v-btn>
<v-btn
v-if="firstProp"
:class="{alignLeft:isActive}"
fab
dark
large
#click.stop="$emit('func4')"
color="white">
>
<v-icon color="red">home</v-icon>
</v-btn>
</v-speed-dial>
</v-card>
</template>
<script>
export default {
name: 'FloatingButton',
props: {
firstProp: Boolean,
secondProp: Boolean,
thirdProp: Boolean,
finalProp: Boolean
},
data: () => ({
direction: 'top',
fab: false,
right: true,
bottom: true,
transition: 'scale-transition',
isActive: false,
backgroundColor: false,
check:true
}),
methods: {
toggleButton:function() {
this.isActive = !this.isActive
this.backgroundColor = !this.backgroundColor
}
},
}
</script>
<style scoped>
.is_active {
min-width:120px
/* width: 380px;
height: 70px;
border-radius: 36px;
margin:5px; */
}
.is_active span {
font-size: 18px;
letter-spacing: 0px;
}
.create {
min-width: 100%;
min-height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 4;
background:rgba(0,0,0,0.4);
color:rgba(0,0,0,0.8);
}
}
</style>
App. vue
<template>
<v-app>
<Test :firstProp=a :secondProp=b :thirdProp=c :lastProp=d />
</v-app>
</template>
<script>
import Test from './components/Test'
export default {
name: 'App',
components: {
Test
},
data(){
return{
a:true,
b:true,
c:true,
d:true
}
}
};
</script>
I don't see what's wrong. Can you check this reproduction? I mostly only changed the casing of attributes on <Test> element.
Vue.component('Test', {
template: `
<v-card :class="{create: backgroundColor }">
<v-speed-dial
:bottom="true"
:right="true"
:direction="direction"
:transition="transition"
fixed
>
<template v-slot:activator>
<v-btn
:class="{is_active:isActive}"
color="#C6002B"
fab
dark
#click="toggleButton"
x-large
>
<v-icon>{{isActive? 'mdi-close' : 'mdi-account-circle'}}</v-icon>
</v-btn>
</template>
<v-btn
v-if="finalProp"
:class="{alignLeft:isActive}"
fab
dark
large
#click.stop="$emit('func1')"
color="white" >
<v-icon color="#F0BE85">mdi-pencil</v-icon>
</v-btn>
<v-btn
v-if="thirdProp"
:class="{alignLeft:isActive}"
fab
dark
large
#click.stop="$emit('func2')"
color="white">
>
<v-icon color="purple">mdi-delete</v-icon>
</v-btn>
<v-btn
:class="{alignLeft:isActive}"
v-if="secondProp"
fab
dark
large
#click.stop="$emit('func3')"
color="white">
>
<v-icon color="green">mdi-plus</v-icon>
</v-btn>
<v-btn
v-if="firstProp"
:class="{alignLeft:isActive}"
fab
dark
large
#click.stop="$emit('func4')"
color="white">
>
<v-icon color="red">home</v-icon>
</v-btn>
</v-speed-dial>
</v-card>
`,
props: {
firstProp: Boolean,
secondProp: Boolean,
thirdProp: Boolean,
finalProp: Boolean
},
data: () => ({
direction: 'top',
fab: false,
right: true,
bottom: true,
transition: 'scale-transition',
isActive: false,
backgroundColor: false,
check:true
}),
methods: {
toggleButton: function() {
this.isActive = !this.isActive
this.backgroundColor = !this.backgroundColor
}
},
})
Vue.config.productionTip = false
new Vue({
el: '#app',
vuetify: new Vuetify(),
data(){
return{
a:true,
b:true,
c:true,
d:true
}
}
});
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/#mdi/font#4.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.min.css" rel="stylesheet">
<style scoped>
.is_active {
/*min-width:120px
width: 380px;
height: 70px;
border-radius: 36px;
margin:5px; */
}
.is_active span {
font-size: 18px;
letter-spacing: 0px;
}
.create {
min-width: 100%;
min-height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 4;
background:rgba(0,0,0,0.4);
color:rgba(0,0,0,0.8);
}
}
</style>
</head>
<body>
<div id="app">
<v-app>
<Test :first-prop="a" :second-prop="b" :third-prop="c" :last-prop="d" />
</v-app>
</div>
</body>
</html>

Vue.js: How to update element's position

I want to move an element while scrolling so the element is always in the screen.
But the position isn't updated.
What I am doing is this.
<v-card class="item" :style="{ top: distance }">
</v-card>
...
data() {
return {
distance: 0,
}
}
methods: {
handleScroll() {
this.distance = window.scrollY
},
},
created() {
window.addEventListener('scroll', this.handleScroll)
},
destroyed() {
window.removeEventListener('scroll', this.handleScroll)
},
}
...
.item {
position: absolute;
}
How can I do it?
Your element should have an absolute position and add the units as #Nikos mentioned :
<v-card class="item" style="position:absolute" :style="{ top: distance+'px' }">
</v-card>

How to set aspect ratio to vue cropperjs

From vue-cropper.js example I can't find way to set aspect ratio like on jquery version in options.
In jquery version you can pass options to element where is crop used, on this one I can't find answer..
<div id="app">
<h2 style="margin: 0;">Vue CropperJS</h2>
<hr/>
<input type="file" name="image" accept="image/*"
style="font-size: 1.2em; padding: 10px 0;"
#change="setImage" />
<br/>
<div style="width: 400px; height:300px; border: 1px solid gray; display: inline-block;">
<vue-cropper
ref='cropper'
:guides="true"
:view-mode="2"
drag-mode="crop"
:auto-crop-area="0.5"
:min-container-width="250"
:min-container-height="180"
:background="true"
:rotatable="true"
:src="imgSrc"
alt="Source Image"
:img-style="{ 'width': '400px', 'height': '300px' }">
</vue-cropper>
</div>
<img :src="cropImg" style="width: 200px; height: 150px; border: 1px solid gray" alt="Cropped Image" />
<br/>
<br />
<button #click="cropImage" v-if="imgSrc != ''" style="margin-right: 40px;">Crop</button>
<button #click="rotate" v-if="imgSrc != ''">Rotate</button>
</div>
</template>
<script>
import VueCropper from 'vue-cropperjs';
export default {
components: {
VueCropper,
},
data() {
return {
imgSrc: '',
cropImg: '',
};
},
methods: {
setImage(e) {
const file = e.target.files[0];
if (!file.type.includes('image/')) {
alert('Please select an image file');
return;
}
if (typeof FileReader === 'function') {
const reader = new FileReader();
reader.onload = (event) => {
this.imgSrc = event.target.result;
// rebuild cropperjs with the updated source
this.$refs.cropper.replace(event.target.result);
};
reader.readAsDataURL(file);
} else {
alert('Sorry, FileReader API not supported');
}
},
cropImage() {
// get image data for post processing, e.g. upload or setting image src
this.cropImg = this.$refs.cropper.getCroppedCanvas().toDataURL();
},
rotate() {
// guess what this does :)
this.$refs.cropper.rotate(90);
},
},
};
</script>
Nothing mentioned on:
https://github.com/Agontuk/vue-cropperjs..
I need to set aspect ratio to 1:1;
Any help?
Thanks
I added this to my vue component:
:aspectRatio="1/1"
:initialAspectRatio="1/1"
And it works as expected

Using vuetify card like bootstrap cards columns

I need a way to use list of v-card from Vuetify like Bootstrap Card Columns
Possible solution:
Pardon me, I couldn't plunkr the code :(
This is what I did.
Update: Unfortunately this messes up with the v-ripple directive
<template>
<v-container grid-list-md>
<div class="v-card-columns">
<v-card tile v-for="post in posts" :key="post.id">
<v-card-title primary-title>
<h3 headline>
{{post.title}}
</h3>
</v-card-title>
<v-card-text>
{{post.body}}
</v-card-text>
</v-card>
</div>
</v-container>
</template>
<script>
import axios from "axios";
export default {
name: "All",
data() {
return {
posts: []
}
},
mounted() {
axios
.get('https://jsonplaceholder.typicode.com/posts')
.then(res => {
this.posts = res.data
})
}
}
</script>
<style scoped>
.v-card-columns .v-card {
margin-bottom: 0.75rem;
}
#media (min-width: 576px) {
.v-card-columns {
-webkit-column-count: 3;
-moz-column-count: 3;
column-count: 3;
-webkit-column-gap: 1.25rem;
-moz-column-gap: 1.25rem;
column-gap: 1.25rem;
orphans: 1;
widows: 1;
}
.v-card-columns .v-card {
display: inline-block;
width: 100%;
}
}
</style>
Pardon me, I couldn't plunkr the code :(
This is what I did.
Update: Unfortunately this messes up with the v-ripple directive
<template>
<v-container grid-list-md>
<div class="v-card-columns">
<v-card tile v-for="post in posts" :key="post.id">
<v-card-title primary-title>
<h3 headline>
{{post.title}}
</h3>
</v-card-title>
<v-card-text>
{{post.body}}
</v-card-text>
</v-card>
</div>
</v-container>
</template>
<script>
import axios from "axios";
export default {
name: "All",
data() {
return {
posts: []
}
},
mounted() {
axios
.get('https://jsonplaceholder.typicode.com/posts')
.then(res => {
this.posts = res.data
})
}
}
</script>
<style scoped>
.v-card-columns .v-card {
margin-bottom: 0.75rem;
}
#media (min-width: 576px) {
.v-card-columns {
-webkit-column-count: 3;
-moz-column-count: 3;
column-count: 3;
-webkit-column-gap: 1.25rem;
-moz-column-gap: 1.25rem;
column-gap: 1.25rem;
orphans: 1;
widows: 1;
}
.v-card-columns .v-card {
display: inline-block;
width: 100%;
}
}
</style>