How to test if a component emits an event in Vue? - javascript

I have two components. Child component emits an 'input' event when it's value changed and parent component takes this value with v-model. I'm testing ChildComponent. I need to write a test with Vue-test-utils to verify it works.
ParentComponent.vue:
<template>
<div>
<child-component v-model="search"></child-component>
<other-component></other-component>
...
</div>
</template>
ChildComponent.vue:
<template>
<input :value="value" #change="notifyChange($event.target.value)"></input>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
#Component
export default class ChildComponent extends Vue {
#Prop({ default: '' }) readonly value!: string
notifyChange(value: string) {
this.$emit('input', value)
}
}
</script>
child-component.spec.ts:
describe('ChildComponent', () => {
let wrapper: any
before(() => {
wrapper = VueTestUtils.shallowMount(ChildComponent, {})
})
it(`Should emit 'input' event when value change`, () => {
const rootWrapper = VueTestUtils.shallowMount(ParentComponent)
wrapper.vm.value = 'Value'
wrapper.findAll('input').at(0).trigger('change')
assert.isTrue(!!rootWrapper.vm.search)
})
})
I didn't write the exact same code but the logic is like this.
My components work properly. 'child-component.spec.ts' doesn't work. I need to write a test for it.

TESTED.
This is a simple way of testing an emit, if someone is looking for this.
in your test describe write this.
describe('Close Button method', () => {
it('emits return false if button is clicked', (done) => {
wrapper.find('button').trigger('click')
wrapper.vm.$nextTick(() => {
wrapper.vm.closeModal() //closeModal is my method
expect(wrapper.emitted().input[0]).toEqual([false]) //test if it changes
done()
})
})
})
my vue comp
<div v-if="closeButton == true">
<button
#click="closeModal()"
>
...
</button>
</div>
my props in vue comp
props: {
value: {
type: Boolean,
default: false
},
my methods in vue comp
methods: {
closeModal() {
this.$emit('input', !this.value)
}
}

Here's an example that will help you:
Child Component.vue
<template>
<div>
<h2>{{ numbers }}</h2>
<input v-model="number" type="number" />
<button #click="$emit('number-added', Number(number))">
Add new number
</button>
</div>
</template>
<script>
export default {
name: "ChildComponent",
props: {
numbers: Array
},
data() {
return {
number: 0
};
}
};
</script>
Parent Component.vue
<template>
<div>
<ChildComponent
:numbers="numbers"
#number-added="numbers.push($event)"
/>
</div>
</template>
<script>
import ChildComponent from "./ChildComponent";
export default {
name: "ParentComponent",
data() {
return {
numbers: [1, 2, 3]
};
},
components: {
ChildComponent
}
};
</script>
Follow this article: https://medium.com/fullstackio/managing-state-in-vue-js-23a0352b1c87
I hope this will help you.

in your parent component listen to the emitted event "input"
<template>
<div>
<child-component #input="get_input_value" v-model="search"></child-component>
<other-component></other-component>
...
</div>
</template>
and in your script add method get_input_value()
<script>
...
methods:
get_input_value(value){
console.log(value)
}
</script>

Related

Vue not reacting to a computed props change

I am using the Vue composition API in one of my components and am having some trouble getting a component to show the correct rendered value from a computed prop change. It seems that if I feed the prop directly into the components render it reacts as it should but when I pass it through a computed property it does not.
I am not sure why this is as I would have expected it to be reactive in the computed property too?
Here is my code:
App.vue
<template>
<div id="app">
<Tester :testNumber="testNumber" />
</div>
</template>
<script>
import Tester from "./components/Tester";
export default {
name: "App",
components: {
Tester,
},
data() {
return {
testNumber: 1,
};
},
mounted() {
setTimeout(() => {
this.testNumber = 2;
}, 2000);
},
};
</script>
Tester.vue
<template>
<div>
<p>Here is the number straight from the props: {{ testNumber }}</p>
<p>
Here is the number when it goes through computed (does not update):
{{ testNumberComputed }}
</p>
</div>
</template>
<script>
import { computed } from "#vue/composition-api";
export default {
props: {
testNumber: {
type: Number,
required: true,
},
},
setup({ testNumber }) {
return {
testNumberComputed: computed(() => {
return testNumber;
}),
};
},
};
</script>
Here is a working codesandbox:
https://codesandbox.io/s/vue-composition-api-example-forked-l4xpo?file=/src/components/Tester.vue
I know I could use a watcher but I would like to avoid that if I can as it's cleaner the current way I have it
Don't destruct the prop in order to keep its reactivity setup({ testNumber }) :
setup(props) {
return {
testNumberComputed: computed(() => {
return props.testNumber;
}),
};
}

Do custom events propagate up the component chain in Vue 3?

Custom events didn't propagate in Vue 2. Is there a change in Vue 3 because, as the following example shows, it looks like custom events bubble up the component chain:
const Comp1 = {
template: `
<button #click="this.$emit('my-event')">click me</button>
`
}
const Comp2 = {
components: {
Comp1
},
template: `
<Comp1/>
`
}
const HelloVueApp = {
components: {
Comp2
},
methods: {
log() {
console.log("event handled");
}
}
}
Vue.createApp(HelloVueApp).mount('#hello-vue')
<script src="https://unpkg.com/vue#next"></script>
<div id="hello-vue" class="demo">
<Comp2 #my-event="log"/>
</div>
Yes, by default they now fall through in VUE v3
You need to define inheritAttrs: false to prevent that
link to docs, though it doesn't seem to indicate that it affects the events too. It just mentions attributes, but events are part of attributes($attrs).
const Comp1 = {
template: `
<button #click="this.$emit('my-event')">click me</button>
`
}
const Comp2 = {
components: {
Comp1
},
template: `
<Comp1/>
`,
inheritAttrs: false
}
const HelloVueApp = {
components: {
Comp2
},
methods: {
log() {
console.log("event handled APP");
}
}
}
Vue.createApp(HelloVueApp).mount('#hello-vue')
<script src="https://unpkg.com/vue#next"></script>
<div id="hello-vue" class="demo">
<Comp2 #my-event="log"/>
</div>
You have to handle the event into Comp2 :
<Comp1 #my-event="this.$emit('my-event')"/>

How to store a value from another component's data?

I have two components, is there a way to store value from another component's data?
Here is Create.vue
<template>
<div id="main">
<Editor />
//some codes here
</div>
</template>
<script>
import Editor from './_Create_Editor.vue'
export default {
components: { Editor },
data: () => ({
text: ''
}),
}
</script>
And here is the _Create_Editor.vue.
<template>
//sample input for demonstration purposes
<input type="text" class="form-control" v-model="text"/>
</template>
The code above returns an error:
Property or method "text" is not defined on the instance but referenced during render
I want everytime I type the data: text from Create.vue has the value of it.
How can I possibly make this? Please help.
You can do this by using $emit.
Create.vue
<template>
<div id="main">
<Editor
#edit={onChangeText}
/>
//some codes here
</div>
</template>
<script>
import Editor from './_Create_Editor.vue'
export default {
components: { Editor },
data: () => ({
text: ''
}),
methods: {
onChangeText: function (value) {
this.text = value
}
}
}
</script>
_Create_Editor.vue
<template>
//sample input for demonstration purposes
<input
type="text"
class="form-control"
#change="onChange"
/>
</template>
<script>
export default {
methods: {
onChange: function (event) {
this.$emit('edit', event.target.value)
}
}
}
</script>

Vue async component callback

<template>
<div>
<AsyncComponent1></AsyncComponent1>
<!-- Render AsyncComponent2 after AsyncComponent1 -->
<AsyncComponent2></AsyncComponent2>
</div>
</template>
<script>
export default {
name: "My Component"
components: {
AsyncComponent1: () => import("./AsyncComponent1"),
AsyncComponent2: () => import("./AsyncComponent2")
}
};
</script>
I'm loading two components asynchronously within a component but I need one of the components to render after the other. I wonder if thats possible?
Add a v-if to a ref in the other component.
<template>
<div>
<AsyncComponent1 ref="c1"></AsyncComponent1>
<!-- Render AsyncComponent2 after AsyncComponent1 -->
<AsyncComponent2 v-if="$refs.c1"></AsyncComponent2>
</div>
</template>
You could have the first component emit an event, that is listened to by the parent and used to toggle the second component
<template>
<div>
<AsyncComponent1 v-on:loaded="componentLoaded"></AsyncComponent1>
<!-- Render AsyncComponent2 after AsyncComponent1 -->
<AsyncComponent2 v-if="hasComponent"></AsyncComponent2>
</div>
</template>
<script>
export default {
name: "My Component",
components: {
AsyncComponent1: () => import("./AsyncComponent1"),
AsyncComponent2: () => import("./AsyncComponent2")
},
data: {
hasComponent: false
},
methods: {
componentLoaded() {
this.hasComponent = true;
}
}
};
</script>
And then in AsyncComponent1.vue:
...
mounted() {
this.$emit("loaded");
}
...

How do you target a button, which is in another component, with a method in App.vue?

I'm making a basic To Do app, where I have an input field and upon entering a task and pressing the "Enter" key the task appears in the list. Along with the task the TodoCard.vue component also generates a button, which I would like to use to delete the task.
I've added a #click="removeTodo" method to the button, but don't know where to place it, in the TodoCard component or in the App.vue file?
TodoCard component:
<template>
<div id="todo">
<h3>{{ todo.text }}</h3>
<button #click="removeTodo(todo)">Delete</button>
</div>
</template>
<script>
export default {
props: ['todo'],
methods: {
removeTodo: function (todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
}
}
}
</script>
App.vue:
<template>
<div id="app">
<input class="new-todo"
placeholder="Enter a task and press enter."
v-model="newTodo"
#keyup.enter="addTodo">
<TodoCard v-for="(todo, key) in todos" :todo="todo" :key="key" />
</div>
</template>
<script>
import TodoCard from './components/TodoCard'
export default {
data () {
return {
todos: [],
newTodo: ''
}
},
components: {
TodoCard
},
methods: {
addTodo: function () {
// Store the input value in a variable
let inputValue = this.newTodo && this.newTodo.trim()
// Check to see if inputed value was entered
if (!inputValue) {
return
}
// Add the new task to the todos array
this.todos.push(
{
text: inputValue,
done: false
}
)
// Set input field to empty
this.newTodo = ''
}
}
}
</script>
Also is the code for deleting a task even correct?
You can send an event to your parent notifying the parent that the delete button is clicked in your child component.
You can check more of this in Vue's documentation.
And here's how your components should look like:
TodoCard.vue:
<template>
<div id="todo">
<h3>{{ todo.text }}</h3>
<button #click="removeTodo">Delete</button>
</div>
</template>
<script>
export default {
props: ['todo'],
methods: {
removeTodo: function (todo) {
this.$emit('remove')
}
}
}
</script>
App.vue:
<template>
<div id="app">
<input class="new-todo"
placeholder="Enter a task and press enter."
v-model="newTodo"
#keyup.enter="addTodo">
<TodoCard v-for="(todo, key) in todos" :todo="todo" :key="key" #remove="removeTodo(key)" />
</div>
</template>
<script>
import TodoCard from './components/TodoCard'
export default {
data () {
return {
todos: [],
newTodo: ''
}
},
components: {
TodoCard
},
methods: {
addTodo: function () {
// Store the input value in a variable
let inputValue = this.newTodo && this.newTodo.trim()
// Check to see if inputed value was entered
if (!inputValue) {
return
}
// Add the new task to the todos array
this.todos.push(
{
text: inputValue,
done: false
}
)
// Set input field to empty
this.newTodo = ''
}
},
removeTodo: function(key) {
this.todos.splice(key, 1);
}
}
</script>

Categories

Resources