Object value is changed but component is not updated - vue.js

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.

Related

In Vue SFC link click is not triggered first time

I am using Vue 3 to show a set of links for which I am assigning event handlers dynamically(based on link id).
The issues is: The first time when any link is clicked, the corresponding event is not triggered. But subsequently clicks are perfectly working.
The updated code is below:
<script setup>
const makeSizer = ([...sizes]) => {
sizes.map((size) =>{
console.log('size-' + size);
document.getElementById('size-' + size).style.display = "";
document.getElementById('size-' + size).onclick = ((e) =>{
e.preventDefault();
document.body.style.fontSize = e.target.text + 'px';
e.target.style.display = "none";
});
});
};
function zoomIt(){
return {
zoom: makeSizer([12,14,16,18])
}
}
</script>
<template>
<div class="greeting"> {{zoom}}
<p>Some paragraph text</p>
<h1>some heading 1 text</h1>
<h2>some heading 2 text</h2>
<div class="link">
12
</div>
<div class="link">
14
</div>
<div class="link">
16
</div>
<div class="link">
18
</div>
</div>
</template>
<style>
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
.link{
padding:5px; display:inline-table;
}
.greeting {
color: red;
font-weight: bold;
}
.greeting a{
border:2px solid blue;
padding:3px;
color:white;
background-color:blue;
}
#size-12{ font-size:12px;}
#size-14{ font-size:14px;}
#size-16{ font-size:16px;}
#size-18{ font-size:18px;}
</style>
The bad news is, the way you approach it is an anti-pattern in Vue. The good news is, with some small changes you will end up with code that is much more simple to read and maintain!
You are doubling your event listeners by calling onclick() inside makeSizer() and defining click events via #click.
However, let us not just fix the bug by altering the existing code. What we want to do is to get rid of the anti-patern. So instead, we try passing the desired value of 'zoom' to the handler directly and avoid the beforementioned duplications altogether.
// Script
// We define a function that adjusts zoom value using only the value that is being passed to it as an argument
setZoom(size) {*code*}
// Template
<button #click.prevent="setZoomTo(12)">
This is a general idea. I modified your code a bit more to make it more maintainable and added comments where changes were made. I hope this helps.
Script
<script setup>
import { ref } from "vue";
const currentZoom = ref(12); // Let us set default zoom to 12
const zoomOptions = [12, 14, 16, 18]; // We define zoom options as an array to dynamically generate buttons
function setZoomTo(size) {
currentZoom.value = size; // Set current zoom value
document.body.style.fontSize = currentZoom.value + "px"; // Adjust fontSize on body
}
</script>
Template
<div class="links">
<button // We use button tag for semantic correctness
v-for="zoom in zoomOptions" // For every value in zoomOptions a button is created
:key="zoom"
:disabled="zoom === currentZoom" // If zoom value represented by the button is also currentZoom value => add disabled attribute to the button
#click.prevent="setZoomTo(zoom)" // Adjust currentZoom value according to the zoom value represented by the button
>
{{ zoom }} // Button's zoom value
</button>
</div>
Style
.links {
display: flex;
gap: 16px;
}
.links button {
border: 2px solid blue;
padding: 3px;
color: white;
background-color: blue;
cursor: pointer;
}
.links button:disabled {
opacity: 0.7; // For better UX we change button's opacity instead of hiding it
}

How to create a copy of the slot with createElement?

How to access name of the component of the slot?
I want to create a copy of the component provided in the slot:
const child1 = slot
const child2 = h(???, slot.props)
So that child1 renders exactly as child2.
I need this, so that I can change properties of that VNode, for examples classes.
Context
import { h } from 'vue';
export default {
setup(props, { slots }) {
const children = [];
for (const slot of slots.default()) {
const child = h(???, slot.props)
children.push(h('div', [child]));
}
return () =>
h('div', children);
},
};
Background
I want to make a component similar to q-button-group:
I need 2 components TButton and TButtonGroup so that I can style TButton independently and create groups just by putting those buttons inside the TButtonGroup.
Example
<TButtonGroup>
<TButton label="Two" />
<TButton label="Three" />
</TButtonGroup>
TButton should have a different list of classes:
when it's inside TButtonGroup: px-4 py-2
when it's not: border rounded-lg px-4 py-2
See full html
Playground
https://stackblitz.com/edit/vue3-button-group-razbakov?file=src%2Fcomponents%2FGroupRender.js
Component name of vnode won't tell much, components are already resolved at this point. VNode's element or component is stored in type property.
The problem with your approach is that render function is an alternative to component template, not a way to access the entire DOM element hierarchy. There will be no TButton child elements like div in render function, only TButton vnode itself. It needs to be rendered in order to access its children.
If TButton were someone else's component which initial behaviour needs to be modified, this could be done by adding some directive to it and accessing component's children elements.
But since TButton is your own component that can be modified to your needs, the most straightforward way is to make it change classes depending on a prop and provide this prop when it's inside TGroup, i.e.:
const child = h(slot.type, {...slot.props, group: true}, slot.children);
children.push(child);
Use the component type you created:
const { h } = Vue
const TBtn = {
props: {
staticClass: {
type: Array,
default: () => [],
},
},
template: `
<div
class="t-btn px-4 py-2"
>
<slot></slot>
</div>
`
}
const TBtnGroup = {
setup(props, {
slots
}) {
const children = [...slots.default()]
.map(slot => h(slot.type, {
class: ['border', 'rounded-lg']
}, slot))
return () => h('div', {
class: ['d-flex']
}, children)
},
}
const App = {
template: `
<t-btn>OUTSIDE 1</t-btn>
<t-btn>OUTSIDE 2</t-btn>
<br />
<t-btn-group>
<t-btn>INSIDE 1</t-btn>
<t-btn>INSIDE 2</t-btn>
<t-btn>INSIDE 3</t-btn>
</t-btn-group>
`
}
const app = Vue.createApp(App)
app.component('TBtn', TBtn)
app.component('TBtnGroup', TBtnGroup)
app.mount('#app')
.px-4 {
padding-left: 16px;
padding-right: 16px;
}
.py-2 {
padding-top: 8px;
padding-bottom: 8px;
}
.border {
border: 1px solid black;
}
.rounded-lg {
border-radius: 8px;
}
.d-flex {
display: flex;
gap: 8px;
}
.t-btn:hover {
cursor: pointer;
background-color: black;
color: white;
}
<script src="https://unpkg.com/vue#next"></script>
<div id="app"></div>

Multiple custom checkbox components default checked state vuex problem

This is my custom checkbox component:
<template>
<label class="checkbox">
<input
type="checkbox"
:checked="isChecked"
#change="change"
>
<span />
<slot />
</label>
</template>
<script>
export default {
name: 'Checkbox',
model: {
prop: 'selectedValues',
event: 'change'
},
props: {
value: {
type: String,
required: true
},
selectedValues: {
type: Array,
default: null
},
checked: {
type: Boolean,
default: false
}
},
computed: {
isChecked() {
return this.selectedValues.includes(this.value);
}
},
created() {
if(this.checked) {
const selectedValues = this.selectedValues;
selectedValues.push(this.value);
this.$emit('change', selectedValues);
}
},
methods: {
change() {
const selectedValues = Array.from(this.selectedValues).slice();
const found = selectedValues.indexOf(this.value);
if (found !== -1) {
selectedValues.splice(found, 1);
} else {
selectedValues.push(this.value);
}
this.$emit('change', selectedValues);
}
}
};
</script>
<style lang="scss">
label.checkbox {
position: relative;
user-select: none;
display: inline-flex;
cursor: pointer;
input {
display: none;
&:checked ~ span {
background: #EEE;
&:after {
visibility: visible;
}
}
}
span {
width: 25px;
height: 25px;
border: 1px solid #EEE;
display: inline-block;
transition: all linear 0.3s;
margin-right: 5px;
&:after {
content: "";
position: absolute;
top: 3px;
left: 9px;
border-bottom: 3px solid #FFF;
border-right: 3px solid #FFF;
height: 13px;
width: 5px;
transform: rotate(45deg);
visibility: hidden;
}
}
}
</style>
Inside my form:
<Checkbox
v-model="selectedBrands"
value="bmw"
checked
>
BMW
</Checkbox>
<Checkbox
v-model="selectedBrands"
value="audi"
checked
>
Audi
</Checkbox>
<Checkbox
v-model="selectedBrands"
value="mazda"
>
Mazda
</Checkbox>
computed: {
selectedBrands: {
get() {
return this.$store.state.selectedBrands;
},
set(value) {
this.$store.commit('setSelectedBrands', {selectedBrands: value});
}
}
}
Vuex store:
export default new Vuex.Store(
{
strict: process.env.NODE_ENV !== 'production',
state: {
selectedBrands: []
},
mutations: {
setSelectedBrands(state, payload) {
state.selectedBrands = payload.selectedBrands;
},
}
});
This actually works, but I get vuex error: Error: [vuex] do not mutate vuex store state outside mutation handlers.
However I can change the created() hook in my checkbox component like this:
created() {
if(this.checked) {
const selectedValues = Array.from(this.selectedValues).slice();
selectedValues.push(this.value);
this.$emit('change', selectedValues);
}
}
The vuex error will go away, but only the last checkbox component (with checked prop) will be checked (in this example Audi).
My guess is this happens, because components are rendering asynchronically? Would be happy to hear correct explanation.
My goal is to create a custom checkbox component that supports multiple checkbox v-model array binding (using vuex!) + setting the initial checked state.
I've spent many hours trying to figure out the proper solution. Will be very thankful for your time and help! Thank you in advance!
Your approach will not work - you want the custom checkbox to modify its v-model based on its checked property. But all checkboxes will see the same (empty) selectedValues from the store at the time they emit the change event in created hook. So they will all emit arrays with a single value - their own value. The end result will be that only the last checkbox will become selected - since the computed setter in the parent will be called only after all checkboxes have been created.
If you want to get rid of the mutation error and your checkboxes still working - you should not rely on the checked prop to set their initial value but only rely on the v-model. Therefore, if you want to set them all checked initially - set the Vuex state in your parent component.

VueJS: Why parent components method unable to delete/destroy child's child (`vue2-dropzone`) component entirely?

I am creating a slider in vuejs and am using vue2-dropzone plugin for file uploads where each slide (slide-template.vue) has a vue2-dropzone component.
When app loads, image files are manually added in each vue2-dropzone (manuallyAddFile plugins API) queried from image API (hosted on heroku)
The issue is when I delete the first slide, calling the parent's (slider.vue) method removeSlideFn (passed down to child as prop) from child (slide-template.vue) component first slide is deleted but not entirely the dropzone images of the first slides are not destroyed and remains in the DOM, instead images of slide2, (the next slide) are deleted from the DOM (Pls try it once on codesandbox demo to actually know what I am mean). This does not happen when I delete slide2 or slide3 but only on slide1.
CodeSandBox Demo
App.vue
<template>
<div id="app">
<img width="15%" src="./assets/logo.png">
<slider />
</div>
</template>
<script>
import slider from "./components/slider";
export default {
name: "App",
components: {
slider
}
};
</script>
components\slider.vue (parent)
<template>
<div>
<hooper ref="carousel" :style="hooperStyle" :settings="hooperSettings">
<slide :key="idx" :index="idx" v-for="(slideItem, idx) in slideList">
<slide-template
:slideItem="slideItem"
:slideIDX="idx"
:removeSlideFn="removeCurrSlide" />
</slide>
<hooper-navigation slot="hooper-addons"></hooper-navigation>
<hooper-pagination slot="hooper-addons"></hooper-pagination>
</hooper>
<div class="buttons has-addons is-centered is-inline-block">
<button class="button is-info" #click="slidePrev">PREV</button>
<button class="button is-info" #click="slideNext">NEXT</button>
</div>
</div>
</template>
<script>
import {
Hooper,
Slide,
Pagination as HooperPagination,
Navigation as HooperNavigation
} from "hooper";
import "hooper/dist/hooper.css";
import slideTemplate from "./slide-template.vue";
import { slideShowsRef } from "./utils.js";
export default {
data() {
return {
sliderRef: "SlideShow 1",
slideList: [],
hooperSettings: {
autoPlay: false,
centerMode: true,
progress: true
},
hooperStyle: {
height: "265px"
}
};
},
methods: {
slidePrev() {
this.$refs.carousel.slidePrev();
},
slideNext() {
this.$refs.carousel.slideNext();
},
//Removes slider identified by IDX
removeCurrSlide(idx) {
this.slideList.splice(idx, 1);
},
// Fetch data from firebase
getSliderData() {
let that = this;
let mySliderRef = slideShowsRef.child(this.sliderRef);
mySliderRef.once("value", snap => {
if (snap.val()) {
this.slideList = [];
snap.forEach(childSnapshot => {
that.slideList.push(childSnapshot.val());
});
}
});
}
},
watch: {
getSlider: {
handler: "getSliderData",
immediate: true
}
},
components: {
slideTemplate,
Hooper,
Slide,
HooperPagination,
HooperNavigation
}
};
</script>
components/slide-template.vue (child, with vue2-dropzone)
<template>
<div class="slide-wrapper">
<slideTitle :heading="slideItem.heading" />
<a class="button delete remove-curr-slide" #click="deleteCurrSlide(slideIDX)" ></a>
<vue2Dropzone
#vdropzone-file-added="fileWasAdded"
#vdropzone-thumbnail="thumbnail"
#vdropzone-mounted="manuallyAddFiles(slideItem.zones)"
:destroyDropzone="false"
:include-styling="false"
:ref="`dropZone${ slideIDX }`"
:id="`customDropZone${ slideIDX }`"
:options="dropzoneOptions">
</vue2Dropzone>
</div>
</template>
<script>
import slideTitle from "./slide-title.vue";
import vue2Dropzone from "#dkjain/vue2-dropzone";
import { generate_ObjURLfromImageStream, asyncForEach } from "./utils.js";
export default {
props: ["slideIDX", "slideItem", "removeSlideFn"],
data() {
return {
dropzoneOptions: {
url: "https://vuejs-slider-node-lokijs-api.herokuapp.com/imageUpload",
thumbnailWidth: 150,
autoProcessQueue: false,
maxFiles: 1,
maxFilesize: 2,
addRemoveLinks: true,
previewTemplate: this.template()
}
};
},
components: {
slideTitle,
vue2Dropzone
},
methods: {
template: function() {
return `<div class="dz-preview dz-file-preview">
<div class="dz-image">
<img data-dz-thumbnail/>
</div>
<div class="dz-details">
<!-- <div class="dz-size"><span data-dz-size></span></div> -->
<!-- <div class="dz-filename"><span data-dz-name></span></div> -->
</div>
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
<div class="dz-error-message"><span data-dz-errormessage></span></div>
<div class="dz-success-mark"><i class="fa fa-check"></i></div>
<div class="dz-error-mark"><i class="fa fa-close"></i></div>
</div>`;
},
thumbnail: function(file, dataUrl) {
var j, len, ref, thumbnailElement;
if (file.previewElement) {
file.previewElement.classList.remove("dz-file-preview");
ref = file.previewElement.querySelectorAll("[data-dz-thumbnail]");
for (j = 0, len = ref.length; j < len; j++) {
thumbnailElement = ref[j];
thumbnailElement.alt = file.name;
}
thumbnailElement.src = dataUrl;
return setTimeout(
(function(_this) {
return function() {
return file.previewElement.classList.add("dz-image-preview");
};
})(this),
1
);
}
},
// Drag & Drop Events
async manuallyAddFiles(zoneData) {
if (zoneData) {
let dropZone = `dropZone${this.slideIDX}`;
asyncForEach(zoneData, async fileInfo => {
var mockFile = {
size: fileInfo.size,
name: fileInfo.originalName || fileInfo.name,
type: fileInfo.type,
id: fileInfo.id,
childZoneId: fileInfo.childZoneId
};
let url = `https://vuejs-slider-node-lokijs-api.herokuapp.com/images/${
fileInfo.id
}`;
let objURL = await generate_ObjURLfromImageStream(url);
this.$refs[dropZone].manuallyAddFile(mockFile, objURL);
});
}
},
fileWasAdded(file) {
console.log("Successfully Loaded Files from Server");
},
deleteCurrSlide(idx) {
this.removeSlideFn(idx);
}
}
};
</script>
<style lang="scss">
.slide-wrapper {
position: relative;
}
[id^="customDropZone"] {
background-color: orange;
font-family: "Arial", sans-serif;
letter-spacing: 0.2px;
/* color: #777; */
transition: background-color 0.2s linear;
// height: 200px;
padding: 40px;
}
[id^="customDropZone"] .dz-preview {
width: 160px;
display: inline-block;
}
[id^="customDropZone"] .dz-preview .dz-image {
width: 80px;
height: 80px;
margin-left: 40px;
margin-bottom: 10px;
}
[id^="customDropZone"] .dz-preview .dz-image > div {
width: inherit;
height: inherit;
// border-radius: 50%;
background-size: contain;
}
[id^="customDropZone"] .dz-preview .dz-image > img {
width: 100%;
}
[id^="customDropZone"] .dz-preview .dz-details {
color: white;
transition: opacity 0.2s linear;
text-align: center;
}
[id^="customDropZone"] .dz-success-mark,
.dz-error-mark {
display: none;
}
.dz-size {
border: 2px solid blue;
}
#previews {
border: 2px solid red;
min-height: 50px;
z-index: 9999;
}
.button.delete.remove-curr-slide {
padding: 12px;
margin-top: 5px;
margin-left: 5px;
position: absolute;
right: 150px;
background-color: red;
}
</style>
slide-title.vue (not that important)
<template>
<h2 contenteditable #blur="save"> {{ heading }} </h2>
</template>
<script>
export default {
props: ["heading"],
methods: {
save() {
this.$emit("onTitleUpdate", event.target.innerText.trim());
}
}
};
</script>
utils.js (utility)
export async function generate_ObjURLfromImageStream(url) {
return await fetch(url)
.then(response => {
return response.body;
})
.then(rs => {
const reader = rs.getReader();
return new ReadableStream({
async start(controller) {
while (true) {
const { done, value } = await reader.read();
// When no more data needs to be consumed, break the reading
if (done) {
break;
}
// Enqueue the next data chunk into our target stream
controller.enqueue(value);
}
// Close the stream
controller.close();
reader.releaseLock();
}
});
})
// Create a new response out of the stream
.then(rs => new Response(rs))
// Create an object URL for the response
.then(response => {
return response.blob();
})
.then(blob => {
// generate a objectURL (blob:url/<uuid> list)
return URL.createObjectURL(blob);
})
.catch(console.error);
}
Technically this is how the app works, slider.vue loads & fetches data from database (firebase) and stores in a data array slideList, loops over the slideList & passes each slideData (prop slideItem) to vue-dropzone component (in slide-template.vue), when dropzone mounts it fires the manuallyAddFiles(slideItem.zones) on the #vdropzone-mounted custom event.
The async manuallyAddFiles() fetches image from an API (hosted on heroku), creates (generate_ObjURLfromImageStream(url)) a unique blob URL for the image (blob:/) and then calls plugins API dropZone.manuallyAddFile() to load the image into the corresponding dropzone.
To delete the current slide, child's deleteCurrSlide() calls parent's (slider.vue) removeSlideFn (passed as prop) method with the idx of current slide. The removeSlideFn use splice to remove the item at the corresponding array idx this.slideList.splice(idx, 1).
The problem is when I delete the first slide, first slide is deleted but not entirely, the dropzone images of the first slides are not destroyed and still remains in the DOM, instead the images of slide2, (the next slide) are deleted from the DOM.
CodeSandBox Demo
I am not sure what is causing the issue, may it's due to something in the vue's reactivity system OR Vue's Array reactivity caveat that is causing this.
Can anybody pls help me understand & resolve this and if possible point out the reason to the root of the problem.
Your help is much appreciated.
Thanks,
I think you probably missunderstand what is going on:
In VueJS there is a caching method which allow the reusing of existing component generated: - Each of your object are considered equals when rendered (at a DOM level).
So VueJS remove the last line because it is probably ask the least calculation and then recalcul the expected state. There are many side case to this (sometime, the local state is not recalculated). To avoir this: As recommended in the documentation, use :key to trace the id of your object. From the documentation:
When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index. This is similar to the behavior of track-by="$index" in Vue 1.x.
This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM state (e.g. form input values).
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item. An ideal value for key would be the unique id of each item. This special attribute is a rough equivalent to track-by in 1.x, but it works like an attribute, so you need to use v-bind to bind it to dynamic values...
new Vue({
el: "#app",
data: {
counterrow: 1,
rows: [],
},
methods: {
addrow: function() {
this.counterrow += 1;
this.rows.push({
id: this.counterrow,
model: ""
});
},
removerows: function(index) {
this.rows.splice(index, 1);
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<table>
<tr>
<td><input type="text" name="test1" /></td>
<td><button class="btn" #click="addrow">add row</button></td>
</tr>
<tr v-for="(row,index) in rows" :key="row.id">
<td><input type="text" name="test2" v-model="row.model" /></td>
<td><button class="btn" #click="removerows(index)">remove </button></td>
</tr>
</table>
</div>
In this code:
I corrected the fact counterrow was never incremented
I added a :key
The documentation of :key
What did you mean by
The problem is when I delete the first slide, first slide is deleted but not entirely, the dropzone images of the first slides are not destroyed and still remains in the DOM, instead the images of slide2, (the next slide) are deleted from the DOM.
From what I see, the elements are no longer in the DOM

Why does this event actually trigger?

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.