I have a form that contains a selector reusable component like this
<template>
<div class="channelDetail" data-test="channelDetail">
<div class="row">
<BaseTypography class="label">{{ t('channel.detail.service') }}</BaseTypography>
<BaseSelector
v-model="serviceId"
data-test="serviceInput"
class="content"
:option="servicePicker.data?.data"
:class="serviceIdErrorMessage && 'input-error'"
/>
</div>
<div class="row">
<BaseTypography class="label">{{ t('channel.detail.title') }}</BaseTypography>
<BaseInput v-model="title" data-test="titleInput" class="content" :class="titleErrorMessage && 'input-error'" />
</div>
</div>
</template>
I'm going to test this form by using vue-test-utils and vitest.
I need to set option props from the script to the selector.
In my thought, this should be worked but not
it('test', async () => {
const wrapper=mount(MyForm,{})
wrapper.findComponent(BaseSelector).setProps({option:[...some options]})
---or
wrapper.find('[data-test="serviceInput"]').setProps({option:[...some options]})
---or ???
});
Could anyone help me to set the props into components in the mounted wrapper component?
The answer is that you should not do that. Because BaseSelector should have it's own tests in which you should test behavior changes through the setProps.
But if you can't do this for some reason, here what you can do:
Check the props passed to BaseSelector. They always depend on some reactive data (props, data, or computed)
Change those data in MyForm instead.
For example
// MyForm.vue
data() {
return {
servicePicker: {data: null}
}
}
// test.js
wrapper = mount(MyForm)
wrapper.setData({servicePicker: {data: [...some data]})
expect(wrapper.findComponent(BaseSelector)).toDoSomething()
But I suggest you to cover the behavior of BaseSelector in separate test by changing it's props or data. And then in the MyForm's test you should just check the passed props to BaseSelector
expect(wrapper.findComponent(BaseSelector).props('options')).toEqual(expected)
Related
I have the vue component with $emit into component and let it return the data from the component. I will use the component to update current page's data. the codes below
Template:
<Testing
#update="update">
</Testing>
<AnotherComponent
:data="text"
>
</AnotherComponent>
Script:
method(){
update: function(data){
this.text = data.text
}
}
it work perfectly if only this one.
Now , i need to make a button to add one more component.
I use the for loop to perform this.
Template
<div v-for="index in this.list">
<Testing
:name="index"
#update="update">
</Testing>
<AnotherComponent
:data="text"
>
</AnotherComponent>
</div>
Script:
method(){
addList : function(){
this.list +=1;
},
deleteList : function(){
this.list -=1;
},
update: function(data){
this.text = data.text
}
}
The add and delete function run perfectly.
However , they share the "update" method and the "text" data.
so , If I change the second component , the first component will also changed.
I think this is not the good idea to copy the component.
Here are my requirements.
This component is the part of the form, so they should have different name for submit the form.
The another component" will use the data from the "testing component" to do something. the "testing" and "another component" should be grouped and the will not change any data of another group.
Any one can give me the suggestion how to improve these code? Thanks
What happends is that both are using the data form the parent, and updating that same data.
It seems that you are making some kind of custom inputs. In that case in your child component you can use 'value' prop, and 'input' event, and in the parent user v-model to keep track of that especific data data.
Child component BaseInput.vue:
<template>
<div>
<input type="text" :value="value" #keyup="inputChanged">
</div>
</template>
<script>
export default {
props: ['value'],
data () {
return {
}
},
methods: {
inputChanged (e) {
this.$emit('input', e.target.value)
}
}
}
</script>
And this is the code on the parent:
<template>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3">
<base-input v-model="firstInputData"></base-input>
<p>{{ firstInputData }}</p>
<hr>
<base-input v-model="secondInputData"></base-input>
<p>{{ secondInputData }}</p>
</div>
</div>
</div>
<script>
import BaseInput from './BaseInput.vue'
export default {
components: {BaseInput},
data() {
return{
firstInputData: 'You can even prepopulate your custom inputs',
secondInputData: ''
}
}
}
</script>
In the parent you could really store the diferent models in an object as properties, and pass that object to that "The another component" , pass them as individual props... pass an array ....
I have a vue component with
<form #keydown="console.error($event.target.name);">
gives
app.js:47961 [Vue warn]: Property or method "console" is not defined
on the instance but referenced during render.
window.console doesn't work either
What is the proper way to use console and window in a template to debug?
Simplest way of providing global objects to the template is to place them in computed, like this:
console: () => console. Same goes for window,
computed: {
console: () => console,
window: () => window,
}
See it here.
If you want to run it inline instead of using a method, just add this to the form:
Codepen: https://codepen.io/x84733/pen/PaxKLQ?editors=1011
<form action="/" #keydown="this.console.log($event.target.name)">
First: <input type="text" name="fname"><br>
Second: <input type="text" name="fname2"><br>
</form>
But it'd be better to use a method instead of running functions inline, so you have more control over it:
<!-- Don't forget to remove the parenthesis -->
<form action="/" #keydown="debug">
First: <input type="text" name="fname"><br>
Second: <input type="text" name="fname2"><br>
</form>
...
methods: {
debug (event) {
console.log(event.target.name)
}
}
You can use $el.ownerDocument.defaultView.console.log() inside your template
Pro: Doesn't require any component changes
Con: Ugly
Also if you want to access console from {{ }} you can use global mixin:
Vue.mixin({
computed: {
console: () => console
}
})
If using Vue 3, do:
const app = createApp(App)
app.config.globalProperties.$log = console.log
If using Vue 2, do:
Vue.prototype.$log = console.log
Use $log inside template:
<h1>{{ $log(message) }}</h1>
To not interfere with the rendering, use $log with ?? (or || if using Vue 2, since ?? is not supported in the template):
<h1>{{ $log(message) ?? message }}</h1>
You can use this.console instead console or wrap call to console in a method, i am using eslint config with rule 'no-console': 'off'
You can use computed property or methods for this case.
If you need to code it as javascript in the Vue template. you have to define console in the data.
Please check the code below.
data(){
return {
selected :"a",
log : console.log
}
}
<span>{{log(selected)}}</span>
This will make functionality of console.log available, while resolving the template.
I'd make a getter for console template variable:
get console() { return window.console; }
For Vue 3, SFC Composition API, you have to define a function and call console or alert inside that function
<script setup>
import Child from "./Child.vue";
function notify(message) {
alert(message);
}
</script>
<template>
<Child #some-event="notify('child clicked')" />
</template>
I have a list of items that don't get created until after an async call happens. I need to be able to get the getBoundingClientRect() of the first (or any) of the created items.
Take this code for instance:
<template>
<div v-if="loaded">
<div ref="myItems">
<div v-for="item in items">
<div>{{ item.name }}</div>
</div>
</div>
</div>
<div v-else>
Loading...
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
items: []
}
},
created() {
axios.get('/url/with/some/data.json').then((response) => {
this.items = response.data;
this.loaded = true;
}, (error) => {
console.log('unable to load items');
});
},
mounted() {
// $refs is empty here
console.log(this.$refs);
// this.$refs.myItems is undefined
}
};
</script>
So, I'm trying to access the myItems ref in the mounted() method, but the this.$refs is empty {} at this point. So, therefore, I tried using a component watch, and various other methods to determine when I can read the ref value, but have been unsuccessful.
Anyone able to lead me in the right direction?
As always, thanks again!!
UPDATES
Added a this.$watch in the mounted() method and the $refs still come back as {}. I then added the updated() method to the code, then was able to access $refs there and it seemed to work. But, I don't know if this is the correct solution?
How does vuejs normally handle something like dynamically moving a div to an on-screen position based on async data? This is similar to what I'm trying to do, grab an element on screen once it has been rendered first (if it even should be rendered at all based on the async data), then access it to do something with it (move it to a position)?
Instead of doing on this.$refs.myItems during mounted, you can do it after the axios promise returns the the response.
you also update items and loaded, sou if you want to use watch, you can use those
A little late, maybe it helps someone.
The problem is, you're using v-if, which means the element with ref="myItems" doesn't exist yet. In your code this only happens when Axios resolves i.e. this.loaded.
A better approach would be to use a v-show.
<template>
<div>
<div v-show="loaded">
<div ref="myItems">
<div v-if="loaded">
<div v-for="item in items">
<div>{{ item.name }}</div>
</div>
</div>
</div>
</div>
<div v-show="!loaded">
Loading...
</div>
</div>
</template>
The difference is that an element with v-show will always be rendered and remain in the DOM; v-show only toggles the display CSS property of the element.
https://v2.vuejs.org/v2/guide/conditional.html#v-show
I have just started using Vue and experienced some unexpected behavior. On passing props from a parent to child component, I was able to access the prop in the child's template, but not the child's script. However, when I used the v-if directive in the parents template (master div), I was able to access the prop in both the child script and child template. I would be grateful for some explanation here, is there a better was of structuring this code? See below code. Thanks.
Parent Component:
<template>
<div v-if="message">
<p>
{{ message.body }}
</p>
<answers :message="message" ></answers>
</div>
</template>
<script>
import Answers from './Answers';
export default {
components: {
answers: Answers
},
data(){
return {
message:""
}
},
created() {
axios.get('/message/'+this.$route.params.id)
.then(response => this.message = response.data.message);
}
}
</script>
Child Component
<template>
<div class="">
<h1>{{ message.id }}</h1> // works in both cases
<ul>
<li v-for="answer in answers" :key="answer.id">
<span>{{ answer.body }}</span>
</li>
</ul>
</div>
</template>
<script>
export default{
props:['message'],
data(){
return {
answers:[]
}
},
created(){
axios.get('/answers/'+this.message.id) //only worls with v-if in parent template wrapper
.then(response => this.answers = response.data.answers);
}
}
</script>
this.message.id only works with v-if because sometimes message is not an object.
The call that you are making in your parent component that retrieves the message object is asynchronous. That means the call is not finished before your child component loads. So when your child component loads, message="". That is not an object with an id property. When message="" and you try to execute this.message.id you get an error because there is no id property of string.
You could continue to use v-if, which is probably best, or prevent the ajax call in your child component from executing when message is not an object while moving it to updated.
I define a simple child component(testSlot.vue) like this:
<template>
<section>
<div>this is title</div>
<slot text="hello from child slot"></slot>
</section>
</template>
<script>
export default {}
</script>
and we can use it in html template like this
<test-slot>
<template scope="props">
<div> {{props.text}}</div>
<div> this is real body</div>
</template>
</test-slot>
but how can I use it in jsx ?
After read the doc three times , I can answer the question myself now O(∩_∩)O .
<test-slot scopedSlots={
{
default: function (props) {
return [<div>{props.text}</div>,<div>this is real body</div>]
}
}}>
</test-slot>
the slot name is default.
So. we can access the scope in the scopedSlots.default ( = vm.$scopedSlots.default)
the callback argument 'props' is the holder of props.
and the return value is vNode you cteated with scope which exposed by child component.
I realize the jsx is just a syntactic sugar of render function ,it still use createElement function to create vNode tree.
now in babel-plugin-transform-vue-jsx 3.5, you need write in this way:
<el-table-column
{ ...{
scopedSlots: {
default: scope => {
return (
<div class='action-list'>
</div>
)
}
}
} }>
</el-table-column>