I have a Vue component with a transition with a dynamic name. I'm trying to find a way to test that the transition name is set properly based on the props I pass into the component. Here's the component.
<template>
<aside :class="['cw-vue-modal', modalSizeClass]" v-show="showModal && modalName === currentModalName">
<transition :name="`${effect}-${speed}`" :duration="500">
<div class="cw-vue-modal-wrap" v-show="showModal">
<div class="cw-vue-modal-body">
<header>
<h2 v-if="currentModalTitle">{{ currentModalTitle }}</h2>
</header>
<article :class="['cw-vue-modal-content', {'cw-vue-modal-pad' : padContent}]">
<slot name="content"></slot>
</article>
</div>
</div>
</transition>
</aside>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
name: 'cw-modal',
props: {
modalName: {
required: true,
type: String
},
padContent: {
required: false,
type: Boolean
},
modalSize: {
required: false,
type: String
},
effect: {
required: false,
type: String
},
speed: {
required: false,
type: String,
default: 'normal'
},
maskLight: {
required: false,
type: Boolean
}
},
computed: {
...mapGetters(['showModal', 'currentModalName', 'currentModalTitle']),
modalSizeClass() {
if (this.modalSize === 'small') return 'cw-vue-modal-sm';
if (this.modalSize === 'large') return 'cw-vue-modal-lg';
return 'cw-vue-modal-md';
}
},
methods: {
...mapActions(['closeModal'])
}
};
</script>
I'm using mocha with chia and the avoriaz library to write unit test. Here is the test.
it('it adds the slide slow effect', () => {
getters = {
showModal: () => true,
currentModalName: () => 'my-modal',
currentModalTitle: () => null
};
store = new Vuex.Store({getters});
const wrapper = mount(Modal, {
propsData: { modalName: 'my-modal', effect: 'slide', speed: 'slow' },
store,
attachToDocument: true
});
expect($('.cw-vue-modal-wrap')).to.have.class('slide-slow-enter-active');
});
It doesn't seem like the class I expect is being inserted into the dom. Any help would be great.
Thanks
I am not 100% sure about how you would implement it into your project following best practices, but there are constructs in computer science like invariants and assertions.
Like I said, I'm not quite familiar with it either, but here's an idea:
you could use this methodology and insert invisible div's into your DOM while running the tests. For example, you can set a variable to true while doing the animation, then using this variable in v-bind:show="yourVar" inside the div tag and check the visibility of that element within your test.
I also didn't find any "official" way of doing it in the docs, so this workaround may be the way.. at leat that's how I did it when writing other functional tests ;)
Related
I have a component that I feed with props.
When an image throws a 404 I want to load a fallback image. So far so good.
However the #error function never fires when an image is broken and I can't figure out why! I never get 'hello event' in the console.
The component is on the first level of a nuxt page, my setup is a static SPA.
I tried to implement it as mentioned at the end of this github issue: https://github.com/vuejs/vue/issues/5404
<template>
<div class="channel">
<div class="img">
<img :src="imgUrl" :alt="name" :title="name" #error="getFallBackImage" />
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
chanCover: this.cover
}
},
props: {
name: {
type: String,
required: true
},
cover: {
type: String,
required: true
}
},
computed: {
imgUrl() {
console.log(this.chanCover)
return "channels/"+this.chanCover+".jpg"
},
},
methods: {
getFallBackImage(event) {
console.log("hello event")
this.chanCover = "default"
}
}
}
</script>
Background: I've built a standard single file component that takes a name prop and looks in different places my app's directory structure and provides the first matched component with that name. It was created to allow for "child theming" in my Vue.js CMS, called Resto. It's a similar principle to how WordPress looks for template files, first by checking the Child theme location, then reverting to the parent them if not found, etc.
Usage : The component can be used like this:
<!-- Find the PageHeader component
in the current child theme, parent theme,
or base components folder --->
<theme-component name="PageHeader">
<h1>Maybe I'm a slot for the page title!</h1>
</theme-component>
My goal : I want to convert to a functional component so it doesn't affect my app's render performance or show up in the Vue devtools. It looks like this:
<template>
<component
:is="dynamicComponent"
v-if="dynamicComponent"
v-bind="{ ...$attrs, ...$props }"
v-on="$listeners"
#hook:mounted="$emit('mounted')"
>
<slot />
</component>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'ThemeComponent',
props: {
name: {
type: String,
required: true,
default: '',
},
},
data() {
return {
dynamicComponent: null,
resolvedPath: '',
}
},
computed: {
...mapGetters('site', ['getThemeName']),
customThemeLoader() {
if (!this.name.length) {
return null
}
// console.log(`Trying custom theme component for ${this.customThemePath}`)
return () => import(`#themes/${this.customThemePath}`)
},
defaultThemeLoader() {
if (!this.name.length) {
return null
}
// console.log(`Trying default component for ${this.name}`)
return () => import(`#restoBaseTheme/${this.componentPath}`)
},
baseComponentLoader() {
if (!this.name.length) {
return null
}
// console.log(`Trying base component for ${this.name}`)
return () => import(`#components/Base/${this.name}`)
},
componentPath() {
return `components/${this.name}`
}, // componentPath
customThemePath() {
return `${this.getThemeName}/${this.componentPath}`
}, // customThemePath()
},
mounted() {
this.customThemeLoader()
.then(() => {
// If found in the current custom Theme dir, load from there
this.dynamicComponent = () => this.customThemeLoader()
this.resolvedPath = `#themes/${this.customThemePath}`
})
.catch(() => {
this.defaultThemeLoader()
.then(() => {
// If found in the default Theme dir, load from there
this.dynamicComponent = () => this.defaultThemeLoader()
this.resolvedPath = `#restoBaseTheme/${this.defaultThemePath}`
})
.catch(() => {
this.baseComponentLoader()
.then(() => {
// Finally, if it can't be found, try the Base folder
this.dynamicComponent = () => this.baseComponentLoader()
this.resolvedPath = `#components/Base/${this.name}`
})
.catch(() => {
// If found in the /components dir, load from there
this.dynamicComponent = () => import(`#components/${this.name}`)
this.resolvedPath = `#components/${this.name}`
})
})
})
},
}
</script>
I've tried SO many different approaches but I'm fairly new to functional components and render functions (never got into React).
The roadblock : I can't seem to figure out how to run the chained functions that I call in my original mounted() function. I've tried running it from inside the render function with no success.
Big Question
How can I find and dynamically import the component I'm targeting before I pass that component to the createElement function (or within my single file <template functional><template/>)?
Thanks all you Vue-heads! ✌️
Update: I stumbled across this solution for using the h() render function and randomly loading a component, but I'm not sure how to make it work to accept the name prop...
Late to the party, but I was in a similar situation, where I had a component in charge of conditionally render one of 11 different child components:
<template>
<v-row>
<v-col>
<custom-title v-if="type === 'title'" :data="data" />
<custom-paragraph v-else-if="type === 'paragraph'" :data="data" />
<custom-text v-else-if="type === 'text'" :data="data" />
... 8 more times
</v-col>
</v-row>
</template>
<script>
export default {
name: 'ProjectDynamicFormFieldDetail',
components: {
CustomTitle: () => import('#/modules/path/to/CustomTitle'),
CustomParagraph: () => import('#/modules/path/to/CustomParagraph'),
CustomText: () => import('#/modules/path/to/CustomText'),
... 8 more times
},
props: {
type: {
type: String,
required: true,
},
data: {
type: Object,
default: null,
}
},
}
</script>
which of course is not ideal and pretty ugly.
The functional equivalent I came up with is the following
import Vue from 'vue'
export default {
functional: true,
props: { type: { type: String, required: true }, data: { type: Object, default: null } },
render(createElement, { props: { type, data } } ) {
// prop 'type' === ['Title', 'Paragraph', 'Text', etc]
const element = `Custom${type}`
// register the custom component globally
Vue.component(element, require(`#/modules/path/to/${element}`).default)
return createElement(element, { props: { data } })
}
}
Couple of things:
lazy imports don't seem to work inside Vue.component, hence require().default is the way to go
in this case the prop 'type' needs to be formatted, either in the parent component or right here
I am currently in the process of writing a Carousel component and am trying to decide on the best method of implementation.
See the following component:
<template>
<!-- Carousel -->
<div class="carousel-container">
<div ref="carousel" class="carousel">
<slot></slot>
</div>
<carousel-navigation v-if="items.length" :orientation="orientation" :container="$refs.carousel" :items="items"></carousel-navigation>
</div>
</template>
<script>
import CarouselNavigation from 'core-js/components/common/CarouselNavigation';
export default {
components: {
'carousel-navigation': CarouselNavigation
},
props: {
orientation: {
type: String,
required: false,
default: 'horizontal',
validator: (value) => {
return ['horizontal', 'vertical'].indexOf(value) !== -1;
}
}
},
data() {
return {
items: []
}
},
mounted () {
this.items = [...this.$refs.carousel.children];
}
}
</script>
The carousel-navigation component accepts various props, but specifically items and container.
The container prop is expected to be a single DOM node, and the items prop is expected to be an array of DOM nodes.
The reason I need to pass DOM nodes is because the carousel-navigation needs to know where to scroll to (items) and what to scroll (container).
Is this bad practice? If so, what would be a better way to achieve this?
I have a series of checkboxes generated by a database. The database call isn't usually finished before the page loads.
This is part of my Vue.
folderList is a list of folders from the database, each has a key, and a doc.label for describing the checkbox and a doc.hash for using as a unique key.
<template>
<ul>
<li v-if="foldersList!=null" v-for="folder in folderData">
<Checkbox :id="folder.key" :name="folder.hash" label-position="right" v-model="searchTypeList[folder.hash]">{{ folder.doc.label }}</Checkbox>
</li>
</ul>
</template>
export default {
name: 'Menu',
components: {
Checkbox,
RouteButton
},
props: {
foldersList: {type: Array, required: true}
},
computed: {
...mapGetters({
searchListType: 'getSearchListType'
}),
searchTypeList: {
get() {
return this.searchTypeList;
},
set(newValue) {
console.log(newValue);
//Vuex store commit
this.$store.commit(Am.SET_SEARCH_TYPES, newValue);
}
}
},
methods: {
checkAllTypes: _.debounce(function (folderList){
const initialList = {};
folderList.forEach((folder) => {
initialList[folder.hash] = true;
});
this.$store.commit(Am.SET_SEARCH_TYPES, initialList);
}, 100)
},
mounted() {
//hacky way of prefilling data after it loads
this.$store.watch(
(state) => {
return this.foldersList;
},
this.checkAllTypes,
{
deep: false
}
);
}
Checkbox is a custom component with a sliding style checkbox, it's v-model is like this
<template>
<div class="checkbox">
<label :for="id" v-if="labelPosition==='left'">
<slot></slot>
</label>
<label class="switch">
<input type="checkbox" :name="name" :id="id" :disabled="isDisabled" v-bind:checked="checked" v-on:change="$emit('change', $event.target.checked)"/>
<span class="slider round"></span>
</label>
<label :for="name" v-if="labelPosition==='right'">
<slot></slot>
</label>
</div>
</template>
<script>
export default {
name: 'Checkbox',
model: {
prop: 'checked',
event: 'change'
},
props: {
id: {default: null, type: String},
name: {required: true, type: String},
isDisabled: {default: false, type: Boolean},
checked: Boolean,
labelPosition: {default: 'left', type: String},
value: {required: false}
}
};
</script>
I verified checkbox is working by using a simple non dynamic v-model and without the loop.
I want to collect an array of checked values.
In my example above, I tried with this computed to try and link to vuex like I have with other fields, but the get counts as a mutation because it is adding properties to the object as it loops through. I don't know how to solve this
Vuex:
const state = {
searchListType: {}
};
const getters = {
getSearchListType: function (state) {
return state.searchListType;
}
};
const mutations = {
[Am.SET_SEARCH_TYPES]: (state, types) => {
state.searchListType = types;
}
};
What is the correct way to link these up? I need the values in vuex so several sibling components can use the values and store them between page changes.
Also, what is the correct way to prefill the data? I assume I have a major structure problem here. FolderList is async and can load at any point, however it doesn't typically change after the application has loaded. It is populated by the parent, so the child just needs to wait for it to have data, and every time it changes, check off everything by default.
Thank you
I was going to suggest using a change event and method instead of computed get & set, but when testing I found the v-model works ok without any additional code. Below is a rough approximation of your scenario.
Maybe something in the custom component interaction is causing your issue?
Ref Customizing Components, did you use
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
in the Checkbox component?
Vue.component('Checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
new Vue({
el: '#app',
data: {
folderData: [],
searchTypeList: {}
},
created() {
// Dynamic checkbox simulation
setTimeout(() => {
this.folderData = [
{ key: 1 },
{ key: 2 },
{ key: 3 },
]
}, 2000)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul>
<li v-if="folderData != null" v-for="folder in folderData">
<Checkbox :id="folder.key" :name="folder.key"
v-model="searchTypeList[folder.key]"></Checkbox>
</li>
</ul>
{{searchTypeList}}
</div>
Handling Vuex updates
I think the simplest way to handle updates to the store is to split v-model into :checked and #change properties. That way your control does not attempt to write back to the store directly, but still takes it's value from the store directly and reacts to store changes (both changes from an api call and from this component's $store.commit() calls).
This is the relevant guide Vuex Form Handling.
<ul>
<li v-if="folderData != null" v-for="folder in folderData">
<Checkbox :id="folder.key" :name="folder.key"
:checked="theList[folder.key]"
#change="changeChecked(folder.key)"
></Checkbox>
</li>
</ul>
...
computed: {
...mapGetters({
theList: 'getSearchListType' // Standard getter with get() only
}),
},
methods: {
changeChecked(key) {
this.$store.commit('updateChecked', key) // Handle details in the mutation
}
}
On my app, I have multiple "upload" buttons and I want to display a spinner/loader for that specific button when a user clicks on it. After the upload is complete, I want to remove that spinner/loader.
I have the buttons nested within a component so on the file for the button, I'm receiving a prop from the parent and then storing that locally so the loader doesn't show up for all upload buttons. But when the value changes in the parent, the child is not getting the correct value of the prop.
App.vue:
<template>
<upload-button
:uploadComplete="uploadCompleteBoolean"
#startUpload="upload">
</upload-button>
</template>
<script>
data(){
return {
uploadCompleteBoolean: true
}
},
methods: {
upload(){
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
}
</script>
Button.vue:
<template>
<button
#click="onClick">
<button>
</template>
<script>
props: {
uploadComplete: {
type: Boolean
}
data(){
return {
uploadingComplete: this.uploadComplete
}
},
methods: {
onClick(){
this.uploadingComplete = false
this.$emit('startUpload')
}
</script>
Fixed event name and prop name then it should work.
As Vue Guide: Custom EventName says, Vue recommend always use kebab-case for event names.
so you should use this.$emit('start-upload'), then in the template, uses <upload-button #start-upload="upload"> </upload-button>
As Vue Guide: Props says,
HTML attribute names are case-insensitive, so browsers will interpret
any uppercase characters as lowercase. That means when you’re using
in-DOM templates, camelCased prop names need to use their kebab-cased
(hyphen-delimited) equivalents
so change :uploadComplete="uploadCompleteBoolean" to :upload-complete="uploadCompleteBoolean"
Edit: Just noticed you mentioned data property=uploadingComplete.
It is easy fix, add one watch for props=uploadComplete.
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('upload-button', {
template: `<div> <button #click="onClick">Upload for Data: {{uploadingComplete}} Props: {{uploadComplete}}</button>
</div>`,
props: {
uploadComplete: {
type: Boolean
}
},
data() {
return {
uploadingComplete: this.uploadComplete
}
},
watch: { // watch prop=uploadComplete, if change, sync to data property=uploadingComplete
uploadComplete: function (newVal) {
this.uploadingComplete = newVal
}
},
methods: {
onClick() {
this.uploadingComplete = false
this.$emit('start-upload')
}
}
})
new Vue({
el: '#app',
data() {
return {
uploadCompleteBoolean: true
}
},
methods: {
upload() {
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
},
changeStatus() {
this.uploadCompleteBoolean = !this.uploadCompleteBoolean
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<button #click="changeStatus()">Toggle Status {{uploadCompleteBoolean}}</button>
<p>Status: {{uploadCompleteBoolean}}</p>
<upload-button :upload-complete="uploadCompleteBoolean" #start-upload="upload">
</upload-button>
</div>
The UploadButton component shouldn't have uploadingComplete as local state (data); this just complicates the component since you're trying to mix the uploadComplete prop and uploadingComplete data.
The visibility of the spinner should be driven by the parent component through the prop, the button itself should not be responsible for controlling the visibility of the spinner through local state in response to clicks of the button.
Just do something like this:
Vue.component('upload-button', {
template: '#upload-button',
props: ['uploading'],
});
new Vue({
el: '#app',
data: {
uploading1: false,
uploading2: false,
},
methods: {
upload1() {
this.uploading1 = true;
setTimeout(() => this.uploading1 = false, Math.random() * 1000);
},
upload2() {
this.uploading2 = true;
setTimeout(() => this.uploading2 = false, Math.random() * 1000);
},
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<upload-button :uploading="uploading1" #click="upload1">Upload 1</upload-button>
<upload-button :uploading="uploading2" #click="upload2">Upload 2</upload-button>
</div>
<template id="upload-button">
<button #click="$emit('click')">
<template v-if="uploading">Uploading...</template>
<slot v-else></slot>
</button>
</template>
Your question seems little bit ambiguë, You can use watch in that props object inside the child component like this:
watch:{
uploadComplete:{
handler(val){
//val gives you the updated value
}, deep:true
},
}
by adding deep to true it will watch for nested properties in that object, if one of properties changed you ll receive the new prop from val variable
for more information : https://v2.vuejs.org/v2/api/#vm-watch
if not what you wanted, i made a real quick example,
check it out hope this helps : https://jsfiddle.net/K_Younes/64d8mbs1/