Vue Jest Pass Data to Component - vuejs2

new to testing and was wondering if there was another way to inject data back to the component other than what's on the docs
The Docs
const Component = {
template: `
<div>
<span id="foo">{{ foo }}</span>
<span id="bar">{{ bar }}</span>
</div>
`,
data() {
return {
foo: 'foo',
bar: 'bar'
}
}
}
const wrapper = mount(Component, {
data() {
return {
bar: 'my-override'
}
}
})
wrapper.find('#foo').text() // 'foo'
wrapper.find('#bar').text() // 'my-override'
It seems like the relevant parts of the component are copied (mocked?) into the test itself and the test + data are self-containing. Is there a way to send the data back into the component and test what's there?
Ideally I want to be able to tweak the value of a variable from the test, and then have that pass to the component to display the correct card.
Something like:
<template>
<div id="wrapper">
<div v-if="cardOne" id="card1">Text 1</div>
<div v-if="cardTwo" id="card2">Text 2</div>
</div>
</template>
<script>
export default {
methods: {
//loop to go through numberArray
// if x = 1, this.cardOne = true and card1 is shown
// if x=2, this.cardTwo = true and card2 is shown etc
//Can I manually set x = 1 from the test, and then see if cardOne renders on the component?
},
data() {
return {
numberArray: [1, 2],
cardOne: false,
cardTwo: false
};
}
};
</script>

If your component gets data via props you can use props (Vue 3) or propsData (Vue 2) property of options object to pass data to it when you call shallowMount/mount.
If your component has some default values after instantiation and you want to change them your way is correct. You pass data via data method and if you want to change these values later in the test you should use setData method of your wrapper instance as it's shown in docs. API of this method is the same for both Vue 2 and Vue 3 apps.

Related

create a reusable component in vue.js

How do I make my vue.js component more general and reusable and can pass only the neccessary data?
this is what I want to build:
Structure:
the component has a header
the component has different inputs
the component has a submit button
the component has a list as a footer - that is passed depending on the input
My Approach
the parent
// App.vue
<template>
<div>
<!-- Component A -->
<SettingsCard
:listA="listA"
cardType="CompA"
>
<template v-slot:header>
Foo - Component A
</template>
</SettingsCard>
<!-- Component B -->
<SettingsCard
:listB="listB"
cardType="CompB"
>
<template v-slot:header>
Bar - Component B
</template>
</SettingsCard>
</div>
</template>
the child:
// SettingsCard.vue
<template>
<div>
<slot name="header"></slot>
<div v-if="cardType === 'CompA'">
<!-- Show input and submit button for component a -->
</div>
<div v-if="cardType === 'CompB'">
<!-- Show input and submit button for component b -->
</div>
<ListComponent
:cardType="cardType"
:list="computedList"
/>
</div>
</template>
<script>
export default {
props: {
cardType: String, // for the v-if conditions
listA: Array,
ListB: Array
},
data() {
return {
namefromCompA: '', // input from component A
namefromCompB: '' // input from component B
}
},
computed: {
computedList() {
// returns an array and pass as prop the the card footer
}
}
}
</script>
The problems
I have undefined props and unused data in my SettingsCard.vue component
// CompA:
props: {
cardType: 'compA',
listA: [1, 2, 3], // comes from the parent
listB: undefined // how to prevent the undefined?
}
// CompA:
data() {
return {
namefromCompA: 'hello world',
namefromCompB: '' // unused - please remove me
}
}
to use v-if="cardType === 'compA'" feels wrong
Do you have a better approach in mind to make this component reusable and remove anything unnecessary?
use a method instead of "cardType === 'CompA'".
just try this in your SettingsCard.vue
methods: {
showMeWhen(type) {
return this.cardType === type;
},
},
}
and your v-if render condition would be like:
v-if="showMeWhen('compA')"
update
for exmaple in your namefromCompA/B you can just pass a new prop to display the correct name.
props: {
cardType: String, // for the v-if conditions
listA: Array,
ListB: Array,
namefromComponent: {
type: String,
default: 'NoName'
}
},
then in your usage you just pass it like you do with the other props.
<SettingsCard
:listB="listB"
cardType="CompB"
namefrom-component="my Name for component B"
>

Passing props to Vue root instance via attributes on element the app is mounted on

I am terribly new to Vue, so forgive me if my terminology is off. I have a .NET Core MVC project with small, separate vue pages. On my current page, I return a view from the controller that just has:
#model long;
<div id="faq-category" v-bind:faqCategoryId="#Model"></div>
#section Scripts {
<script src="~/scripts/js/faqCategory.js"></script>
}
Where I send in the id of the item this page will go grab and create the edit form for. faqCategory.js is the compiled vue app. I need to pass in the long parameter to the vue app on initialization, so it can go fetch the full object. I mount it with a main.ts like:
import { createApp } from 'vue'
import FaqCategoryPage from './FaqCategoryPage.vue'
createApp(FaqCategoryPage)
.mount('#faq-category');
How can I get my faqCategoryId into my vue app to kick off the initialization and load the object? My v-bind attempt seems to not work - I have a #Prop(Number) readonly faqCategoryId: number = 0; on the vue component, but it is always 0.
My FaqCategoryPAge.vue script is simply:
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Prop } from 'vue-property-decorator'
import Card from "#/Card.vue";
import axios from "axios";
import FaqCategory from "../shared/FaqCategory";
#Options({
components: {
Card,
},
})
export default class FaqCategoryPage extends Vue {
#Prop(Number) readonly faqCategoryId: number = 0;
mounted() {
console.log(this.faqCategoryId);
}
}
</script>
It seems passing props to root instance vie attributes placed on element the app is mounting on is not supported
You can solve it using data- attributes easily
Vue 2
const mountEl = document.querySelector("#app");
new Vue({
propsData: { ...mountEl.dataset },
props: ["message"]
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Vue 3
const mountEl = document.querySelector("#app");
Vue.createApp({
props: ["message"]
}, { ...mountEl.dataset }).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Biggest disadvantage of this is that everything taken from data- attributes is a string so if your component expects something else (Number, Boolean etc) you need to make conversion yourself.
One more option of course is pushing your component one level down. As long as you use v-bind (:counter), proper JS type is passed into the component:
Vue.createApp({
components: {
MyComponent: {
props: {
message: String,
counter: Number
},
template: '<div> {{ message }} (counter: {{ counter }}) </div>'
}
},
}).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app">
<my-component :message="'Hello from HTML'" :counter="10" />
</div>
Just an idea (not a real problem)
Not really sure but it can be a problem with Props casing
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
Try to change your MVC view into this:
<div id="faq-category" v-bind:faq-category-id="#Model"></div>
Further to Michal LevĂ˝'s answer regarding Vue 3, you can also implement that pattern with a Single File Component:
app.html
<div id="app" data-message="My Message"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const mountEl = document.querySelector("#app");
Vue.createApp(MyComponent, { ...mountEl.dataset }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
Or you could even grab data from anywhere on the parent HTML page, eg:
app.html
<h1>My Message</h1>
<div id="app"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const message = document.querySelector('h1').innerText;
Vue.createApp(MyComponent, { message }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
To answer TheStoryCoder's question: you would need to use a data prop. My answers above demonstrate how to pass a value from the parent DOM to the Vue app when it is mounted. If you wanted to then change the value of message after it was mounted, you would need to do something like this (I've called the data prop myMessage for clarity, but you could also just use the same prop name message):
<template>
{{ myMessage }}
<button #click="myMessage = 'foo'">Foo me</button>
</template>
<script>
export default {
props: {
message: String
},
data() {
return {
myMessage: this.message
}
}
};
</script>
So I'm not at all familiar with .NET and what model does, but Vue will treat the DOM element as a placeholder only and it does not extend to it the same functionality as the components within the app have.
so v-bind is not going to work, even without the value being reactive, the option is not there to do it.
you could try a hack to access the value and assign to a data such as...
const app = Vue.createApp({
data(){
return {
faqCategoryId: null
}
},
mounted() {
const props = ["faqCategoryId"]
const el = this.$el.parentElement;
props.forEach((key) => {
const val = el.getAttribute(key);
if(val !== null) this[key] = (val);
})
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="12">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
where you get the value from the html dom element, and assign to a data. The reason I'm suggesting data instead of props is that props are setup to be write only, so you wouldn't be able to override them, so instead I've used a variable props to define the props to look for in the dom element.
Another option
is to use inject/provide
it's easier to just use js to provide the variable, but assuming you want to use this in an mvc framework, so that it is managed through the view only. In addition, you can make it simpler by picking the exact attributes you want to pass to the application, but this provides a better "framework" for reuse.
const mount = ($el) => {
const app = Vue.createApp({
inject: {
faqCategoryId: {
default: 'optional'
},
},
})
const el = document.querySelector($el)
Object.keys(app._component.inject).forEach(key => {
if (el.getAttribute(key) !== null) {
app.provide(key, el.getAttribute(key))
}
})
app.mount('#app')
}
mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="66">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
As i tried in the following example
https://codepen.io/boussadjra/pen/vYGvXvq
you could do :
mounted() {
console.log(this.$el.parentElement.getAttribute("faqCategoryId"));
}
All other answers might be valid, but for Vue 3 the simple way is here:
import {createApp} from 'vue'
import rootComponent from './app.vue'
let rootProps = {};
createApp(rootComponent, rootProps)
.mount('#somewhere')

Vue stored valued through props not being reactive

So I pass value using [props] and stored it in child component's data. However, when passing [props] value changes from parent, it's not updating in child component's data. Is there a fix for this..?
Here is the link to w3 test (I tried to clarify the problem as much as possible here)
<div id='app'>
<div id='parent'>
<button #click='current_value()'>Click to see parent value</button>
<br><br>
<button #click='change_value($event)'>{{ txt }}</button>
<br><br>
<child-comp :test-prop='passing_data'></child-comp>
</div>
<br><br>
<center><code>As you can see, this methods is <b>NOT</b> reactive!</code></center>
</div>
<script>
new Vue({
el: "#parent",
data: {
passing_data: 'Value',
txt: 'Click to change value'
},
methods: {
current_value(){
alert(this.passing_data);
},
change_value(e){
this.passing_data = 'New Vaule!!';
this.txt = 'Now click above button again to see new value';
e.target.style.backgroundColor = 'red';
e.target.style.color = 'white';
}
},
components: {
"child-comp": {
template: `
<button #click='test()'>Click here to see child (stored) value</button>
`,
props: ['test-prop'],
data(){
return {
stored_data: this.testProp
}
},
methods: {
test(){
alert(this.stored_data);
}
},
watch: {
stored_data(){
this.stored_data = this.testProp;
}
}
}
}
});
Props have one way data flow, that's why it doesn't react when you update it from the parent component. Define a clone of your prop at data to make it reactive, and then you can change the value within the child component.
Short answer: you don't need stored_data. Use alert(this.testProp) directly.
Long answer: when child component is created, stored_data get it's value from this.testProp. But data is local, it won't change automatically. That's why you need to watch testProp and set it again. But is not working because of a simple mistake, your watch should be:
watch: {
testProp(){ // here was the mistake
this.stored_data = this.testProp;
}
}

how do I pass the value from child to parent with this.$emit

What I trying to achieve here is to pass the const randomNumber inside the child component [src/components/VueForm/FormQuestion.vue] that need to be passed to parent component [src/App.vue]. Therefore I use $emit to pass the date, but since this is my first time working with $emit, I am not really sure how to do that. Could someone help me with this.
In order to run this app, I would add a working code snippet. Click on the start button and fill in the input fields. When the input field validates correctly it will pop up the button and if the user clicks on that is should pass the data to the parent. At the end it should be stored inside the App.vue in localStorage, so therefore I want to receive the randomNumber from that child component.
working code snippet here
// child component
<template>
<div class="vue-form__question">
<span class="question" :class="{ big: !shouldShowNumber }"> {{ getRandomNumber() }} </span>
</div>
</template>
<script>
export default {
methods: {
getRandomNumber() {
const randomNumber = Math.floor((Math.random() * 3) + 1);
const question = this.question.question;
this.$emit('get-random-number', question[randomNumber]);
return question[randomNumber];
}
}
};
// parent component
<template>
<div id="app">
<vue-form
:data="formData"
#complete="complete"
#getRandomNumber="newRandomNumber"
></vue-form>
</div>
</template>
<script>
import VueForm from "#/components/VueForm";
import data from "#/data/demo";
export default {
data() {
return {
formData: data
}
},
components: {
VueForm
},
created() {
this.complete()
},
methods: {
complete(data) {
// Send to database here
// localStorage.setItem('questions', data.map(d => d.question[this.randomNumber] + ': ' + d.answer));
},
}
};
</script>
v-on:get-random-number (or the superior short-hand syntax: #get-random-number). Just like you'd listen to any other event, such as #click or #mouseenter.
Though I don't know off the top of my head if dashes are valid in event names. Might have to camelcase it.

How to change vue.js components data outside scope

I want to change vue.js data outside the default export statement. Given the example below, how would I go about doing that?
<template>
<div>
<h6 class="font-weight-normal mb-3">{{ name }}</h6>
</div>
</template>
<script>
export default {
data() {
return {
name: ""
}
}
}
let changeName = (name) => {
//How do I change the name data property here
}
</script>
If you assign the component to a variable/constant, you should be able to simply trigger the proxy setter of the data object or with component-level methods.
const component = new Vue({
data() {
return {
name: "Initial value."
}
},
methods: {
changeName(newName) {
this.name = newName;
}
}
});
// Mount it to an element (for demo purposes)
component.$mount('#app');
document.getElementById('btn-setter').onclick = function() {
component.name = 'Changed with SETTER';
};
document.getElementById('btn-method').onclick = function() {
component.changeName('Changed with METHOD');
};
// Uncomment this to start exporting it.
// export default component;
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h6 class="font-weight-normal mb-3">{{ name }}</h6>
<button id="btn-setter">Change with setter</button>
<button id="btn-method">Change with method</button>
</div>
You can write any function you want in the page outside of the component (or export statement) but you would need to invoke it in your methods section or somewhere in the component. I use this for functions that create default values, instead of importing them from outside just write a function initVal = () => someVal then in the data or computed or somewhere reference initVal (no this).