How to refresh Vue components upon list item removal? - javascript

I know how to remove a list item from a Vue instance. However, when list items are passed to Vue components, how to remove a list item while keeping the components in sync with the list data?
Here is the use case. Consider an online forum with a Markdown editor. We have a Vue instance whose data are a list of saved comments fetched from a server. These comments are supposed to be written in Markdowns.
To facilitate edits and previews, we also have a list of components. Each component contains an editable input buffer as well as a preview section. The content of the saved comment in the Vue instance is used to initialise the input buffer and to reset it when a user cancels an edit. The preview is a transformation of the content of the input buffer.
Below is a test implementation:
<template id="comment">
<div>
Component:
<textarea v-model="input_buffer" v-if="editing"></textarea>
{{ preview }}
<button type="button" v-on:click="edit" v-if="!editing">edit</button>
<button type="button" v-on:click="remove" v-if="!editing">remove</button>
<button type="button" v-on:click="cancel" v-if="editing">cancel</button>
</div>
</template>
<div id="app">
<ol>
<li v-for="(comment, index) in comments">
<div>Instance: {{comment}}</div>
<comment
v-bind:comment="comment"
v-bind:index="index"
v-on:remove="remove">
</comment>
</li>
</ol>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.8/vue.js"></script>
<script>
let comments = ['111', '222', '333']
Vue.component('comment', {
template: '#comment',
props: ['comment', 'index'],
data: function() {
return {
input_buffer: '',
editing: false,
}
},
mounted: function() { this.cancel() },
computed: {
preview: function() {
// This is supposed to be a transformation of the input buffer,
// but for now, let's simply output the input buffer
return this.input_buffer
},
},
methods: {
edit: function() { this.editing = true },
remove: function() { this.$emit('remove', this.index) },
cancel: function() { this.input_buffer = this.comment; this.editing = false },
//save: function() {}, // submit to server; not implemented yet
},
})
let app = new Vue({
el: '#app',
data: { comments: comments },
methods: {
remove: function(index) { this.comments.splice(index, 1); app.$forceUpdate() },
},
})
</script>
The problem is that, if we remove a comment, the components are not refreshed accordingly. For example, we have 3 comments in the above implementation. if you remove comment 2, the preview of item 3 will still show the content of item 2. It is updated only if we press edit followed by cancel.
I've tried app.$forceUpdate(), but that didn't help.

You just need to add key attribute in the v-for loop like following:
<li v-for="(comment, index) in comments" :key="comment">
See working fiddle: https://fiddle.jshell.net/mimani/zLrLvqke/
Vue tries to optimises rendering, by providing key attribute, it treats those as completely different elements and re-renders those properly.
See the key documentation for more information.

try with:
Vue.component('comment', {
template:
`<div>
{{ comment }}
<button v-on:click="remove"> X </button>
</div>`,
props: ['comment', 'index'],
methods: {
remove: function() {
this.$emit('remove', this.index);
}
},
});
vm = new Vue({
el: '#app',
data: {
comments: ['a','b','c','d','e']
},
methods: {
remove: function(index) {
this.comments.splice(index, 1);
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.8/vue.min.js"></script>
<div id="app">
<ol>
<li v-for="(comment, index) in comments">
<comment
v-bind:comment="comment"
v-bind:index="index"
v-on:remove="remove">
</comment>
</li>
</ol>
</div>

Related

Vue.js update contents in div dynamically

I am attempting to do a SPA using Vue.js but unfortunately I know almost nothing about it, I followed a tutorial and got something up and running. This should hopefully be relatively simple!
I'm trying to create a simple page that:
Does a REST API call and pulls some JSON
A list with links of a particular field in the list of results is displayed on the left side of the screen
(I've managed until here)
Now I would like to be able to click on one of the links and see on the right side of the screen the value of another field for the same record.
For instance, suppose my JSON is:
{
"jokes":{
[
"setup":"setup1",
"punchline":"punchline1"
],
[
"setup":"setup2",
"punchline":"punchline2"
],
[
"setup":"setup3",
"punchline":"punchline3"
]
}
}
So in my screen I would see:
setup1
setup2
setup3
So if I click in setup1 I see punchline1, setup2 displays punchline2 and so on.
Here is my code - I'm basically trying to display the punchline in the moduleinfo div. I realise the current solution does not work. I've been searching but can't find any similar examples. Any pointers would be greatly appreciated.
<template>
<div class="home">
<div class="module-list">
<input type="text" v-model.trim="search" placeholder="Search"/>
<div>
<ul>
<li class="modules" v-for="value in modulesList" :key="value.id">
{{ value.setup }}
</li>
</ul>
</div>
</div>
<div class="moduleinfo">
<h2>Module info</h2>
<!-- <p>{{ value.punchline }}</p> -->
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Home',
data: function(){
return {
jokes: [],
search : ""
}
},
mounted() {
this.getModules();
},
methods: {
getModules() {
var self = this
const options = {
method: 'GET',
url: 'https://dad-jokes.p.rapidapi.com/joke/search',
params: {term: 'car'},
headers: {
'x-rapidapi-key': '...',
'x-rapidapi-host': 'dad-jokes.p.rapidapi.com'
}
};
axios.request(options)
.then(response => {
self.jokes = response.data;
console.log(response.data);
}).catch(function (error) {
console.error(error);
});
}
},
computed: {
modulesList: function () {
var jokes = this.jokes.body;
var search = this.search;
if (search){
jokes = jokes.filter(function(value){
if(value.setup.toLowerCase().includes(search.toLowerCase())) {
return jokes;
}
})
}
return jokes;
}
},
};
</script>
Thanks!
I was building a sample Single File Component in my Vue 2 CLI app, and when I came back to post it, Ryoko had already answered the question with the same approach that I recommend, adding a new property to track showing the punchline.
Since I already built it, I figured that I might as well post my component, which does change the layout, using a table instead of a list, but the functionality works.
<template>
<div class="joke-list">
<div class="row">
<div class="col-md-6">
<table class="table table-bordered">
<thead>
<tr>
<th>SETUP</th>
<th>PUNCHLINE</th>
</tr>
</thead>
<tbody>
<tr v-for="(joke, index) in jokes" :key="index">
<td>
{{ joke.setup }}
</td>
<td>
<span v-if="joke.showPunchline">{{ joke.punchline }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
jokes: [
{
setup: "setup1",
punchline: "punchline1"
},
{
setup: "setup2",
punchline: "punchline2"
},
{
setup: "setup3",
punchline: "punchline3"
}
]
}
},
methods: {
getPunchline(index) {
this.jokes[index].showPunchline = true;
},
addPropertyToJokes() {
// New property must be reactive
this.jokes.forEach( joke => this.$set(joke, 'showPunchline', false) );
}
},
mounted() {
this.addPropertyToJokes();
}
}
</script>
You can add a new property inside the data object and then make a new method to set it accordingly when you click the <a> tag. Have a look at the code below, it was a copy of your current solution, edited & simplified to show the addition that I made to make it easier for you to find it.
The select method will insert the object of the clicked joke to the selectedJoke so you can render it below the Module Info.
Because it's defaults to null, and it might be null or undefined, you have to add v-if to the attribute to check wether there is a value or not so you don't get error on the console.
<template>
<div class="home">
<div class="module-list">
<input type="text" v-model.trim="search" placeholder="Search"/>
<div>
<ul>
<li class="modules" v-for="value in modulesList" :key="value.id">
{{ value.setup }}
</li>
</ul>
</div>
</div>
<div class="moduleinfo">
<h2>Module info</h2>
<p v-if="selectedJoke">{{ selectedJoke.punchline }}</p>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Home',
data: function(){
return {
jokes: [],
search : "",
selectedJoke: null,
}
},
methods: {
select(joke) {
this.selectedJoke = joke;
},
},
};
</script>

VueJS component display other component content

The objective: Vue component input-address has to be inside Vue component mail-composer and display a list of addresses only when someone click Address Book button. When someone click one of displayed mails or fill the To field by hand, createdmail.to has to get the value and I have to hide the list of addresses.
Vue component mail-composer. This component receives a list of addresses. (Everything is working here, I think the only part that is not working properly is v-model inside input-address tag)
Vue.component('mail-composer', {
props: ['addressesbook'],
methods: {
send: function(createmail) {
this.$emit('send', createmail);
}
},
template:
`
<div>
<input-address :addresses="addressesbook" v-model="createmail.to"></input-address>
<p><b>Subject: </b><input type="text" v-model="createmail.subject"></input></p>
<p><b>Body: </b><textarea v-model="createmail.body"></textarea></p>
<button #click="send(createmail)">Send</button>
</div>
`,
data(){
return{
createmail:{
to: '',
subject: '',
body: ''
}
}
}
});
The other Vue component is this one, which is in the same file. (I think all problems are here).
I need to display the list of addresses only when someone click Address Book button, and I have to hide it when someone click again the button or one of the emails which are in the list. When someone clicks a mail from list, the createmail.to property from the mail-composer has to get the value of the mail , also if I decide to put the mail by hand it has to occurs the same.
Vue.component('input-address',{
props:["addresses"],
template:
`
<div>
<label><b>To: </b><input type="text"></input><button #click="!(displayAddressBook)">Address Book</button></label>
<ul v-if="displayAddressBook">
<li v-for="address in addresses">
{{address}}
</li>
</ul>
</div>
`,
data(){
return{
displayAddressBook: false
}
}
})
There're some errors in your code:
#click="!(displayAddressBook)" should be #click="displayAddressBook = !displayAddressBook" - the first really does nothing (interesting), the second (suggested) sets the value of displayAddressBook to the opposite it has currently.
the input-address component does not really do anything with the input field (missing v-model)
the changes in the child component (input-address) are not sent back to the parent (added a watcher to do that in the child component)
the parent component (mail-composer) has to handle the values emitted from the child (added the #address-change action handler)
the v-for in your input-address component does not have a key set. Added key by using the index for it (not the best solution, but easy to do).
just put createmail.to: {{ createmail.to }} at the end of MailComposer, so you can see how it changes
Suggestions
always use CamelCase for component names - if you get used to it, then you get less "why is it not working?!" moments
watch for typos: createmail doesn't look good - createEmail or just simply createemail would be better (ok, it doesn't look so nice - maybe you should choose a totally different name for that)
Vue.component('InputAddress', {
props: ["addresses"],
data() {
return {
displayAddressBook: false,
address: null
}
},
template: `
<div>
<label><b>To: </b>
<input
type="text"
v-model="address"
/>
<button
#click="displayAddressBook = !displayAddressBook"
>
Address Book
</button>
</label>
<ul v-if="displayAddressBook">
<li
v-for="(address, i) in addresses"
:key="i"
#click="clickAddressHandler(address)"
>
{{address}}
</li>
</ul>
</div>
`,
watch: {
address(newVal) {
// emitting value to parent on change of the address
// data attribute
this.$emit('address-change', newVal)
}
},
methods: {
clickAddressHandler(address) {
// handling click on an address in the address book
this.address = address
this.displayAddressBook = false
}
}
})
Vue.component('MailComposer', {
props: ['addressesbook'],
data() {
return {
createmail: {
to: '',
subject: '',
body: ''
}
}
},
methods: {
send: function(createmail) {
this.$emit('send', createmail);
},
addressChangeHandler(value) {
this.createmail.to = value
}
},
template: `
<div>
<input-address
:addresses="addressesbook"
v-model="createmail.to"
#address-change="addressChangeHandler"
/>
<p>
<b>Subject: </b>
<input
type="text"
v-model="createmail.subject"
/>
</p>
<p>
<b>Body: </b>
<textarea v-model="createmail.body"></textarea>
</p>
<button #click="send(createmail)">Send</button><br />
createmail.to: {{ createmail.to }}
</div>
`
});
new Vue({
el: "#app",
data: {
addressesbook: [
'abcd#abcd.com',
'fghi#fghi.com'
]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<mail-composer :addressesbook="addressesbook" />
</div>

how to see changes in new data generated in a vue component?

Well I hope to explain, I'm generating this data from a component, when I click the checkbox changes in the generated data are not reflected, but when clicking the button with a data already in the instance changes are made, I appreciate if you explain Why or do they have a solution?
this my code
js
Vue.component('fighters', {
template: '#fighters-template',
data() {
return {
items: [
{ name: 'Ryu' },
{ name: 'Ken' },
{ name: 'Akuma' }
],
msg: 'hi'
}
},
methods: {
newData() {
this.items.forEach(item => {
item.id = Math.random().toString(36).substr(2, 9);
item.game = 'Street Figther';
item.show = false;
});
this.items.push()
},
greeting() {
this.msg = 'hola'
}
}
});
new Vue({
el: '#app'
})
html
<main id="app">
<fighters></fighters>
</main>
<template id="fighters-template">
<div class="o-container--sm u-my1">
<ul>
<li v-for="item in items">
<input type="checkbox" v-model="item.show">
{{ item.name }}</li>
</ul>
<button class="c-button c-button--primary" #click="newData()">New Data</button>
<h2>{{ msg }}</h2>
<button class="c-button c-button--primary" #click="greeting()">Greeting</button>
<hr>
<pre>{{ items }}</pre>
</div>
</template>
this live code
https://jsfiddle.net/cqx12a00/1/
Thanks for you help
You don't declare the show variables that your checkboxes are bound to, so they are not reactive – Vue is not aware when one is updated.
It should be initialized like so:
items: [
{ name: 'Ryu', show: false },
{ name: 'Ken', show: false },
{ name: 'Akuma', show: false }
]
You might think that newData would fix it, since it assigns a show member to each item, but Vue cannot detect added properties so they're still not reactive. If you initialized the data as I show above, then the assignment would be reactive.
If you want to add a new reactive property to an object, you should use Vue.set.

Updating an input's bound value when edited

I'm working on a basic to-do application. Each to-do/task item gets listed as an input item in a Vue <list-item> component, and the <list-item>s are displayed with a v-for pointing to a tasks array.
I'm trying to allow the user to edit each task input, and upon changing the value, have this update the array item (rather than just the input itself). My #change event on the input is firing, but I'm at a loss as to what to do after this point.
https://jsfiddle.net/xbxm7hph/
HTML:
<div class="app">
<div class="add-control-area columns is-mobile is-multiline">
<responsive-container>
<div class="field is-grouped">
<div class="control is-expanded">
<input class="input add-control-text" type="text" placeholder="New Task" v-model="newTask" v-on:keyup.enter="addTask">
</div>
<div class="control">
<a class="button is-white add-control-button" #click="addTask" :disabled="!isThereText">Add Task</a>
</div>
</div>
</responsive-container>
<responsive-container>
<list-item v-for="task, index in tasks" :item="task" :index="index" #task-completed="completeTask(index)" #task-deleted="deleteTask(index)" ></list-item>
</responsive-container>
</div>
</div>
JS:
Vue.component('list-item', {
props: ['item', 'index'],
template: `<div class="task-wrapper">
<input class="task" :value="item" #change="updateTask()">
<div class="task-control delete-task" #click="deleteTask()"></div>
<div class="task-control complete-task" #click="completeTask()"></div>
</div>
`,
methods: {
completeTask: function() {
this.$emit('task-completed', this.index);
},
deleteTask: function() {
this.$emit('task-deleted', this.index);
},
updateTask: function() {
console.log('changed');
}
}
});
Vue.component('responsive-container', {
template: `
<div class="column is-4-desktop is-offset-4-desktop is-10-tablet is-offset-1-tablet is-10-mobile is-offset-1-mobile">
<div class="columns is-mobile">
<div class="column is-12">
<slot></slot>
</div>
</div>
</div>
`
});
var app = new Vue({
el: '.app',
data: {
tasks: [],
completedTasks: [],
newTask: ''
},
methods: {
addTask: function() {
if(this.isThereText) {
this.tasks.push(this.newTask);
this.newTask = '';
this.updateStorage();
}
},
completeTask: function(index) {
this.completedTasks.push(this.tasks[index]);
this.tasks.splice(index, 1);
this.updateStorage();
},
deleteTask: function(index) {
this.tasks.splice(index, 1);
this.updateStorage();
},
updateStorage: function() {
localStorage.setItem("tasks", JSON.stringify(this.tasks));
}
},
computed: {
isThereText: function() {
return this.newTask.trim().length;
}
},
// If there's already tasks stored in localStorage,
// populate the tasks array
mounted: function() {
if (localStorage.getItem("tasks")) {
this.tasks = JSON.parse(localStorage.getItem("tasks"));
}
}
});
Use a v-model directive on your <list-item> component, instead of passing in an item property. You will also need to pass in a reference from the array (tasks[index]), because task in this scope is a copy that is not bound to the element of the array:
<list-item v-for="task, index in tasks" v-model="tasks[index]"></list-item>
In your component definition for the list item, you'll need to now take in a value prop (this is what gets passed when using v-model) and set a data property item to that value. Then, emit an input event on the change to pass the item value (this is what the component is listening for when using v-model):
Vue.component('list-item', {
props: ['value'],
template: `<div class="task-wrapper">
<input class="task" v-model="item" #change="updateTask"></div>
</div>
`,
data() {
return {
item: this.value,
}
},
methods: {
updateTask: function() {
this.$emit('input', this.item);
}
}
});
Here's a fiddle with those changes.
As Bert Evans mentioned, even though this works, Vue requires that components using the v-for directive also use a key attribute (you will get a warning from Vue otherwise):
<list-item
v-for="task, index in tasks"
:key="index"
v-model="tasks[index]"
></list-item>
Also, realize that the index variable in a v-for scope can change, meaning that the item at index 1 might change to index 4 and this can pose some problems as the application gets more complex. A better way would be to store items as an object with an id property. This way you can have an immutable id associated with the item.
You can pass the index and new value to your change event handler:
<input class="task" :value="item" #change="updateTask(index, $event)">
Then access them accordingly:
updateTask: function(index, event) {
console.log(index);
console.log(event.target.value);
}

Sortable.js with Vue 2.0 sorts incorrectly

I am using Sortable.js and Vue.js. The goal is to sort items and keep data updated.
It worked well with Vue 1.x, but after update to 2.0 sorting became incorrect. Array still properly updates, but items in DOM are in the wrong places.
new Vue({
el: '#app',
template: '#sort',
data: function() {
return {
items: [
"http://placehold.it/200X300?text=image1",
"http://placehold.it/200X300?text=image2",
"http://placehold.it/200X300?text=image3",
"http://placehold.it/200X300?text=image4"
],
}
},
mounted: function() {
this.$nextTick(function () {
Sortable.create(document.getElementById('sortable'), {
animation: 200,
onUpdate: this.reorder.bind(this),
});
})
},
methods: {
reorder: function(event) {
var oldIndex = event.oldIndex,
newIndex = event.newIndex;
this.items.splice(newIndex, 0, this.items.splice(oldIndex, 1)[0]);
}
}
});
jsFiddle
https://jsfiddle.net/4bvtofdd/4/
Can someone help me?
I had a similar problem today.
Add :key value to make sure Vue rerenders elements in correct order after Sortable changes the items order
<div v-for="item in items" :key="item.id">
<!-- content -->
</div>
https://v2.vuejs.org/v2/guide/list.html#key
As it happens, Sortable keeps track of the order in sortable.toArray(), so it's pretty easy to make a computed that will give you the items in sorted order, while the original item list is unchanged.
new Vue({
el: '#app',
template: '#sort',
data: {
items: [
"http://placehold.it/200X300?text=image1",
"http://placehold.it/200X300?text=image2",
"http://placehold.it/200X300?text=image3",
"http://placehold.it/200X300?text=image4"
],
order: null
},
computed: {
sortedItems: function() {
if (!this.order) {
return this.items;
}
return this.order.map((i) => this.items[i]);
}
},
mounted: function() {
this.$nextTick(() => {
const sortable = Sortable.create(document.getElementById('sortable'), {
animation: 200,
onUpdate: () => { this.order = sortable.toArray(); }
});
})
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/Sortable/1.4.2/Sortable.min.js"></script>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"/>
<script src="//unpkg.com/vue#2.0.1/dist/vue.js"></script>
<div id='app'></div>
<template id="sort">
<div class="container">
<div class="row sort-wrap" id="sortable">
<div class="col-xs-6 col-md-3 thumbnail" v-for="(item, index) in items" :data-id="index">
<img v-bind:src="item" alt="" class="img-responsive">
</div>
</div>
<div v-for="item in sortedItems">
{{item}}
</div>
</div>
</template>
Make sure you aren't using a prop, or you won't be able to sort. If you are using a prop, assign the prop data to a data attribute and use the data attribute instead.

Categories