Related
Very simple question. I'm learning VueJS and have created a simple component:
Vue.component('blog-post', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})
I then have parsed some data to it like this:
new Vue({
el: '#blog-post-demo',
data: {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
})
My question is how can get the title of a specefic element based on the id in my HTML? For now I can only render through the items and get them all, but I want to be able to specify which title I want to display based on the Id. Here is my HTML which gives me all the data:
<div id="blog-post-demo">
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:title="post.title"
></blog-post>
</div>
You can achieve with COMPUTED property, like that
<template>
<div id="blog-post-demo">
<p v-for="post in thisOne" :key="post.id" >
{{post.title}}
</p>
</div>
</template>
<script>
export default {
el: '#blog-post-demo',
data() {
return {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
},computed: {
thisOne(){
return this.posts.filter(x => x.id === 3); /*choose your id*/
}
}
};
</script>
Or you can use event too to select the id of the posts to display (more dynamically)
Tip: If you start with VueJS, learn about the properties of VueJs (DATA, COMPUTED, CREATED, METHOD) and look at the uses and strengths of each one. For my part, the VueJS site is very very well done for beginners: https://v2.vuejs.org/v2/guide/
I'm not sure if I understand correctly what you want to do. But if you want to go through all posts and display title of particular post then you can try this way:
<blog-post
v-for="post in posts"
:key="post.id"
:title="setTitle(post)"
/>
(: instead of :v-bind it's a short form, also if you don't pass slots in your component you can go with self-closing tag)
Then in your methods section you can create a method:
setTitle(post) {
if(post.id === 2) return post.title
}
I'm building a list that users can choose items.
It would be a nested list with at least 3 layers.
As can only offer one layer of sub-option, I would like to build it as <ul> and <li>.
But I can't figure out how to change my code with two <ul> and <li>.
Hope someone could give me some ideas.
Here are what I have
<div id="applyApp" class="container">
<div class="pool">
<ul>
<h3 #click.prevent="isShow = !isShow">Category</h3>
<li v-for="items in filterData" :value="items.id">
{{items.id}} {{items.ame}}
</li>
</ul>
<ul class="selected-item">
<li v-for="items in secondLayer" :value="items.id">
{{items.id}} {{items.name}}
</li>
</ul>
</div>
Vue
new Vue({
el: "#applyApp",
data: {
firstLayer: [
{
name: "name1",
id: "0101",
},
{
name: "name2",
id: "010101",
},
{
name: "name3",
id: "010101001B",
},
],
secondLayer: [],
firstLayerValue: [],
secondLayerValue: [],
},
methods: {
moveHelper(value, arrFrom, arrTo) {
const index = arrFrom.findIndex(function (el) {
return el.id == value;
});
const item = arrFrom[index];
arrFrom.splice(index, 1);
arrTo.push(item);
},
addItems() {
const selected = this.firstLayerValue.slice(0);
for (const i = 0; i < selected.length; ++i) {
this.moveHelper(selected[i], this.firstLayer, this.secondLayer);
}
},
removeItems() {
const selected = this.secondLayerValue.slice(0);
for (const i = 0; i < selected.length; ++i) {
this.moveHelper(selected[i], this.secondLayer, this.firstLayer);
}
}
},
});
If you want nested items in your list, you might have to use a recursion component, which means adding child data to your items. And calling the component inside of itself until it exhausts the list of children, by taking the data props of the child in every call.
Vue.component("listRecursion", {
props: ['listData'],
name: "listRecursion",
template: `
<div>
<ul v-for="item in listData">
<li>{{item.name}}</li>
<list-recursion v-if="item.children.length" :list-data="item.children"/>
</ul>
</div>`
})
new Vue({
el: "#app",
data: {
todos: [{
name: "name1",
id: "0101",
children: [
{
name: "sub item 1",
id: "0d201",
children: []
},
{
name: "sub item 2",
id: "020g1",
children: []
},
{
name: "sub item 3",
id: "20201",
children: [
{
name: "subsub item 1",
id: "40201",
children: [
{
name: "subsub item 1",
id: "40201",
children: [
{
name: "subsubsub item 1",
id: "40201",
children: []
}]
}]
},
{
name: "subsub item 2",
id: "50201",
children: []
},
]
},
]
},
{
name: "name2",
id: "010101",
children: []
},
{
name: "name3",
id: "010101001B",
children: []
},
]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h2>Recursive list:</h2>
<list-recursion :list-data="todos"/>
</div>
As you can see, this saves you from manually adding new levels, just add to the data to the child nodes
I see #procoib which solves your first case and for the li rearrange, you can do the same approach similar to 'select' which is shown below.
new Vue({
el: "#app",
data() {
return {
first: [1, 2, 3, 4, 5],
second: []
}
},
methods: {
alter: function (index, src, desc) {
this[desc].push(this[src].splice(index, 1)[0])
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
<div id="app">
<h1>First</h1>
<ul id="firstList">
<li v-for="(_, i) in first" v-on:click="alter(i, 'first', 'second')">{{_}}</li>
</ul>
<h1>Second</h1>
<ul id="secondList">
<li v-for="(_, i) in second" v-on:click="alter(i, 'second', 'first')">{{_}}</li>
</ul>
</div>
If you click any number for a list, it is added to the other list.
In vue I have a component that displays the content of an array in its parent App.vue.
If I push to the array, ListofLists how do I get the display list component to display the updated data?
I have a form in a child component that $emits a javascript object newList which is captured by the method addList. I expected Vue to detect a change to the ListofList and recompute the value of loadedList. If I am right to expect this how do I get Vue to display the updated list?
Should I use computed or watch? Is it necessary to use forceRerender or does the problem lie elsewhere.
What I have tried
<ShowListComponent>
:key="componentKey"
v-bind:loadedList="loadedList"/>
computed: {
loadedList: function() {
return this.ListofLists[this.currentList];
}
},
methods: {
changeList: function(id) {
this.currentList = id;
this.ListofLists[this.currentPlaylist];
console.log('changed List' + this.ListofLists[this.currentList]);
},
addList: function(newList) {
this.ListofLists.push( newList );
console.log('lists after push ' +this.ListofLists);
this.forceRerender();
},
What I see
The console.log shows the ListofLists is updated correctly.
I get an error:
[Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined"
What I expect
The new list being displayed correctly.
EDIT - The template and component used
<template>
<div id="app">
<MediaPlayer
:key="componentKey"
v-on:openAllSongs="showAllSongs = !showAllSongs"
v-on:openPlaylist="showPlaylist= !showPlaylist"
v-bind:loadedPlaylist="loadedPlaylist"/>
<AllSongs
v-on:closeAllSongs="showAllSongs = false"
v-bind:songs="playlists[0].songs"
v-bind:class="{show : showAllSongs}"
/>
<Playlists
v-on:closePlaylist="showPlaylist = false"
v-bind:playlists="playlists"
v-bind:class="{show : showPlaylist}"
v-on:change-playlist="changePlaylist"
v-on:add-playlist="addPlaylist"
/>
</div>
</template>
<script>
import MediaPlayer from './components/MediaPlayer.vue'
import Playlists from "./components/Playlists.vue";
export default {
name: 'App',
components: {
MediaPlayer,
AllSongs,
Playlists,
},
props: ['playlist'],
data() {
return {
playlists: [
{
id: 1,
name: 'All Songs',
description: 'Magnum Opus',
created: '18/09/1830',
updated: '23/04/2020',
created_by: 'Jamie',
songs: [
{
id: '1',
position_in_list_index: 47,
name: 'i dont know what this is but i like it',
filename: 'i dont know what this is but i like it.mp3',
length_in_seconds: 200,
startdate: 'yesterday',
tags: 'quirky, instumental'
},
{
id:"3",
position_in_list_index: 38,
name: "sunday junkie",
filename: "sunday junkie.mp3",
length_in_seconds: 222,
tags: 'indie'
},
{
id: 4,
position_in_list_index: 40,
name: "burned",
filename: "burned.mp3",
length_in_seconds: 303,
tags: 'melancholy, serious'
},
{
id: 5,
position_in_list_index: 46,
name: "losing limbs like theres no tomorrow",
filename: "losing limbs like theres no tomorrow.mp3",
length_in_seconds: 232,
tags: 'hiphop, dark, synth'
}
],
},
{
id:3,
name: 'Rock Hard Seduction',
description: 'songs that bring a tear to your eye',
created: '21/02/2020',
updated: '23/03/2020',
created_by: 'Dewi',
songs: [
{
id: 4,
position_in_list_index: 21,
name: "trefnant rainbow v2",
filename: "trefnant rainbow v2.mp3",
length_in_seconds: 174,
tags: "rock"
},
{
id: 9,
position_in_list_index: 61,
name: "trefnant wind added solo",
filename: "trefnant wind added solo.mp3",
length_in_seconds: 268,
tags: 'folk'
}
]
},
{
id:4,
name: 'Family friendly',
description: 'they got no lyrics',
created: '20/02/2020',
updated: '23/02/2020',
created_by: 'Dewi',
songs: [
{
id: "15",
position_in_list_index: 74,
name: "dont waltz with a psycho like me",
filename: "dont waltz with a psycho like me.mp3",
length_in_seconds: 273
},
{
id: "17",
position_in_list_index: 76,
name: "lie down on the tracks",
filename: "lie down on the tracks.mp3",
length_in_seconds: 225,
tags: 'rock, instrumental, sounds like someone else'
},
{
id: "28",
position_in_list_index: 87,
name: "you two frozen heroes",
filename: "you two frozen heroes.mp3",
length_in_seconds: 267
}
]
},
],
showPlaylist: false,
currentPlaylist: 0,
componentKey: 0
}
},
computed: {
loadedPlaylist: function() {
console.log('playlist length in loadedPlaylist ' + this.playlists.length);
console.log('current playlist id '+ this.currentPlaylist);
return this.playlists[this.currentPlaylist];
}
},
methods: {
changePlaylist: function(id) {
this.currentPlaylist = id - 1;
this.playlists[this.currentPlaylist];
console.log('changed playlist' + this.playlists[this.currentPlaylist]);
},
addPlaylist: function(newPlaylist) {
this.playlists.push( newPlaylist );
console.log('playlists after push ' +this.playlists);
this.forceRerender();
this.changePlaylist(newPlaylist.id);
},
forceRerender: function() {
this.componentKey++;
console.log("componentKey " + this.componentKey);
}
}
}
</script>
the component template I am using to display the list.
<template>
<div class="media-player">
<draggable
v-model="loadedPlaylist.songs" class="songlist"
:group="{ name: 'songs'}"
ghost-class="ghost"
v-bind:key="loadedPlaylist.id"
>
<div
class="songlist-item"
#click='loadTrack(index, true)'
v-bind:class="{ 'is-active': index === activeTrack }"
v-bind:key="song.id"
v-for="(song, index) in loadedPlaylist.songs"
>
<h3>{{index + 1}}. {{song.name}}<span></h3>
</div>
</draggable>
</div>
</template>
My code was adding a new object to the array correctly and the update was passed to the child component correctly.
I was getting an error because the new object contained an empty array. trying to access it directly caused an error. Adding
<h2 v-if="loadedPlaylist.songs.length">{{loadedPlaylist.songs[activeTrack].name}}</h2>
<h2 v-else>Add Songs below</h2>
fixed the error.
Suppose I want to display a List of Questions. For each question, there is a list of answers, none of which are right or wrong. For each question, the user can choose an answer. I'm wondering how to create two-way binding on the selected answer.
The Vue:
new Vue(
{
el: "#app",
data:
{
questions: [{}]
}
}
Example Question Model:
{
id: 1,
name: "Which color is your favorite?",
selectedAnswerId: null,
selectedAnswerName: null,
answers:
[
{id: 1, name: red, photoUrl: ".../red", selected: false},
{id: 2, name: green, photoUrl: ".../green", selected: false},
{id: 3, name: blue, photoUrl: ".../blue", selected: false},
]
}
Components:
var myAnswer =
{
props: ["id", "name", "url", "selected"],
template:
`
<div class="answer" v-bind:class="{selected: selected}">
<img class="answer-photo" v-bind:src="url">
<div class="answer-name">{{name}}</div>
</div>
`
};
Vue.component("my-question",
{
props: ["id", "name", "answers"],
components:
{
"my-answer": myAnswer
},
template:
`
<div class ="question">
<div class="question-name">{{name}}</div>
<div class="question-answers">
<my-answer v-for="answer in answers" v-bind:id="answer.id" v-bind:name="answer.name" v-bind:url="answer.photoUrl" v-bind:selected="answer.selected"></my-answer>
</div>
</div>
`
});
When the user selects an answer to a question by clicking on the div, I want the Question model's selectedAnswerId/selectedAnswerName along with the answers selected property to be set accordingly. Therefore, what do I need to add to my components in order to accomplish this two-way binding? I believe it requires input elements and v-model, but I couldn't quite figure it out. Also, I am only one day into Vue.js and have no experience with related frameworks. So if I am doing anything blatantly wrong or against best practice, that would be good to know as well. Thanks in advance!
The answer will handle a click event and emit a (custom) selected-answer event. The question will have its own data item to store the selected answer ID; the answer component's selected prop will be based on that. The question will handle the selected-answer event by setting its selectedId.
var myAnswer = {
props: ["id", "name", "url", "selected"],
template: `
<div class="answer" v-bind:class="{selected: selected}"
#click="setSelection()"
>
<img class="answer-photo" :src="url">
<div class="answer-name">{{name}}</div>
</div>
`,
methods: {
setSelection() {
this.$emit('selected-answer', this.id);
}
}
};
Vue.component("my-question", {
props: ["id", "name", "answers"],
data() {
return {
selectedId: null
};
},
components: {
"my-answer": myAnswer
},
template: `
<div class ="question">
<div class="question-name">{{name}}</div>
<div class="question-answers">
<my-answer v-for="answer in answers"
:id="answer.id" :name="answer.name" :url="answer.photoUrl"
:selected="answer.id === selectedId" #selected-answer="selectAnswer"></my-answer>
</div>
</div>
`,
methods: {
selectAnswer(answerId) {
this.selectedId = answerId;
}
}
});
new Vue({
el: '#app',
data: {
questions: [{
id: 1,
name: "Which color is your favorite?",
answers: [{
id: 1,
name: 'red',
photoUrl: ".../red"
},
{
id: 2,
name: 'green',
photoUrl: ".../green"
},
{
id: 3,
name: 'blue',
photoUrl: ".../blue"
},
]
}]
}
});
.answer {
cursor: pointer;
}
.selected {
background-color: #f0f0f0;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js"></script>
<div id="app">
<my-question v-for="q in questions" :name="q.name" :answers="q.answers"></my-question>
</div>
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