Vue Test utils, Testing scoped slot logic within parent context - vue.js

I have a "parent" component whose sole focus is to implement a custom table component called <VTable>
I would like to test the logic specific to the parent as it is used in the context of various scoped slots on the child table.
the slots are dynamically declared based on properties supplied in the header.
Given:
const headers = [
{ trackBy: 'lastName', sortable: true, width: '85px' },
{ trackBy: 'status', sortable: true, width: '95px' }
]
then the following slots will be explosed in an implementation
<template>
<div class="Foo">
<div class="Foo-table">
<VTable
:headers="headers"
:rows="rows"
:sort="Sort"
:numeration="getNumeration"
:loading="isLoading"
striped
numerated
#sort="onSort"
>
<template v-slot:row.lastName="{ row, index }">
... Custom Implementation ...
</template>
<template v-slot:row.status="{ row, index }">
... Custom Implementation ...
</template>
...
I am interested in the specific logic within each of the scoped slots.
I can resort to
const wrapper = shallowMount(
index,
{
localVue,
mocks,
store: new Vuex.Store(store),
stubs: {
'VTable': VTable
}
})
and find the raw output via:
const table = wrapper.findComponent({ name: 'VTable' })
table.vnode.elm.innerHTML
but any attempts to use find/findComponent/etc. whether called from wrapper or table are unsuccessful. Is there a way to do this? I imagine matching raw html is discouraged.

The solution in my instance was to stub every other component bar the VTable component with the scoped slots.
const wrapper = mount(
index,
{
localVue,
mocks,
store: new Vuex.Store(store),
stubs: {
FooComponent: { template: '<div class="FooComponent"></div>' },
BarComponent: true,
...
}
})
This approach carries the caveat that tests could become brittle if the component that wasn't stubbed is less generic (that in itself might be an indicator that the component in question requires refactoring)

Related

Proper way to use Vuex with child components

I have used VueJS and Vuex before. Not in production, just for some simple, personal side projects and it felt pretty straight forward.
However, now I face a problem that is just slightly more complex but using Vuex already feels way more complicated. So I'm looking for some guidance here.
In the main view I present a list of cards to the user. A card is an editable form of about ten properties (inputs, selects, etc.). I obviously implemented a component for these cards, since they are repeated in a list view. My first naive approach was to fetch a list of - let's call it forms - from my store and then using v-for to present a card for each form in that list of forms. The form is passed to the child component as a property.
Now I want to bind my form controls to the properties. To do this I implemented Two-way Computed Properties to properly utilize my store mutations. But it feels extremely repetitive to implement a custom computed property with getter and setter plus mutation for every property on my model. Additionally, the forms are an array in my state. So I have to pass the id of the form to edit in the store to every mutation.
Something else I had in mind was just passing the store id of the form to the child component and have "by id" getters for every property of my model as well as a matching mutation. But this doesn't feel like the proper way to do this, either. It is essentially the same, right?!
Is there a better solution to this problem? Maybe I'm just missing something or I'm overcomplicating things.
A trimmed down example:
Editor.vue:
<template>
<v-container>
<EditableCard v-for="(card, i) in cards" :key="i" :card="card" />
</v-container>
</template>
<script>
import EditableCard from "#/components/EditableCard";
import { mapGetters } from "vuex";
export default {
name: "Editor",
components: {
EditableCard
},
computed: {
...mapGetters("cards", {
cards: "list"
})
}
};
</script>
EditableCard:
<template>
<v-card>
<v-form>
<v-card-title>
<v-text-field v-model="title"></v-text-field>
</v-card-title>
<v-card-text>
<v-text-fieldv-model="text"></v-text-field>
<!-- And some more fields... -->
</v-card-text>
</v-form>
</v-card>
</template>
<script>
import { mapMutations } from "vuex";
export default {
name: "EditableCard",
props: {
card: Object
},
computed: {
title: {
get() {
return card.title;
},
set(value) {
this.setCardTitle(this.card.id, value);
}
},
text: {
get() {
return card.text;
},
set(value) {
this.setCardText(this.card.id, value);
}
}
// Repeat for every form input control
},
methods: {
...mapMutations("cards", {
setCardTitle: "setTitle",
setCardText: "setText"
// Repeat for every form input control
})
}
};
</script>
It would be nice to create a computed setter for the whole form object using a clone, but this won't work because changes won't trigger the computed setter.
If anyone wants to explore this interesting failure, see here)
To work around this, you can use a watch and a data clone:
<v-form>
<v-text-field v-model="clone.title" />
<v-text-field v-model="clone.text" />
</v-form>
props: ['index', 'card'],
data() {
return {
clone: {}
}
},
watch: {
card: {
handler(card) {
this.clone = { ...card }
},
immediate: true,
deep: true
},
clone: {
handler(n,o) {
if (n === o) {
this.$store.commit('SET_CARD', { index: this.index, card: n })
}
},
deep: true
}
}
Your v-for:
<EditableCard v-for="(card, index) in cards" :card="card" :index="index" :key="index" />
The mutation:
mutations: {
SET_CARD(state, { index, card }) {
Vue.set(state.cards, index, card);
}
}
This is way more complex than it should need to be... but it works.

Extend data object that is passed to Vue component with shared methods/props

this is my first Vue question as I am still learning.
So let's say I have this component:
Vue.component('list-card', {
props: ['entry'],
computed: {
isArchived: function() { return this.entry.Status == 'Archived' }
},
template: `<div class="card">
<span class="icon" :style="{ opacity: isArchived ? 0.2 : 1 }"></span>
</div>`
});
Now, I want to extract the method isArchived because I may have other similar components that render the card - grid-card, board-card, etc... and they all will need the isArchived which is not in the entry object.
To me, the problem is that the entry object being passed to the component is a POJO - plain data object with no methods or computed properties. So I have to define the property on the component itself, not the data object. Which is wrong from my OOP experience - the entry.IsArchived() or entry.IsArchived prop would make more sense.
Now, I can extend this object before passing the entry object to the component, or even when getting the entries in some store, etc. But I don't want to do that - 1. Only components should know if they need isArchived or not, and 2. This affects performance - component may decide not to use isArchived but I already calculated that.
So what's the proper VueJS way to solve this?
As docs said, Mixins are a flexible way to distribute reusable functionalities for Vue components, so you can use the mixins
// define a mixin object
var myMixin = {
props: ['entry'],
computed: {
isArchived: function() { return this.entry.Status == 'Archived' }
},
}
// define a component that uses this mixin
var Component = Vue.extend({
mixins: [myMixin],
template: `<div class="card">
<span class="icon" :style="{ opacity: isArchived ? 0.2 : 1 }"></span>
</div>`
})
var component = new Component() // => "hello from mixin!"

Is it possible to use dynamic scoped slots to override column values inside <v-data-table>?

I'm trying to create a reusable table component that utilizes Vuetify's v-data-table component in order to group common aspects such as a search bar, table actions (refresh, create, etc.) and other features that all of my tables will have. However, I'm running into issues with implementing dynamic, scoped slots inside the table component to account for custom columns. Think of columns like actions, formatted ISO strings, etc.
Here's a simplified example of what I'm trying currently. In the example, I am passing the array customColumns to CustomDataTable.vue as a prop. customColumns has one element with two properties. The slotName property specifies the name of the slot that I'd like to reference in the parent component. The itemValue property specifies the header value that CustomDataTable.vue should override and replace with a scoped slot. The scoped slot is then used in the parent component to correctly format the date in the 'Created At' column.
Parent Component that is implementing the table component:
<template>
<custom-data-table
:items="items"
:headers="headers"
:customColumns="customColumns"
>
<template v-slot:custom-column="slotProps">
<span>{{ formatDate(slotProps.item.createdAt) }}</span>
</template>
</custom-data-table>
</template>
<script>
import CustomDataTableVue from '#/components/table/CustomDataTable.vue'
export default {
data: () => ({
items: [
{
id: 0,
createdAt: new Date().toISOString(),
...
},
...
],
headers: [
{
text: 'Created At',
value: 'createdAt',
sortable: true
},
...
],
customColumns: [
{
slotName: 'custom-column',
itemValue: 'createdAt'
}
]
})
}
</script>
CustomDataTable.vue
<template>
<v-data-table
:items="items"
:headers="headers"
>
<template v-for="column in customColumns" v-slot:item[column.itemValue]="{ item }">
<slot :name="column.slotName" :item="item"/>
</template>
</v-data-table>
</template>
<script>
export default {
name: 'custom-data-table',
props: {
items: {
type: Array,
required: true
},
headers: {
type: Array,
required: true
},
customColumns: {
type: Array
}
}
}
</script>
Is there a way to achieve this? The example does not override the column values and just displays the createdAt ISO string unformatted. I believe the problem might be coming from how I'm assigning the template's slot in CustomDataTable.vue, but I'm sure how else you could dynamically specify a template's slot. Any ideas?
I think the syntax for dynamic slots should be:
<template
v-for="column in customColumns"
v-slot:[`item.${column.itemValue}`]="{ item }"
>
<slot :name="column.slotName" :item="item"/>
</template>

Vue- best practice for loops and event handlers

I am curious if it is better to include methods within loops instead of using v-if. Assume the following codes work (they are incomplete and do not)
EX: Method
<template >
<div>
<div v-for="(d, i) in data" v-bind:key="i">
<span v-on:click="insertPrompt($event)">
{{ d }}
</span>
</div>
</div>
</template>
<script>
export default {
data() {
data:[
.....
]
},
methods:{
insertPrompt(e){
body.insertBefore(PROMPT)
}
}
}
</script>
The DOM would be updated via the insertPrompt() function which is just for display
EX: V-IF
//Parent
<template >
<div>
<div v-for="(d, i) in data" v-bind:key="i">
<child v-bind:data="d"/>
</div>
</div>
</template>
<script>
import child from './child'
export default {
components:{
child
},
data() {
data:[
.....
]
},
}
</script>
//Child
<template>
<div>
<span v-on:click="display != display">
{{ d }}
</span>
<PROMPT v-if="display"/>
</div>
</template>
<script>
import child from './child'
export default {
components:{
child
},
data(){
return {
display:false
}
},
props: {
data:{
.....
}
},
}
</script>
The PROMPT is a basic template that is rendered with the data from the loop data click.
Both methods can accomplish the same end result. My initial thought is having additional conditions within a loop would negatively impact performance?!?!
Any guidance is greatly appreciated
Unless you are rendering really huge amounts of items in your loops (and most of the times you don't), you don't need to worry about performance at all. Any differences will be so small nobody will ever notice / benefit from having it a tiny touch faster.
The second point I want to make is that doing your own DOM manipulations is often not the best idea: Why do modern JavaScript Frameworks discourage direct interaction with the DOM
So I would in any case stick with the v-if for conditionally rendering things. If you want to care about performance / speed here, you might consider what exactly is the way your app will be used and decide between v-if and v-show. Citing the official documentation:
v-if is “real” conditional rendering because it ensures that event
listeners and child components inside the conditional block are
properly destroyed and re-created during toggles.
v-if is also lazy: if the condition is false on initial render, it
will not do anything - the conditional block won’t be rendered until
the condition becomes true for the first time.
In comparison, v-show is much simpler - the element is always rendered
regardless of initial condition, with CSS-based toggling.
Generally speaking, v-if has higher toggle costs while v-show has
higher initial render costs. So prefer v-show if you need to toggle
something very often, and prefer v-if if the condition is unlikely to
change at runtime.
https://v2.vuejs.org/v2/guide/conditional.html#v-if-vs-v-show
There are numerous solutions to solving this issue, but let's stick to 3. Options 2 and 3 are better practices, but option 1 works and Vue was designed for this approach even if hardcore developers might frown, but stick yoru comfort level.
Option 1: DOM Manipulation
Your data from a click, async, prop sets a condition for v-if or v-show and your component is shown. Note v-if removes the DOM element where v-show hides the visibility but the element is still in the flow. If you remove the element and add its a complete new init, which sometimes works in your favor when it come to reactivity, but in practice try not to manipulate the DOM as that will always be more expensive then loops, filters, maps, etc.
<template >
<div>
<div v-for="(d, i) in getData"
:key="i">
<div v-if="d.active">
<child-one></child-one>
</div>
<div v-else-if="d.active">
<child-two></child-two>
</div>
</div>
</div>
</template>
<script>
import ChildOne from "./ChildOne";
import ChildTwo from "./ChildTwo";
export default {
components: {
ChildOne,
ChildTwo
},
data() {
return {
data: [],
}
},
computed: {
getData() {
return this.data;
},
},
mounted() {
// assume thsi woudl come from async but for now ..
this.data = [
{
id: 1,
comp: 'ChildOne',
active: false
},
{
id: 2,
comp: 'ChildTwo',
active: true
},
];
}
}
</script>
Option 2: Vue's <component> component
Always best to use Vue built in component Vue’s element with the is special attribute: <component v-bind:is="currentTabComponent"></component>
In this example we pass a slug or some data attribute to activate the component. Note we have to load the components ahead of time with the components: {}, property for this to work i.e. it has to be ChildOne or ChildTwo as slug string. This is often used with tabs and views to manage and maintain states.
The advantage of this approach is if you have 3 form tabs and you enter data on one and jump to the next and then back the state / data is maintained, unlike v-if where everything will be rerendered / lost.
Vue
<template >
<div>
<component :is="comp"/>
</div>
</template>
<script>
import ChildOne from "./ChildOne";
import ChildTwo from "./ChildTwo";
export default {
components: {
ChildOne,
ChildTwo
},
props: ['slug'],
data() {
return {
comp: 'ChildOne',
}
},
methods: {
setComponent () {
// assume prop slug passed from component or router is one of the components e.g. 'ChildOne'
this.comp = this.slug;
}
},
mounted() {
this.nextTick(this.setModule())
}
}
</script>
Option 3: Vue & Webpack Async and Dynamic components.
When it comes to larger applications or if you use Vuex and Vue Route where you have dynamic and large number of components then there are a number of approaches, but I'll stick to one. Similar to option 2, we are using the component element, but we are using WebPack to find all Vue files recursively with the keyword 'module'. We then load these dynamically / asynchronous --- meaning they will only be loaded when needed and you can see this in action in network console of browser. This means I can build components dynamically (factory pattern) and render them as needed. Example, of this might be if a user adds projects and you have to build and config views dynamically for projects created e.g. using vue router you passed it a ID for a new project, then you would need to dynamically load an existing component or build and load a factory built one.
Note: I'll use v-if on a component element if I have many components and I'm unsure the user will need them. I don't want to maintain state on large collections of components because I will end up memory and with loads of observers / watches / animations will most likely end up with CPU issues
<template >
<div>
<component :is="module" v-if="module"/>
</div>
</template>
<script>
const requireContext = require.context('./', true, /\.module\.vue$/);
const modules = requireContext.keys()
.map(file =>
[file.replace(/(.*\/(.+?)\/)|(\.module.vue$)/g, ''), requireContext(file)]
)
.reduce((components, [name, component]) => {
// console.error("components", components)
components[name] = component.default || component
return components
}, {});
export default {
data() {
return {
module: [],
}
},
props: {
slug: {
type: String,
required: true
}
},
computed: {
getData() {
return this.data;
},
},
methods: {
setModule () {
let module = this.slug;
if (!module || !modules[module]) {
module = this.defaultLayout
}
this.module = modules[module]
}
},
mounted() {
this.nextTick(this.setModule())
}
}
</script>
My initial thought is having additional conditions within a loop would negatively impact performance?
I think you might be confused by this rule in the style guide that says:
Never use v-if on the same element as v-for.
It's only a style issue if you use v-if and v-for on the same element. For example:
<div v-for="user in users" v-if="user.isActive">
But it's not a problem if you use v-if in a "child" element of a v-for. For example:
<div v-for="user in users">
<div v-if="user.isActive">
Using v-if wouldn't have a more negative performance impact than a method. And I'm assuming you would have to do some conditional checks inside your method as well. Remember that even calling a method has some (very small) performance impact.
Once you use Vue, I think it's a good idea not to mix it up with JavaScript DOM methods (like insertBefore). Vue maintains a virtual DOM which helps it to figure out how best to update the DOM when your component data changes. By using JavaScript DOM methods, you won't be taking advantage of Vue's virtual DOM anymore.
By sticking to Vue syntax you also make your code more understandable and probably more maintainable other developers who might read or contribute to your code later on.

Attach v-model to a dynamic element added with .appendChild in Vue.js

I'm working with a library that doesn't have a Vue.js wrapper.
The library appends elements in the DOM in a dynamic way.
I want to be able to bind the v-model attribute to those elements with Vue and once appended work with them in my model.
I've done this in the past with other reactive frameworks such as Knockout.js, but I can't find a way to do it with vue.js.
Any pay of this doing?
It should be something among these lines I assume:
var div = document.createElement('div');
div.setAttribute('v-model', '{{demo}}');
[VUE CALL] //tell vue.js I want to use this element in my model.
document.body.appendChild(div);
You could create a wrapper component for your library and then setup custom v-model on it to get a result on the lines of what you're looking for. Since your library is in charge of manipulating the DOM, you'd have to hook into the events provided by your library to ensure your model is kept up-to-date. You can have v-model support for your component by ensuring two things:
It accepts a value prop
It emits an input event
Here's an example of doing something similar: https://codesandbox.io/s/listjs-jquery-wrapper-sdli1 and a snipper of the wrapper component I implemented:
<template>
<div>
<div ref="listEl">
<ul ref="listUlEl" class="list"></ul>
</div>
<div class="form">
<div v-for="variable in variables" :key="variable">
{{ variable }}
<input v-model="form[variable]" placeholder="Enter a value">
</div>
<button #click="add()">Add</button>
</div>
</div>
</template>
<script>
export default {
props: ["value", "variables", "template"],
data() {
return {
form: {}
};
},
mounted() {
this.list = new List(
this.$refs.listEl,
{
valueNames: this.variables,
item: this.template
},
this.value
);
this.createFormModels();
},
methods: {
createFormModels() {
for (const variable of this.variables) {
this.$set(this.form, variable, "");
}
},
add() {
this.$emit("input", [
...this.value,
{
id: this.value.slice(-1)[0].id + 1,
...this.form
}
]);
}
},
watch: {
value: {
deep: true,
handler() {
this.$refs.listUlEl.innerHTML = "";
this.list = new List(
this.$refs.listEl,
{
valueNames: this.variables,
item: this.template
},
this.value
);
}
}
},
beforeDestroy() {
// Do cleanup, eg:
// this.list.destroy();
}
};
</script>
Key points:
Do your initialization of the custom library on mounted() in order to create the DOM. If it needs an element to work with, provide one via <template> and put a ref on it. This is also the place to setup event listeners on your library so that you can trigger model updates via $emit('value', newListOfStuff).
watch for changes to the value prop so that you can reinitialize the library or if it provides a way to update its collection, use that instead. Make sure to cleanup the previous instance if the library provides support for it as well as unbind event handlers.
Call any cleanup operations, event handler removals inside beforeDestroy.
For further reference:
https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components