I want study v-for and template by vue manual.
For v-for and components, My code:
<div id="app-7">
<input v-model="newTodoText" v-on:keyup.enter="addNewTodo" placeholder="Add a todo">
<ul>
<todo-item2
v-for="(todo, index) in todos"
v-bind:key="todo.id"
v-bind:id="todo.id"
v-bind:title="todo.title"
v-on:remove="todos.splice(index, 1)">
</todo-item2>
</ul>
</div>
My main.js code is:
Vue.component('todo-item2', {
template: '<li>{{ title }}-{{ id }}<div v-on:remove="testRemove"><button v-on:click="$emit(\'remove\')">X</button></div></li>',
props: ['title', 'id'],
methods: {
testFunc: function () {
console.log('div click event trigger')
},
testRemove: function () {
console.log('div remove event trigger')
}
}
})
var app7 = new Vue({
el: '#app-7',
data: {
newTodoText: '',
todos: [
{
id: 1,
title: 'Do the dishes'
},
{
id: 2,
title: 'Take out the trash'
},
{
id: 3,
title: 'Mow the lwan'
}
],
nextTodoId: 4
},
methods: {
addNewTodo: function () {
this.todos.push({
id: this.nextTodoId++,
title: this.newTodoText
})
this.newTodoText = ''
}
}
})
I found that click button cause to remove li label, so I know li's remove event is triggered. But I don't understand why div'remove event is not triggered. Who can tell my $emit('remove')?
What's mean for $emit(args) triggered current instance event?
Your div component doesn't have remove event. If you change to click event (v-on:click="testRemove"), you can see console log div remove event trigger
Vue.component('todo-item2', {
template: '<li>{{ title }}-{{ id }}<div v-on:click="testRemove"><button v-on:click="$emit(\'remove\')">X</button></div></li>',
props: ['title', 'id'],
methods: {
testFunc: function () {
console.log('div click event trigger')
},
testRemove: function () {
console.log('div remove event trigger')
}
}
})
By default, Vue on support DOM events https://www.w3schools.com/jsref/dom_obj_event.asp
Updated
v-on:click="$emit(\'remove\')" will call this.$emit event. Here, this will be todo-item2 component. It only emit event to parent of todo-item2
Related
I'm trying to pass editTodo as props function from parent app.vue to child components ...
TodoItem.vue component there is a list Of Items and Time returns to main user input of newTodo and dateTime field. Actually, I'm a new learner of Vue js there is a little knowledge of pass props b/w the components communication.
<template>
<div id="app" class="container">
<TodoInput :addTodo="addTodo"
:updateTodo="updateTodo"
/>
<todo-item v-for="(todo, index) in todos"
:key=todo.id
:todo=todo
:index =index
:removeTodo="removeTodo"
:editTodo="editTodo" />
</div>
</template>
<script>
import TodoInput from "./components/TodoInput.vue";
import TodoItem from "./components/TodoItem.vue";
export default {
name: "App",
components: {
TodoInput,
TodoItem,
},
data() {
return {
editing:false,
editItems:{},
todos: [
// {
// id: 1,
// title: "",
// date: new Date(),
// editing: false,
// completed: false,
// },
// {
// id: 1,
// title: "",
// date: new Date(),
// editing: false,
// completed: false,
// },
],
};
},
methods: {
editTodo(index, newTodo, dateTime){
, ' dateTime ', dateTime)
// this.editItems = {
// id,
// title,
// time,
// }
this.todo = newTodo
this.todo = dateTime
this.selectedIndex = index
this.editing = true
},
TodoItem.vue component there is a list Of Items and Time returns to main user input of newTodo and dateTime field.***enter code here
`**
-->
{{todo.title}}
{{todo.time}}
</div>
<div class="remove-item" #click="removeTodo(index)">
×
</div>
<div class="edit-item" #click="eiditTodo(index)"
>
<i class="fas fa-edit" id="edit"></i>
</div>
export default {
name: 'todo-item',
props:{
todo:{
type: Object,
required: true,
},
removeTodo:{
type:Function,
required:true,
},
index:{
type:Number,
required: true,
},
},
data(){
return{
'id': this.todo.id,
'title': this.todo.newTodo,
'time': this.todo.dateTime,
}
methods:
getEdit(){
this.$emit('editTodo', this.selectedIndex)
}
**`
Instead of passing a function as a prop to the child component in order to run it, you should instead emit an event from the child component and execute the function in the parent component.
To emit an event from the child component
#click="$emit('edit-todo')"
And then in the parent component
<div #edit-todo="editTodo">
</div>
Alternatively, you could just define the editTodo function in theTodoItem component and call it directly.
I'm going to build a customized virtual keyboard, so that's the first problem I've encountered.
I have an input element, whose value is changed from outside, in my case by pressing a button. The problem is that there seems to be no way to trigger the normal 'change' event.
Neither clicking outside the input, nor pressing Enter gives any result. What might be the correct way of solving this problem?
<template>
<div class="app-input">
<input #change="onChange" type="text" v-model="value" ref="input">
<button #click="onClick">A</button>
</div>
</template>
<script>
export default {
name: "AppInput",
data() {
return {
inputDiv: null,
value: ""
};
},
props: {
msg: String
},
methods: {
onClick() {
this.value = this.value + "A";
this.inputDiv.focus();
},
onChange() {
alert("changed");
}
},
mounted() {
this.$nextTick(() => {
this.inputDiv = this.$refs.input;
});
}
};
</script>
The whole pen can be found here.
v-on:change would only trigger on a direct change on the input element from a user action.
What you are looking for is a wathcer for your data property, whenever your value changes, watcher will execute your desired function or task.
watch: {
value: function() {
this.onChange();
}
}
The watch syntax is elaborated on the provided official vuejs docs link. use your data property as the key and provide a function as a value.
Check the snippet.
export default {
name: "AppInput",
data() {
return {
inputDiv: null,
value: ""
};
},
props: {
msg: String
},
methods: {
onClick() {
this.value = this.value + "A";
this.inputDiv.focus();
},
onChange() {
alert("changed");
}
},
// this one:
watch: {
value: function() {
this.onChange();
}
},
// --- rest of your code;
mounted() {
this.$nextTick(() => {
this.inputDiv = this.$refs.input;
});
}
};
When I build any new vue application, I like to use these events for a search input or for other inputs where I don't want to fire any functions on #change
<div class="class">
<input v-model="searchText" #keyup.esc="clearAll()" #keyup.enter="getData()" autofocus type="text" placeholder="Start Typing ..."/>
<button #click="getData()"><i class="fas fa-search fa-lg"></i></button>
</div>
These will provide a better user experience in my opinion.
I'm using dynamic forms to break up a long form. Beneath each dynamic form are two action buttons which, when clicked, send the user back one form set or onto the next one (unless the end of the form has been reached - the 'Next' button is replaced by a 'Submit' button).
As I have three different forms that all use the same action buttons, I decided to create a child component ('ActionButtons') for action buttons - and in doing so I've gone from code that worked to code that doesn't.
Specifically, I'm having problems with ref information involved in controlling the 'Next' button. The error message is Error in nextTick: "TypeError: Cannot read property '$v' of undefined"
The code that worked fine (i.e. before I created 'ActionButtons') is:
<template>
...
<keep-alive>
<component
ref="currentStep"
:is="currentStep"
#update="processStep"
:wizard-data="formvars"
></component>
</keep-alive>
...
<button
#click="nextButtonAction"
:disabled="!canGoNext"
class="btn"
>{{isLastStep ? 'Submit' : 'Next'}}</button>
...
</template>
<script>
import Gap2InputInfo from './Gap2InputInfo'
import Gap2Materials from './Gap2Materials'
export default {
name: 'GAP2Form',
components: {
Gap2InputInfo,
Gap2Materials
},
data () {
return {
currentStepNumber: 1,
canGoNext: false,
wizardData: {},
steps: [
'Gap2InputInfo',
'Gap2Materials',
],
formvars: {
id: 0,
purchase: null,
name: null,
quantity: null,
supplier: null,
nsteps: 0
},
updatedData: null
}
},
computed: {
isLastStep () {
return this.currentStepNumber === this.length
},
length () {
this.formvars.nsteps = this.steps.length;
return this.steps.length
},
currentStep () {
return this.steps[this.currentStepNumber - 1];
},
},
methods: {
nextButtonAction () {
if (this.isLastStep) {
this.submitForm()
} else {
this.goNext()
}
},
processStep (step) {
Object.assign(this.formvars, step.data);
this.canGoNext = step.valid
},
goBack () {
this.currentStepNumber--;
this.canGoNext = true;
},
goNext () {
this.currentStepNumber++;
this.$nextTick(() => {
this.canGoNext = !this.$refs.currentStep.$v.$invalid
})
}
}
}
</script>
In creating the child component, I send the following props to the child
<action-buttons :currentStepNumber="currentStepNumber" :canGoNext="canGoNext" :isLastStep="isLastStep" :currentStep="currentStep"></action-buttons>
and have moved the methods associated with the button actions from the parent to the child component.
My child component is:
<template>
<div>
<div id="actions" class="buttons">
<button
#click="goBack"
v-if="mcurrentStepNumber > 1"
class="btn-outlined"
>{{$t('back')}}
</button>
<button
#click="nextButtonAction"
:disabled="!canGoNext"
class="btn"
>{{isLastStep ? 'Submit' : 'Next'}}</button>
</div>
</div>
</template>
<script>
import {required} from 'vuelidate/lib/validators'
export default {
name: "ActionButtons",
props: ['currentStepNumber','canGoNext','isLastStep','currentStep'],
data: function () {
return {
mcurrentStepNumber: this.currentStepNumber,
mcurrentStep: this.currentStep,
mcanGoNext: this.canGoNext,
misLastStep: this.isLastStep
}
},
methods: {
nextButtonAction () {
if (this.misLastStep) {
this.submitForm()
} else {
this.goNext()
}
},
goBack () {
this.mcurrentStepNumber--;
this.mcanGoNext = true;
},
goNext () {
this.mcurrentStepNumber++;
this.$nextTick(() => {
this.mcanGoNext = !this.$refs.currentStep.$v.$invalid **** Error triggered at this line
})
}
}
}
</script>
Now when I click the 'Next' button, I get the Cannot read property '$v' of undefined error message. If I've interpreted it correctly, it cannot read the $refs data. I tried altering the code in the child component to
this.mcanGoNext = !this.$parent.$refs.currentStep.$v.$invalid
but the outcome remained the same. Where am I going wrong?
Thanks, Tom.
I am new to Vue and am trying to build a simple movie app, fetching data from an API and rendering the results. I want to have an incremental search feature. I have an input field in my navbar and when the user types, I want to redirect from the dashboard view to the search results view. I am unsure of how to pass the query params from the navbar to the search results view.
Here is my App.vue component
<template>
<div id="app">
<Navbar></Navbar>
<router-view/>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue'
export default {
name: 'App',
components: {
Navbar
},
}
</script>
And here is my navbar component where I have the input field
<template>
<nav class="navbar">
<h1 class="logo" v-on:click="goToHome">Movie App</h1>
<input class="search-input" v-on:keyup="showResults" v-model="query" type="text" placeholder="Search..."/>
</nav>
</template>
<script>
import router from '../router/index'
export default {
data: function () {
return {
query: this.query
}
},
methods: {
goToHome () {
router.push({name: 'Dashboard'})
},
showResults () {
//here on each key press I want to narrow my results in the SearchedMovies component
}
}
}
</script>
If I use router.push to the SearchedMovies component then I am only able to pass the query as a parameter once. I thought about using Vuex to store the query and then access it from the SearchedMovies component, but surely there is a better way of doing it?
I also read about using $emit but since my parent contains all the routes, I'm not sure how to go about this.
You don't need to redirect user anywhere. I've made a small demo to show how one might do it. I used this navbar component as you described and emit an event from it:
const movies = {
data: [
{
id: 0,
title: 'Eraserhead',
},
{
id: 1,
title: 'Erazerhead',
},
{
id: 2,
title: 'Videodrome',
},
{
id: 3,
title: 'Videobrome',
},
{
id: 4,
title: 'Cube',
},
]
};
Vue.component('navbar', {
template: '<input v-model="filter" #input="onInput" placeholder="search">',
data() {
return {
filter: '',
};
},
methods: {
onInput() {
this.$emit('filter', this.filter);
}
}
});
// this is just a request imitation.
// just waiting for a second until we get a response
// from the datasample
function request(title) {
return new Promise((fulfill) => {
toReturn = movies.data.filter(movie => movie.title.toLowerCase().indexOf(title.toLowerCase()) !== -1)
setTimeout(() => fulfill(toReturn), 1000);
});
}
new Vue({
el: '#app',
data: {
movies: undefined,
loading: false,
filter: '',
lastValue: '',
},
methods: {
filterList(payload) {
// a timeout to prevent
// instant request on every input interaction
this.lastValue = payload;
setTimeout(() => this.makeRequest(), 1000);
},
makeRequest() {
if (this.loading) {
return;
}
this.loading = true;
request(this.lastValue).then((response) => {
this.movies = response;
this.loading = false;
});
}
},
mounted() {
this.makeRequest('');
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<navbar v-on:filter="filterList"></navbar>
<ul v-if="!loading">
<li v-for="movie in movies" :key="movie.id">{{ movie.title }}</li>
</ul>
<p v-else>Loading...</p>
</div>
Also jsfiddle: https://jsfiddle.net/oniondomes/rsyys3rp/
If you have any problem to understand the code above let me know.
EDIT: Fixed some bugs and added a couple of comments
EDIT2(after the comment below):
Here's what you can do. Every time user inputs something inside a navbar you call a function:
// template
<navbar v-on:input-inside-nav-bar="atInputInsideNavBar"></navbar>
// script
methods: {
atInputInsideNavBar(userInput) {
this.$router.push({
path: '/filtred-items',
params: {
value: userInput
}
})
}
}
Then inside you 'searched movies' page component you can access this value so:
this.$route.params.value // returns userInput from root component
For my app I'm using two Vue components. One that renders a list of "days" and one that renders for each "day" the list of "locations". So for example "day 1" can have the locations "Berlin", "London", "New York".
Everything gets rendered ok but after removing the "Day 1" from the list of days the view isn't rendered corrected. This is what happens:
The title of the day that was removed is replaced -> Correct
The content of the day that was removed isn't replaced -> Not correct
Vue.component('day-list', {
props: ['days'],
template: '<div><div v-for="(day, index) in dayItems">{{ day.name }} Remove day<location-list :locations="day.locations"></location-list><br/></div></div>',
data: function() {
return {
dayItems: this.days
}
},
methods: {
remove(index) {
this.dayItems.splice(index, 1);
}
}
});
Vue.component('location-list', {
props: ['locations', 'services'],
template: '<div><div v-for="(location, index) in locationItems">{{ location.name }} <a href="#" #click.prevent="remove(index)"</div></div>',
data: function() {
return {
locationItems: this.locations
}
},
methods: {
remove(index) {
this.locationItems.splice(index, 1);
}
}
});
const app = window.app = new Vue({
el: '#app',
data: function() {
return {
days: [
{
name: 'Day 1',
locations: [
{name: 'Berlin'},
{name: 'London'},
{name: 'New York'}
]
},
{
name: 'Day 2',
locations: [
{name: 'Moscow'},
{name: 'Seul'},
{name: 'Paris'}
]
}
]
}
},
methods: {}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.js"></script>
<div id="app">
<day-list :days="days"></day-list>
</div>
Please use Vue-devtools if you are not already using it. It shows the problem clearly, as seen in the image below:
As you can see above, your day-list component comprises of all the days you have in the original list, with locations listed out directly. You need one more component in between, call it day-details, which will render the info for a particular day. You may have the location-list inside the day-details.
Here is the updated code which works:
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.js"></script>
<div id="app">
<day-list :days="days"></day-list>
</div>
Vue.component('day-list', {
props: ['days'],
template: `
<div>
<day-details :day="day" v-for="(day, index) in days">
Remove day
</day-details>
</div>`,
methods: {
remove(index) {
this.days.splice(index, 1);
}
}
});
Vue.component('day-details', {
props: ['day'],
template: `
<div>
{{ day.name }}
<slot></slot>
<location-list :locations="day.locations"></location-list>
<br/>
</div>`
});
Vue.component('location-list', {
props: ['locations', 'services'],
template: `
<div>
<div v-for="(location, index) in locations">
{{ location.name }}
[x]
</div>
</div>
`,
methods: {
remove(index) {
this.locations.splice(index, 1);
}
}
});
const app = window.app = new Vue({
el: '#app',
data: function() {
return {
days: [{
name: 'Day 1',
locations: [{
name: 'Berlin'
}, {
name: 'London'
}, {
name: 'New York'
}]
}, {
name: 'Day 2',
locations: [{
name: 'Moscow'
}, {
name: 'Seul'
}, {
name: 'Paris'
}]
}]
}
},
methods: {}
});
One other thing - your template for location-list has an error - you are not closing the <a> element. You may use backtick operator to have multi-line templates as seen in the example above, to avoid template errors.
Also you are not supposed to change objects that are passed via props. It works here because you are passing objects which are passed by reference. But a string object getting modified in child component will result in this error:
[Vue warn]: Avoid mutating a prop directly...
If you ever get this error, you may use event mechanism as explained in the answer for this question: Delete a Vue child component