Why does this event actually trigger? - vue.js

Im in a vue project using routing, its a tutorial: https://www.youtube.com/watch?v=Wy9q22isx3U
The repo with the full code is here:
https://github.com/bradtraversy/vue_crash_todolist
My Home.vue looks like this:
<template>
<div id="app">
<AddTodo v-on:add-todo="addTodo"/>
<Todos v-bind:todos="todos" v-on:del-todo="deleteTodo"/>
</div>
</template>
<script>
import AddTodo from '../components/AddTodo'
import Todos from '../components/Todos'
export default {
name: 'home',
components: {
Todos,
AddTodo
},
data() {
return {
todos: [
{
id: 1,
title: "Todo one",
completed: false
},
{
id: 2,
title: "Todo two",
completed: true
},
{
id: 3,
title: "Todo three",
completed: false
}
]
}
},
methods: {
deleteTodo(id){
this.todos = this.todos.filter(todo => todo.id != id)
},
addTodo(newTodo){
this.todos = [...this.todos, newTodo]
}
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
Whats important here is the Todos element in the markup.
Its a complex component which imported TodoItem itself.
Now Todos looks like this:
<template>
<div>
<div v-bind:key="todo.id" v-for="todo in todos">
<TodoItem v-bind:todo="todo" v-on:del-todo="$emit('del-todo', todo.id)"/>
</div>
</div>
</template>
<script>
import TodoItem from './TodoItem.vue'
export default {
name: "Todos",
components: {
TodoItem
},
props: ["todos"]
}
</script>
<style scoped>
</style>
And it imported TodoItem, which is here:
<template>
<div class="todo-item" v-bind:class="{'is-complete':todo.completed}">
<p>
<input type="checkbox" v-on:change="markComplete">
{{todo.title}}
<button #click="$emit('del-todo', todo.id)"class="del">x</button>
</p>
</div>
</template>
<script>
export default {
name: "TodoItem",
props:["todo"],
methods: {
markComplete(){
this.todo.completed = !this.todo.completed
}
}
}
</script>
<style scoped>
.todo-item {
background: #f4f4f4;
padding: 10px;
border-bottom: 1px #ccc dotted;
}
.is-complete {
text-decoration: line-through;
}
.del {
background: #ff0000;
color: #fff;
border: none;
padding: 5px 9px;
border-radius: 50%;
cursor: pointer;
float: right;
}
</style>
Now what confuses me is the syntax surrounding the emitted events.
In TodoItem, I have this emitted event from the button:
<button #click="$emit('del-todo', todo.id)"class="del">x</button>
Now this is completely understandable for me because we have the event trigger specified with "#click".
This is then exported to parent, Todos.vue, and there we can see this:
<TodoItem v-bind:todo="todo" v-on:del-todo="$emit('del-todo', todo.id)"/>
Here Im starting to get confused.
Again, in the long syntax, a event trigger is defined:
v-on:del-todo
But del-todo is not an event trigger. Its neither click, nor change, nor input.
So how can this code even work? What does vue.js imply when it encounters code like above?
My confusion then gets even worse in Home.vue
<Todos v-bind:todos="todos" v-on:del-todo="deleteTodo"/>
For the third time, an event trigger is specified.
And for the second time, this event trigger doesn't specify a "native" trigger like click.
Now I already wrapped my head around this and I could at least beat SOME sense into it.
In Todos.vue and Home.vue, the specified events seem to execute when del-todo has fired. So they are like callbacks, they take the return value of del-todo.
In Todos.vue, triggering del-todo emits del-todo to its parent, Home.vue.
Is that correct?
Home.vue then triggers deleteTodo when del-todo is fired.
However, deleteTodo requires an id to be handed over through the parameter, but interestingly, <Todos v-bind:todos="todos" v-on:del-todo="deleteTodo"/> doesnt.
Still, the function works. So how does id ever arrive in deleteTodo?
A similar problem arises in TodoItem.vue. Here, del-todo is called, but actually we haven't any sort of declaration of this function anywhere in the script inside TodoItem.vue. So again, what does vueJS imply when it encounters a situation where a function is emitted/called which wasn't defined anywhere?

In Vue.js there are not only the events click change and input, it also allows you to define custom events. All you have to do ist throw an event in the child-component with $emit('my-custom-event', param1, param2) and catch it in the direct parent-component with v-on:my-custom-event="handler" (you can also write #my-custom-event, thats synonym). The handler is a function that takes the parameters passed when emitting the event. Your handler in the Todos-component catches the event del-todo and throws a new event with the same name. The Home-component catches that event and has its function deleteTodo defined as its handler, so this function is being called (the id that was passed with the event is the parameter for deleteTodo).

What is happening is that each time you click there is an event AND value emitted to the parent component, the parent component has a listener v-on:del-todo means it is listening on the del-todo event, once it is triggered/handled it emits it again one level up until it reaches the component where you want to actually manipulate the data (delete the item based on id).
Note: the value is implicitly passed into the handler function deleteTodo so even though it is not explicitly there (i.e. deleteTodo($event) it is there.

Related

Super small Vue button

I'm learning Vue, and even with the simplest examples there is something wrong. For example, buttons. I have a defined component, myButton, responds to clicks, but it doesn't look like it should, is super small and dont have any label. What am I doing wrong?
Part of index.js:
Vue.component('mybutton', {
props: {
buttonLabel: String,
},
template: '<button #click="onClick()" class="btn">{{ buttonLabel }}</button>',
methods: {
onClick(){
console.log('Click');
}
},
})
Part of index.html:
<div id="app">
<mybutton text="From Vue"></mybutton>
<button class="btn">Test</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="index.js"></script>
And CSS:
.btn {
display: inline-block;
background: #000;
color: #fff;
border: none;
padding: 10px,20px;
border-radius: 5px;
text-decoration: none;
font-size: 15px;
font-family: inherit;
}
Your prop is called buttonLabel, while you pass a property called text inside your index.html. Therefore, the button doesn't get any text and then it's rendered without any inner content (and therefore slim, since you didn't give it fixed width and height).
You need to change the part of index.html and replace text with button-label (Vue automatically maps buttonLabel to it, and it is the better option. Using buttonLabel might not work in this case, since you are not using single file components.
Call it like
<mybutton mylabel="hI"></mybutton>
Vue.component('mybutton', {
props: ['mylabel'],
template: '<button>{{ mylabel }}</button>'
})
https://codepen.io/flakerimi/pen/wvgGqVb
https://v2.vuejs.org/v2/guide/components.html

Displaying multiple instances of a component in Vue.js

I'm completely new to vue.js and as part of my learning I'm just trying to create a button, that when each time this button is pressed, an annotation (a draggable text box where users can input their text) will be created. This is what I currently have :
<template>
<div id="app">
<button v-on:click="addAnnotation()">Add annotation</button>
<div v-for="(annotation, index) in annotations" :key="index">
<component :is="annotation">
</component>
</div>
</div>
</template>
<script>
import DraggableAnnotation from "./components/DraggableAnnotation.vue";
export default {
name: "App",
components: {
DraggableAnnotation,
},
data: function () {
return {
annotations: [],
};
},
methods: {
addAnnotation: function () {
this.annotations.push({ DraggableAnnotation });
},
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
I'm not very sure if my implementation is correct. Each time the button is pressed, I want to create an annotation, and add it to the list of annotations. Then, I'm trying to render the list of annotations using v-for to display all the annotations to the user, but I'm not sure what to pass in as the key as I don't have any props for the DraggableAnnotation component.
When I press my addAnnotation button, I get this error in the console log.
vue.runtime.esm.js?2b0e:619 [Vue warn]: Failed to mount component: template or render function not defined.
found in
---> <Anonymous>
<App>
<Root>
Edit : defined index in here
<div v-for="(annotation, index) in annotations" :key="index">
I think it error cuz index not definded
<div v-for="annotation in annotations" :key="index">
instead
<div v-for="(annotation, index) in annotations" :key="index">

Can't get the value of Date Range Picker (Vuejs)

I am a newbie in VueJS. I want to get the value of the date range that is selected and console.log it when the user clicks the button. However, whenever I click the button, the value printing in console is null. Kindly Help.
This is the code:
App.vue
<template>
<div id="app">
<VueRangedatePicker v-model="datepicker"></VueRangedatePicker>
<button class="button" #click="showdata()" value="Test">Normal</button>
</div>
</template>
<script>
import VueRangedatePicker from "vue-rangedate-picker";
export default {
name: "App",
components: {
VueRangedatePicker
},
data() {
return {
datepicker: null
};
},
methods: {
showdata() {
console.log("DATE PICKER", this.datepicker);
}
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
Code can be accessed here.
For v-model to work, a component needs to have a prop called value and emit an input event.
VueRangedatePicker doesn't have that prop or event. Instead, it emits a selected event when it updates. You can listen for it using #selected. For example:
<VueRangedatePicker #selected="updateDatePicker"></VueRangedatePicker>
methods: {
showdata() {
console.log("DATE PICKER", this.datepicker);
console.log("start: " + this.datepicker.start);
console.log("end: " + this.datepicker.end);
},
updateDatePicker(value) {
console.log("updating datepicker value");
this.datepicker = value;
}
See updated code here.

Object value is changed but component is not updated

I am using Vue (2.0) in my project. WorkingArea component get a object via props. Words in the object are rendered by 'vfor' in WorkingArea component and they are create a sentence. I add external field named "status" the object in before component mounted. Object status can be active or inactive. I think that when status is active, color of word is changed red. Although the object is updated, component did not triggered for rendering. I'm sharing below WorkingArea component:
<template>
<div id='sentence' class="drtl mt-3">
<p :class="word.status == 'active' ? active : inactive" v-for="(word, index) in hadithObject.hadith_words" :key="index" :id='index'>
{{ word.kelime}}
</p>
</div>
<b-button variant="danger" #click="nextWord()" >next</b-button>
</template>
<script>
export default {
props: {
hid:String,
ho: Object,
},
data() {
return {
hadithObject: null,
cursor: 0,
//css class binding.
inactive: 'inactive',
active: 'active',
}
},
beforeMount () {
this.hadithObject = this.ho;
this.hadithObject.hadith_words.forEach(item => {
item.status = this.inactive;
});
},
nextWord(){
// when click to button, status of word is set active.
this.hadithObject.hadith_words[this.cursor].status = this.active;
this.cursor += 1;
}
</script>
<style lang="scss" scoped>
#import url('https://fonts.googleapis.com/css?family=Amiri&display=swap');
.inactive{
font-family: 'Amiri', serif;
font-size: 23px;
line-height: 2.0;
display: inline-block;
color: black;
}
.drtl{
direction: rtl;
}
.active{
color: red;
font-family: 'Amiri', serif;
font-size: 23px;
line-height: 2.0;
display: inline-block;
}
</style>
-------UPDATED FOR SOLUTION--------
After #Radu Diță answers, I examine shared this link. I learned that Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
My mistake is trying first item. "newtWord" method is updated like below:
nextWord(){
var newItem = this.hadithObject.hadith_words[this.cursor];
newItem.status = this.active;
this.$set(this.hadithObject.hadith_words,this.cursor,newItem);
this.cursor += 1;
}
You are updating hadithObject's keys. They are not reactive as they aren't added from the beginning.
Look over the caveats regarding reactivity.
You have 2 options:
either assign the object again this.hadithObject = Object.assign({}, ...this.hadithObject)
use Vue.set to set the new keys on the object.

Append child to $slot.default

I have a component that I need display some custom modal on screen. I don't know where I should put this dialog content, so I did something like that:
<template>
<div class="ComponentItself">
<div v-show="false" ref="ModalContent">
Hello!
</div>
<button v-on:click="showModal">Show modal</button>
</div>
</template>
[...]
Note: I could not set the tag name of [ref=ModalContent] to template because the vue reserves this tag to another feature.
My idea is when I click on "show modal" it open creates an instance of another component (v-dialog) that I have created with the [ref=ModalContent] content (it should be compiled to support nested vue components).
import Dialog from './Dialog';
const DialogCtor = Vue.extend(Dialog);
const dialog = new DialogCtor({ propsData: {...} });
dialog['$slots'].default = [ this.$refs['templateNewFolder'].innerHTML ];
{something like document.body.appendChild(dialog.$el)}
This another component have a slot that could receives the HTML content to be displayed inside of that. And it just not works. The modal is displayed, but the slot content is undefined or the HTML content not parsed.
<div class="Dialog">
[...]
<slot></slot>
[...]
</div>
The current result is something like:
What I need:
I need to know if I am on the right way. I have about the component feature, but I could not identify or understand if it is/could resolve my problem;
What I could do to make it work;
Some similar project could help it, but I could not found anyone;
Maybe I could resolve my problem if is possible I just .appendChild() directly to $slot.default, but it is not possible;
It seems to me this might be a case of an XY problem.
What probably happens is that you do not need to manually fill $slot.default, but use your Dialog component a more standard way. Since there is little detail about the latter in your question, that component might also need some refactoring to fit this "standard way".
So a more standard approach would be to directly use your <custom-dialog> component in the template of your parent, instead of using a placeholder (the one you reference as ModalContent) that you have to hide. That way, whatever HTML you pass within that <custom-dialog> will be fed into your Dialog's <slot> (designed beaviour of slot).
That way you also save the hassle of having to manually instantiate your Dialog component.
Then you can toggle your <custom-dialog> visibility (with v-if or v-show) or even manipulate its position in the DOM as you mention in your code; you can access its DOM node as $el: this.$refs.ModalContent.$el when ModalContent is a Vue instance.
You could also factorize the showModal method by delegating it to the Dialog component.
Code example:
Vue.component('modal-dialog', {
template: '#modal-dialog',
data() {
return {
modalShown: false,
};
},
methods: {
showModal() {
this.modalShown = true;
},
hideModal() {
this.modalShown = false;
},
},
});
new Vue({
el: '#app',
methods: {
showModal() {
this.$refs.ModalContent.showModal();
},
},
});
/*
https://sabe.io/tutorials/how-to-create-modal-popup-box
MIT License https://sabe.io/terms#Licensing
*/
.modal {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0;
visibility: hidden;
transform: scale(1.1);
transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 1rem 1.5rem;
width: 24rem;
border-radius: 0.5rem;
}
.close-button {
float: right;
width: 1.5rem;
line-height: 1.5rem;
text-align: center;
cursor: pointer;
border-radius: 0.25rem;
background-color: lightgray;
}
.close-button:hover {
background-color: darkgray;
}
.show-modal {
opacity: 1;
visibility: visible;
transform: scale(1.0);
transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;
}
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<modal-dialog ref="ModalContent">
Hello!
</modal-dialog>
<h1>Hello World</h1>
<button v-on:click="showModal">Show modal</button>
</div>
<template id="modal-dialog">
<div class="modal" :class="{'show-modal': modalShown}" #click="hideModal">
<div class="modal-content">
<span class="close-button" ref="closeButton" #click="hideModal">×</span>
<slot></slot>
</div>
</div>
</template>
Now if you really want to fiddle with $slot, #Sphinx's linked answer in the question comments is an acceptable approach. Note that the accepted answer there also favours the standard usage. It seems to me this is also what #Sphinx implies in their 2nd comment.