Vue.js 3 optional v-scroll-to - vue.js

Here is the code that I use.
<template>
<div class="button-layout" :style="`margin: ${margin}; text-align: ${align};`">
<component
:is="buttonComponent"
v-for="(button, index) in buttons.filter(btn => btn.url)"
:key="button.label"
:label="button.label"
v-scroll-to="button.url"
:style="`margin-left: ${index === 0 ? '' : space};`" />
<component
:is="buttonComponent"
v-for="(button, index) in buttons.filter(btn => !btn.url)"
:key="button.label"
:label="button.label"
:type="button.type"
:style="`margin-left: ${index === 0 ? '' : space};`" />
</div>
</template>
<script>
export default {
name: "ButtonLayout",
components: { },
props: {
button: String,
margin: String,
align: String,
space: String,
buttons: Array
},
computed: {
buttonComponent() {
return () => import(`./button/${this.button}`)
}
}
};
</script>
I can use these two list of object structures and it works fine.
[
{ url: '#video', label: lang.video },
{ url: '#info', label: lang.info }
]
[
{ type: 'reset', label: lang.clear },
{ type: 'submit', label: lang.send }
]
As I don't like repeating my code, I tried to add dynamically the attribute type and v-scroll-to based on the first object on the list, however, it doesn't work. What would be the best way to achieve it? (See the code below)
<template>
<div class="button-layout" :style="`margin: ${margin}; text-align: ${align};`">
<component
:is="buttonComponent"
v-for="(button, index) in buttons"
:key="button.label"
:label="button.label"
v-bind:[optionalDirective.directive]="button[optionalDirective.key]"
:style="`margin-left: ${index === 0 ? '' : space};`" />
</div>
</template>
<script>
export default {
name: "ButtonLayout",
components: { },
props: {
button: String,
margin: String,
align: String,
space: String,
buttons: Array
},
computed: {
buttonComponent() {
return () => import(`./button/${this.button}`)
},
optionalDirective(){
if(this.buttons[0].url) {
return {
directive: 'v-scroll-to',
key: 'url'
}
} else {
return {
directive: 'type',
key: 'type'
}
}
}
}
};
</script>

You can pass an object to v-bind and it will create html attributes based on the keys of the object.
Something like this should work
<component
:is="buttonComponent"
v-for="(button, index) in buttons"
:key="button.label"
:label="button.label"
v-bind="{[button.url ? 'v-scroll-to' : 'type' ] : (button.url || button.type} }"
:style="`margin-left: ${index === 0 ? '' : space};`" />
Or you could declare a new method that returns the desired object
methods: {
buttonDirective (button) {
if (button.url) {
return {
'v-scroll-to': button.url
}
} else {
return {
'type': button.type
}
}
}
}
and then call it in component
<component
:is="buttonComponent"
v-for="(button, index) in buttons"
:key="button.label"
:label="button.label"
v-bind="buttonDirective(button)"
:style="`margin-left: ${index === 0 ? '' : space};`" />

As v-scroll-to cannot be bound to v-bind, I found a little work around that actually solve my issue and avoid to duplicate the code twice. I bind the #click to a method that inspect the url value and $scollTo if needed, and it solve my problem.
<template>
<div class="button-layout" :style="`margin: ${margin}; text-align: ${align};`">
<component
:is="buttonComponent"
v-for="(button, index) in buttons"
:key="button.label"
:label="button.label"
v-bind="type(button.type)"
#click="scrollTo(button.url)"
:style="`margin-left: ${index === 0 ? '' : space};`"
/>
</div>
</template>
<script>
export default {
name: "ButtonLayout",
components: {},
props: {
button: String,
margin: String,
align: String,
space: String,
buttons: Array
},
methods: {
type(type) {
return type ? { type } : {}
},
scrollTo(url) {
if (url) this.$scrollTo(url)
}
},
computed: {
buttonComponent() {
return () => import(`./button/${this.button}`);
}
}
};
</script>

Related

I am getting a warning regarding an infinite update loop in a component render. What part of my code is causing this?

In my application I have a list of documents that I display in a table, each document has a specific type, which is specified in json file as enum values.
I can already display all the documents without any problems, but now I have tried to create an input where the user can choose from the list of enum values, and when doing so only the documents with the selected enum type will be shown in the table.
This does sorta work, but the problem is that I have somehow created an infinite update loop, which causes the application to randomly stop working.
This is the template. I am using a custom made template component, but that is not related to the issue.
<template>
<b-container>
<div #dragover.prevent.stop #drop.prevent.stop="onDropEvent">
<b-card-header header-tag="header" class="p-1" role="tab">
<b-button-toolbar justify>
<b-button
:class="showCollapse ? 'collapsed' : null"
#click="showCollapse = !showCollapse"
variant="outline-info"
class="flex-grow-1"
>
<span class="float-left">{{ folderName }}</span>
<span class="float-right">
{{
search ?
$t('documentCountFiltered', {filtered: filteredDocuments.length, count: documents.length}) :
$tc('documentCount', documents.length)
}}
</span>
</b-button>
<template v-if="!inherited && mode !== 'READ_ONLY'">
<label class="document-uploader btn btn-sm btn-outline-primary ml-2">
<span>
<i class="fas fa-fw fa-file-upload"></i>
{{ $t('uploadFiles') }}
</span>
<input type="file" multiple #change="selectFiles">
</label>
<b-dropdown
v-if="mode !== 'READ_ONLY' && mode !== 'RESTRICTED'"
size="sm" variant="outline-primary" class="ml-2 template-dropdown" no-caret right>
<template slot="button-content">
<i class="fas fa-fw fa-file-medical"></i> {{ $t('createDocument') }}
</template>
<b-dropdown-header v-if="companyTemplates.length !== 0" id="dropdown-header-templates">
{{ $t('companyTemplates') }}
</b-dropdown-header>
<b-dropdown-item v-if="companyTemplates.length !== 0" v-for="template of companyTemplates"
#click="emitWordDocumentCreateSelectEvent(template)">
{{ template.name }} <i v-if="template.freeTextEnabled" class="fa-fw fas fa-paragraph"></i>
</b-dropdown-item>
<b-dropdown-divider
v-if="companyTemplates.length !== 0 && companyFormBuilderTemplates.length > 0"></b-dropdown-divider>
<b-dropdown-header v-if="companyFormBuilderTemplates.length > 0" id="dropdown-header-inheritedDocuments">
{{ $t('formBuilderTemplates') }}
</b-dropdown-header>
<b-dropdown-item
v-for="template of companyFormBuilderTemplates"
#click="emitDocumentCreateSelectEvent(template)">
{{ template.name }}
</b-dropdown-item>
<b-dropdown-divider
v-if="globalTemplates.length !== 0"></b-dropdown-divider>
<b-dropdown-header v-if="globalTemplates.length > 0" id="dropdown-header-globalDocuments">
{{ $t('globalTemplates') }}
</b-dropdown-header>
<b-dropdown-item
v-for="template of globalTemplates"
#click="emitGlobalDocumentCreateSelectEvent(template)">
{{ template.name }}
</b-dropdown-item>
</b-dropdown>
</template>
</b-button-toolbar>
</b-card-header>
<b-form-group class="mt-4">
<w-b-form-select v-model="filteredDocumentType">
<template>
<b-form-select-option :value="null" disabled>-- {{ $t('selectDocumentByType') }} --</b-form-select-option>
</template>
<b-form-select-option :value="'all'">({{ $t('all') }})</b-form-select-option>
<option v-for="documentType in availableDocumentTypes" :key="documentType" :value="documentType">
{{ $t('model.document.types.' + documentType) }}
</option>
</w-b-form-select>
</b-form-group>
<b-collapse v-model="showCollapse">
<common-table
:fields="fields"
:items="filteredDocuments"
primary-key="id"
sort-by="creationDateTime"
sort-desc
>
<template slot="head(select)">
<check-all-checkbox :list="filteredDocuments" #change="handleTag(documents, $event)" property="selected"/>
</template>
<template slot="cell(select)" slot-scope="data">
<w-b-form-checkbox v-model="data.item.selected" v-if="data.item.selected != null"
#change="handleTag([data.item], $event)" data-test-id="check-box"/>
</template>
<template slot="cell(name)" slot-scope="data">
<div class="d-flex">
<div style="flex: 2 0 0">
<b-dropdown :text="data.item.name" variant="link" toggle-class="name-cell">
<b-dropdown-item #click="viewFile(data.item)" v-if="data.item.type != 'EMAIL'"><i
class="fas fa-eye"></i> {{ $t('showDocument') }}
</b-dropdown-item>
<b-dropdown-item #click="downloadFile(data.item)"><i class="fas fa-file-download"></i> {{
$t('download')
}}
</b-dropdown-item>
</b-dropdown>
</div>
<div style="flex: 1 0 0" v-if="data.item.uploading">
<b-progress :animated="!data.item.error" striped class="h-100">
<b-progress-bar
:value="100"
:variant="data.item.error ? 'danger' : 'primary'"
:label="data.item.error ? 'Error' : 'Uploading...'"
/>
</b-progress>
</div>
</div>
</template>
</common-table>
</b-collapse>
<p-d-f-j-s-viewer ref="pdf-viewer"/>
</div>
</b-container>
</template>
My script
<script>
import CheckAllCheckbox from '#/components/CheckAllCheckbox';
import CommonTable from '#/components/common/CommonTable';
import CommonInput from '#/components/common/CommonInput';
import {applianceService} from '#/services/appliance';
import PDFJSViewer from '#/components/PDFJSViewer';
import axios from '#/config/axios';
import {documentService} from '#/services/document';
import documentTypes from '#/models/document/type';
import {propertyFacilityService} from '#/services/property-facility';
import CommonCollapsible from '#/components/common/CommonCollapsible';
export default {
props: {
documents: Array,
documentOwnerType: String,
companyTemplates: Array,
companyFormBuilderTemplates: Array,
globalTemplates: Array,
inherited: Boolean,
startCollapsed: {
type: Boolean,
default: false
},
mode: String,
search: String,
applianceId: String,
propertyFacilityId: String,
noteId:String
},
components: {
CheckAllCheckbox,
CommonTable,
CommonCollapsible,
CommonInput,
PDFJSViewer
},
data() {
return {
documentTypes,
showCollapse: true,
filteredDocumentType: null
};
},
computed: {
filteredDocuments() {
console.log(this.filteredDocumentType)
console.log(this.documents)
if (this.filteredDocumentType === null || this.filteredDocumentType === 'all') {
return (this.documents ?? []).filter(document =>
(document.name.toUpperCase().includes(this.search.toUpperCase()) ||
this.$t(`model.document.types.${document.type}`).toUpperCase().includes(this.search.toUpperCase())))
}
else {
return (this.documents ?? []).filter(document =>
(document.name.toUpperCase().includes(this.search.toUpperCase()) ||
this.$t(`model.document.types.${document.type}`).toUpperCase().includes(this.search.toUpperCase())) &&
document.type === this.filteredDocumentType)
}
},
folderName() {
if (this.inherited) {
return this.$t('sharedDocuments');
} else if (this.documentOwnerType === 'COMPANY') {
return this.$t('companyDocuments');
} else {
return this.$t('documents');
}
},
availableDocumentTypes() {
return this.documentTypes.sort((a, b) => this.getDocumentTypeText(a).localeCompare(this.getDocumentTypeText(b)));
},
fields() {
return [
{
key: 'select',
thStyle: 'width: 1%'
},
{
key: 'name',
label: this.$t('name'),
sortable: true
},
{
key: 'inherited',
label: this.$t('inherited'),
formatter: inherited => this.$t(inherited ? 'yes' : 'no'),
sortable: true,
hide: this.inherited
},
{
key: 'sharedOnTC',
label: this.$t('sharedOnTC'),
formatter: sharedOnTC => this.$t(sharedOnTC ? 'yes' : 'no'),
sortable: true,
hide: this.sharedOnTC
},
{
key: 'type',
label: this.$t('type'),
formatter: type => this.$t('model.document.types.' + type),
sortable: true,
sortByFormatted: true
},
{
key: 'tags',
label: this.$t('tags'),
formatter: tags => tags.join(', '),
sortable: true,
sortByFormatted: true
},
{
key: 'signed',
label: this.$t('signed'),
formatter: sharedOnTC => this.$t(sharedOnTC ? 'yes' : 'no'),
sortable: true,
sortByFormatted: true
},
{
key: 'creationDateTime',
label: this.$t('created'),
sortable: true,
template: {type: 'date', format: 'L LT'}
},
{
key: 'changedDateTime',
label: this.$t('changed'),
sortable: true,
template: {type: 'date', format: 'L LT'}
},
{
key: 'actions',
hide: this.inherited || this.mode === 'READ_ONLY',
template: {
type: 'actions',
cell: [
{
icon: 'fa-edit',
tooltip: this.$t('edit'),
if: this.mode !== 'RESTRICTED',
disabled: data => data.item.uploading || !data.item.documentTemplateId || data.item.signed,
action: data => this.emitDocumentEditTemplateSelectEvent(data.item)
},
{
icon: 'fa-cog',
tooltip: this.$t('documentSettings'),
disabled: data => data.item.uploading,
action: data => this.emitDocumentEditSelectEvent(data.item)
},
{
icon: 'fa-trash',
variant: 'outline-danger',
disabled: data => data.item.uploading,
action: data => this.emitDocumentDeleteSelectEvent(data.item)
}
]
}
}
].filter(field => !field.hide);
}
},
methods: {
getDocumentTypeText(type) {
return this.$t(`model.document.types.${type}`);
},
selectFiles(event) {
this.emitFileUploadEvent(event.target.files);
event.target.value = '';
},
onDropEvent(event) {
this.emitFileUploadEvent(event.dataTransfer.files);
},
downloadFile(document) {
documentService.downloadDocument(document.id)
.catch(error => {
console.error(error);
});
},
viewFile(document) {
documentService.getPublicDownloadToken(document.id).then(result => {
let fileName = `${axios.defaults.baseURL}/file/public/${result.data}/download`;
this.$refs['pdf-viewer'].show(fileName);
}).catch(error => {
console.error(error);
});
},
emitGlobalDocumentCreateSelectEvent(template) {
this.$emit('document-global-create-select', template);
},
emitDocumentCreateSelectEvent(template) {
this.$emit('document-create-select', template);
},
emitWordDocumentCreateSelectEvent(template) {
this.$emit('document-word-create-select', template);
},
emitDocumentEditSelectEvent(document) {
documentService.getDocument(document.id).then(result => {
document = result.data;
this.$emit('document-edit-select', document);
});
},
emitDocumentEditTemplateSelectEvent(document) {
this.$emit('document-edit-template-select', document);
},
emitDocumentDeleteSelectEvent(document) {
this.$emit('document-delete-select', document);
},
emitFileUploadEvent(files) {
if (files && files.length) {
this.$emit('file-upload', [...files]);
}
},
handleTag(documents, selected) {
if (this.applianceId) {
applianceService.updateDocuments(this.applianceId, {
documentIds: documents.map(doc => doc.id), selected: selected
}).then(({data: documents}) => {
documents.forEach(doc => {
this.documents.splice(this.documents.findIndex(d => d.id === doc.id), 1,
Object.assign(doc, {selected: selected}));
});
});
}
if (this.propertyFacilityId) {
propertyFacilityService.updateDocuments(this.propertyFacilityId, {
documentIds: documents.map(doc => doc.id), selected: selected
}).then(({data: documents}) => {
documents.forEach(doc => {
this.documents.splice(this.documents.findIndex(d => d.id === doc.id), 1,
Object.assign(doc, {selected: selected}));
});
});
}
}
},
created() {
this.showCollapse = !this.startCollapsed;
}
};
</script>

Don't show selected option in vue-select

I have an array "option" with some element inside. And have vue-select feature. I want to not to show selected option in all options list.
So, I want to delete "RU" option form that list if "RU" is selected. Are there any decisions?
My component file:
v-select:
<v-select :options="options" label="title" class="select" v-model="selectedLang">
<template slot="option" slot-scope="option">
<img class="language-flag" :src="option.img" /> {{ option.title }}
</template>
<template slot="selected-option" slot-scope="option">
<img class="language-flag" :src="option.img" /> {{ option.title }}
</template>
</v-select>
script part:
export default {
data() {
return {
options: [{
title: 'RU',
img: require('../../assets/icons/flags/RU.svg'),
},
{
title: 'KZ',
img: require('../../assets/icons/flags/KZ.svg')
},
],
selectedLang: null,
}
},
mounted() {
this.selectedLang = this.options[0];
}
}
You can use computed:
computed: {
items () {
return this.options.filter(i => i.title !== this.selectedLang?.title)
}
}
and then use these "items" as options in select
<v-select :options="items" label="title" class="select" v-
model="selectedLang">
If you're looking multi-select, you can use the following,
<v-select multiple :options="getOptions" ... />
{{ selectedLang }} // Prints selected options
{
data: {
selectedLang: [],
options: [
{ title: 'RU', img: require(...) },
{ title: 'KZ', img: require(...) }
]
},
computed: {
getOptions() {
return this.options.filter(option => !this.selectedLang.find(o => o.title === option.title))
}
}
}

Error: Do not mutate vuex store state outside mutation handlers

Scenario
I am using Vuex, to store some data in it, and in my case the ticket details.
Initially, I have a ticket which has an array of discounts, to be empty.
Once I hit the button "Add discount" I mount the component called "testDiscount" which in the mounted hook pushes the first object ({"code": "Foo", "value":"Boo"}) in the discounts array of a specific ticket in the store.
The problem arise when I try to type in the input boxes (changing its state) in this component where I get the error "do not mutate Vuex store state outside mutation handlers.". How could I best handle this?
Test.vue
<template>
<div>
<test-component v-for="(t, key) in tickets" :key="key" :ticket-key="key" :tid="t.id"></test-component>
</div>
</template>
<script>
import TestComponent from "~/components/testComponent.vue";
export default {
layout: "noFooter",
components: {
"test-component": TestComponent,
},
data() {
return {
tickets: this.$store.state.ticketDiscount.tickets,
};
},
mounted() {
if (this.tickets.length == 0) {
this.$store.commit("ticketDiscount/addTicket", {
id:
this.$store.state.ticketDiscount.tickets.length == 0
? 0
: this.$store.state.ticketDiscount.tickets[
this.$store.state.ticketDiscount.tickets.length - 1
].id + 1,
discount: [],
});
}
},
};
</script>
ticketDiscount.js
export const state = () => ({
tickets: []
});
export const mutations = {
addTicket(state, ticket) {
state.tickets.push(ticket);
},
addDiscount(state, property) {
state.tickets.find(ticket => ticket.id == property.id)[property.name].push(property.value);
}
testComponent.vue
<template>
<div>
<h3>Ticket number: {{ticketKey + 1}}</h3>
<button #click="showDiscount = true">Add discount</button>
<test-discount v-model="discount_" v-if="showDiscount" :tid="tid"></test-discount>
</div>
</template>
<script>
import testDiscount from "~/components/test-discount.vue";
export default {
components: {
testDiscount,
},
data() {
return {
showDiscount: false,
tid_: this.tid,
};
},
props: {
tickets: Array,
ticketKey: { type: Number },
tid: { type: Number, default: 0 },
},
methods: {
updateTicket() {
this.$emit("updateTicket", {
id: this.tid_,
value: {
discount: this.discount_,
},
});
},
},
mounted() {
this.$watch(
this.$watch((vm) => (vm.discount_, Date.now()), this.updateTicket)
);
},
computed: {
discount_: {
get() {
return this.$store.state.ticketDiscount.tickets.find(
(ticket) => ticket.id == this.tid
)["discount"];
},
set(value) {
// set discount
},
},
},
};
</script>
testDiscount.vue
<template>
<div class="container">
<div class="title">
<img src="~/assets/svgs/price_tag.svg" />
<span>Discount code</span>
{{ discounts }}
</div>
<div class="discount-container">
<div v-for="(c,idx) in discounts" class="discounts" :key="idx">
<div class="perc-input">
<input style="max-width: 50px;" v-model.number="c.discount" type="number" min="1" max="100" step="1" placeholder="10">
<div>%</div>
</div>
<input class="code-input" v-model="c.code" placeholder="Code">
<img src="~/assets/svgs/bin.svg" title="Delete code" #click="deleteCode(idx)" v-if="discounts.length > 1"/>
</div>
</div>
<span #click="newDiscount" class="add-another">+ Add another discount</span>
</div>
</template>
<script>
export default {
props: {
value: {
type: Array,
},
tid: { type: Number, default: 0 },
},
data() {
return {
discounts: this.value,
}
},
mounted() {
if (this.discounts.length == 0) {
this.newDiscount();
}
},
methods: {
newDiscount() {
this.$store.commit('ticketDiscount/addDiscount',
{
"id": this.tid,
"name": "discount",
"value": { code: null,discount: null }
});
},
deleteCode(index) {
this.discounts.splice(index, 1);
}
},
watch: {
discounts() {
this.$emit('input', this.discounts)
}
},
beforeDestroy() {
this.$emit('input', []);
}
}
</script>
you shouldn't use v-model in this case.
<input style="max-width: 50px;" v-model.number="c.discount" .../>
you could just set the value
<input style="max-width: 50px;" :value="c.discount" #change="handleValueChange" .../>
and then in handleValueChange function to commit the action to update just for that value.

Cannot read property '$i18n' of undefined when using Vue Test Utils

I'm trying to test a BaseDialog component that uses VueI18n for translations with vue-test-utils. I cannot get the test to run do the the following error:
TypeError: Cannot read property '$i18n' of undefined
at VueComponent.default (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/src/components/BaseDialog/BaseDialog.vue:2671:220)
at getPropDefaultValue (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:1662:11)
at validateProp (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:1619:13)
at loop (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4612:17)
at initProps (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4643:33)
at initState (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4586:21)
at VueComponent.Vue._init (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4948:5)
at new VueComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5095:12)
at createComponentInstanceForVnode (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3270:10)
at init (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3101:45)
at createComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5919:9)
at createElm (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5866:9)
at VueComponent.patch [as __patch__] (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:6416:7)
at VueComponent.Vue._update (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3904:19)
at VueComponent.updateComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4025:10)
at Watcher.get (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4426:25)
at new Watcher (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4415:12)
at mountComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4032:3)
at VueComponent.Object.<anonymous>.Vue.$mount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:8350:10)
at mount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/#vue/test-utils/dist/vue-test-utils.js:8649:21)
at shallowMount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/#vue/test-utils/dist/vue-test-utils.js:8677:10)
at Object.it (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/src/components/BaseDialog/__tests__/BaseDialog.spec.js:22:21)
at Object.asyncJestTest (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/jasmine_async.js:108:37)
at resolve (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:56:12)
at new Promise (<anonymous>)
at mapper (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:43:19)
at promise.then (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:87:41)
at process.internalTickCallback (internal/process/next_tick.js:77:7)
I've tried every solution listed here with no result.
Here is the relevant code:
// BaseDialog.spec.js
import { shallowMount, createLocalVue } from '#vue/test-utils'
import BaseDialog from '#/components/BaseDialog/BaseDialog'
import VueI18n from 'vue-i18n'
describe('BaseDialog', () => {
it('is called', () => {
let localVue = createLocalVue()
localVue.use(VueI18n)
const messages = {
gb: {
'ui.universal.label.ok': 'OK',
'ui.universal.label.cancel': 'Cancel'
}
}
const i18n = new VueI18n({
locale: 'gb',
fallbackLocale: 'gb',
messages
})
const wrapper = shallowMount(BaseDialog, {
i18n,
localVue
})
expect(wrapper.name()).toBe('BaseDialog')
expect(wrapper.isVueInstance()).toBeTruthy()
})
})
// BaseDialog.vue
<template>
<transition :name="animation">
<div v-if="isActive" class="dialog modal is-active" :class="size">
<div class="modal-background" #click="cancel('outside')" />
<div class="modal-card animation-content">
<header v-if="title" class="modal-card-head">
<p class="modal-card-title">{{ title }}</p>
</header>
<section
class="modal-card-body"
:class="{ 'is-titleless': !title, 'is-flex': hasIcon }"
>
<div class="media">
<div v-if="hasIcon" class="media-left">
<b-icon
:icon="icon ? icon : iconByType"
:pack="iconPack"
:type="type"
:both="!icon"
size="is-large"
/>
</div>
<div class="media-content">
<p v-html="message" />
<div v-if="hasInput" class="field">
<div class="control">
<input
ref="input"
v-model="prompt"
class="input"
:class="{ 'is-danger': validationMessage }"
v-bind="inputAttrs"
#keyup.enter="confirm"
/>
</div>
<p class="help is-danger">{{ validationMessage }}</p>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot">
<button
v-if="showCancel"
ref="cancelButton"
class="button"
#click="cancel('button')"
>
{{ cancelText }}
</button>
<button
ref="confirmButton"
class="button"
:class="type"
#click="confirm"
>
{{ confirmText }}
</button>
</footer>
</div>
</div>
</transition>
</template>
<script>
import Modal from '../BaseModal/BaseModal'
import config from '../../utils/config'
import { removeElement } from '../../utils/helpers'
export default {
name: 'BaseDialog',
extends: Modal,
props: {
title: {
type: String,
default: null
},
message: {
type: String,
default: null
},
icon: {
type: String,
default: null
},
iconPack: {
type: String,
default: null
},
hasIcon: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'is-primary'
},
size: {
type: String,
default: null
},
confirmText: {
type: String,
default: () => {
return config.defaultDialogConfirmText
? config.defaultDialogConfirmText
: this.$i18n('ui.universal.label.ok')
}
},
cancelText: {
type: String,
default: () => {
return config.defaultDialogCancelText
? config.defaultDialogCancelText
: this.$i18n('ui.universal.label.cancel')
}
},
hasInput: Boolean, // Used internally to know if it's prompt
inputAttrs: {
type: Object,
default: () => ({})
},
onConfirm: {
type: Function,
default: () => {}
},
focusOn: {
type: String,
default: 'confirm'
}
},
data() {
const prompt = this.hasInput ? this.inputAttrs.value || '' : ''
return {
prompt,
isActive: false,
validationMessage: ''
}
},
computed: {
/**
* Icon name (MDI) based on the type.
*/
iconByType() {
switch (this.type) {
case 'is-info':
return 'information'
case 'is-success':
return 'check-circle'
case 'is-warning':
return 'alert'
case 'is-danger':
return 'alert-circle'
default:
return null
}
},
showCancel() {
return this.cancelOptions.indexOf('button') >= 0
}
},
beforeMount() {
// Insert the Dialog component in body tag
this.$nextTick(() => {
document.body.appendChild(this.$el)
})
},
mounted() {
this.isActive = true
if (typeof this.inputAttrs.required === 'undefined') {
this.$set(this.inputAttrs, 'required', true)
}
this.$nextTick(() => {
// Handle which element receives focus
if (this.hasInput) {
this.$refs.input.focus()
} else if (this.focusOn === 'cancel' && this.showCancel) {
this.$refs.cancelButton.focus()
} else {
this.$refs.confirmButton.focus()
}
})
},
methods: {
/**
* If it's a prompt Dialog, validate the input.
* Call the onConfirm prop (function) and close the Dialog.
*/
confirm() {
if (this.$refs.input !== undefined) {
if (!this.$refs.input.checkValidity()) {
this.validationMessage = this.$refs.input.validationMessage
this.$nextTick(() => this.$refs.input.select())
return
}
}
this.onConfirm(this.prompt)
this.close()
},
/**
* Close the Dialog.
*/
close() {
this.isActive = false
// Timeout for the animation complete before destroying
setTimeout(() => {
this.$destroy()
removeElement(this.$el)
}, 150)
}
}
}
</script>
<style lang="scss">
.dialog {
.modal-card {
max-width: 460px;
width: auto;
.modal-card-head {
font-size: $size-5;
font-weight: $weight-semibold;
}
.modal-card-body {
.field {
margin-top: 16px;
}
}
.modal-card-foot {
justify-content: flex-end;
.button {
display: inline; // Fix Safari centering
min-width: 5em;
font-weight: $weight-semibold;
}
}
#include tablet {
min-width: 320px;
}
}
&.is-small {
.modal-card,
.input,
.button {
#include control-small;
}
}
&.is-medium {
.modal-card,
.input,
.button {
#include control-medium;
}
}
&.is-large {
.modal-card,
.input,
.button {
#include control-large;
}
}
}
</style>
I don't really know what else to try here. This is the beginning of a project where I must support 9 languages with over 500 keys a piece, so I've got to get this working. Any help is very much appreciated.
The problem was I was referencing this in the props. Props are processed before the component is instantiated and therefore I had no access to this. It's always the little things that make you bash your head into the wall hahaha.

How can I add condition in array on vue.js 2?

My vue component is like this :
<a :href="baseUrl+'/message/inbox'"
:class="{ 'active': currentPath === '/message/inbox' }"
>
Message
</a>
If they meet the conditions then the message menu will be active
But, I want to make it like this :
<a :href="baseUrl+'/message/inbox'"
:class="{ 'active': currentPath in array ('/message/inbox', '/message/inbox/detail') }"
>
Message
</a>
So it will check currentPath in the array
How can I do it?
Update :
If I have menu again like this :
<a :href="baseUrl+'/store/sale'"
:class="{ 'active': currentPath in array ('/store/sale', '/store/sale/detail') }"
>
Sale
</a>
Or more menu
How to implement it?
Update 2
<a :href="baseUrl+'/message/inbox'"
:class="{ 'active': isActive }"
>
Message
</a>
<a :href="baseUrl+'/store/sale'"
:class="{ 'active': isActiveSale }"
>
Message
</a>
computed: {
isActive () {
return ['/message/inbox', '/message/inbox/detail'].indexOf(this.currentPath) > -1
},
isActiveSale () {
return ['/store/sale', '/store/sale/detail'].indexOf(this.currentPath) > -1
}
}
You can use computed properties :
computed: {
currentPathInInbox: function() {
var arrayInbox = ['/message/inbox', '/message/inbox/detail'];
return arrayInbox.indexOf(this.currentPath) > -1;
}
}
and in template :
:class="{ 'active': currentPathInInbox }"
or with no computed properties :
:class="{ 'active': (currentPath === '/message/inbox' || (currentPath === '/message/inbox/detail') }"
UPDATED :
I think you need component :
Vue.component( 'linkWithPath', {
template: '<div><a :href="baseUrl + relativeUrl"' +
':class="{ \'active\': isActive }">' +
'<slot>Link name</slot></a></div>',
props: {
baseUrl: { type: String },
currentPath: { type: String, default: '' },
relativeUrl: { type: String }
},
computed: {
isActive: function() {
return [ this.relativeUrl, this.relativeUrl + '/detail'].indexOf(this.currentPath) > -1;
}
}
});
Vue.component( 'listOfLinksWithPath', {
template: '<div><link-with-path v-for="menuItem in menuArray"' +
':key="menuItem" :base-url="baseUrl" :current-path="currentPath"' +
':relative-url="menuItem.url">{{ menuItem.name }}</link-with-path></div>',
props: {
baseUrl: { type: String },
currentPath: { type: String },
menuArray: { type: Array }
}
});
new Vue({
el: "#app",
data: function() {
return {
baseUrl: 'http://www.CHANGETHISURL.com',
currentPath: '/message/inbox',
menuArray: [ { name: 'Message', url: '/message/inbox' },
{ name: 'Sale', url: '/store/sale' } ]
}
},
methods: {
changeCurrentPath: function() {
this.currentPath = '/store/sale'
}
}
});
a.active{
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.5/vue.js"></script>
<div id="app">
<p>Link is active when it is red.</p>
<list-of-links-with-path :base-url="baseUrl" :menu-array="menuArray" :current-path="currentPath"></list-of-links-with-path>
<br />
<button #click="changeCurrentPath" type="button">Change current path</button>
<br />
currentPath : {{ currentPath }}
</div>
Add a computed property.
computed: {
isActive () {
return ['/message/inbox', '/message/inbox/detail'].indexOf(this.currentPath) > -1
}
}
So you'll be able to use:
<a :href="baseUrl+'/message/inbox'"
:class="{ 'active': isActive }"
>
Message
</a>