Getting data from component VueJS - vue.js

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
}

Related

VueJs Pass array of object to child component do not refresh on changes

I'm trying to pass an array of object to a childComponent as prop but when I add an object in it, it doesn't render. (Note: I'm working on vuejs 2.6)
I suppose it has a link with the "monitoring" of the items of the array and not the array itself? Stuff is that if I do not pass the prop and use the default value instead, it's working perfectly. I think I'm missing something here. Could someone help me ?
By curiosity is this kind of behavior still stand with vue3js ?
As you can see below:
App.vue:
<template>
<div id="app">
<Card
v-for="user in users"
:key="user.userId"
:userId="user.userId"
:username="getUsernameFromUserId(user.userId)"
:links="getUserLinksFromUserId(user.userId)"
/>
</div>
</template>
<script>
import Card from "./components/Card.vue";
export default {
name: "App",
components: {
Card,
},
data: function () {
return {
users: [
{ userId: 1, name: "Bob" },
{ userId: 2, name: "Alice" },
{ userId: 3, name: "Eliot" },
],
links: [
{ userId: 1, link: "hello->world" },
{ userId: 1, link: "world->!" },
{ userId: 3, link: "hello->back" },
{ userId: 4, link: "hello->you" },
],
};
},
methods: {
getUsernameFromUserId: function (userId) {
return this.users.filter((obj) => obj.userId == userId)?.[0]?.name ?? "Not found";
},
getUserLinksFromUserId: function (userId) {
return this.links.filter((obj) => obj.userId == userId);
},
},
};
</script>
Card.vue
<template>
<div class="card">
<h1>{{ username }}</h1>
<button #click="addLink">Add One link</button><br><br>
<span v-if="links.length == 0">No links</span>
<div class="links">
<Link v-for="link in links" :key="links.indexOf(link)" :link="link"></Link>
</div>
</div>
</template>
<script>
import Link from '../components/Link'
export default {
components:{Link},
props: {
userId: Number,
username: String,
links: { type: Array, default: () => [], required: false },
},
methods:{
addLink: function(){
this.links.push({
userId: this.userId,
link: 'newlink->cool'
});
}
}
}
</script>
Link.vue
<template>
<div>
<span>UserId: {{ this.link.userId }} Link: {{ this.link.link }</span>
</div>
</template>
<script>
export default {
props: {
link: { type: Object, default: () => [], required: false },
},
};
</script>
This is a bad way to work with props
Note: do not focus on Dev Tools too much as it can be "buggy" at times - especially if you use Vue in a wrong way. Focus on your app output
Your Card.vue component is modifying (push) a prop, which is not recommended but it sort of works if the prop is object/Array and you do not replace it, just modify it's content (as you do)
But in your case, the values passed to props are actually generated by a method! The getUserLinksFromUserId method is generating a new array every time it is called, and this array is NOT reactive. So by pushing to it, your component will not re-render and what is worse, parent's links array is not changed at all! (on top of that - if App.vue ever re-renders, it will generate new arrays, pass it to pros and your modified arrys will be forgoten)
So intead of modifying links prop in Card.vue, just emit an event and do the modification in App.vue

Marking a Vue Todo List Item as done

This may have been answered before, but I have been unable to find an answer that works in this specific situation.
I'm new to Vue and trying to build a Todo list in which I can click on a list item when it is complete, changing or adding a class that will change the style of the item.
I guess I don't fully understand how the scopes work together between the main Vue and a component. The code I have right now does absolutely nothing. I have tried moving methods between the main and component, but it always gives me some error.
I guess I'm just looking for some guidance as to how this should be done.
Vue.component('todo-item', {
props: ['todo'],
template: '<li>{{ todo.id + 1 }}. {{ todo.text }}</li>'
})
var app = new Vue({
el: '#app',
data: {
isDone: false,
todos: [
{ id: 0, text: 'This is an item to do today' },
{ id: 1, text: 'This is an item to do today' },
{ id: 2, text: 'This is an item to do today' },
{ id: 3, text: 'This is an item to do today' },
{ id: 4, text: 'This is an item to do today' },
{ id: 5, text: 'This is an item to do today' }
]
},
methods: {
markDone: function(todo) {
console.log(todo)
this.isDone = true
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<div class="content">
<ul class="flex">
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
#click="markDone"
:class="{'done': isDone}"
></todo-item>
</ul>
</div>
</div>
Thanks for the help, guys.
You were getting so close! You simply had your :class="{'done': isDone}" #click="markDone" in the wrong place!
The important thing to remember with components is that each one has to have their own data. In your case, you were binding all todo's to your root Vue instance's done variable. You want to instead bind each one to their own done variable in their own data.
The way you do this is by creating a function version of data that returns individual data for each component. It would look like this:
data () {
return {
isDone: false,
}
},
And then you move the :class="{'done': isDone} from the todo to the li internal to it:
<li :class="{'done': isDone}">{{ todo.id + 1 }}. {{ todo.text }}</li>
Now we have the 'done' class depending on an individual data piece for each individual todo element. All we need to do now is be able to mark it as complete. So we also want each todo component to have it's own method to do so. Add a methods: object to your todo component and move your markDone method there:
methods: {
markDone() {
this.isDone = true;
},
}
Now move the #click="markDone" to the li as well:
<li :class="{'done': isDone}" #click="markDone">{{ todo.id + 1 }}. {{ todo.text }}</li>
And there you go! Now you should be able to create as many todo's as you want, and mark them all complete!
Bonus:
Consider changing your function to toggleDone() { this.isDone = !this.isDone; }, that way you can toggle them back and forth between done and not done!
Full code below :)
Vue.component('todo-item', {
props: ['todo'],
template: `<li :class="{'done': isDone}" #click="toggleDone">{{ todo.id + 1 }}. {{ todo.text }}</li>`,
data () {
return {
isDone: false,
}
},
methods: {
toggleDone() {
this.isDone = !this.isDone;
},
}
})
var app = new Vue({
el: '#app',
data: {
isDone: false,
todos: [
{ id: 0, text: 'This is an item to do today' },
{ id: 1, text: 'This is an item to do today' },
{ id: 2, text: 'This is an item to do today' },
{ id: 3, text: 'This is an item to do today' },
{ id: 4, text: 'This is an item to do today' },
{ id: 5, text: 'This is an item to do today' }
]
},
methods: {
markDone: function(todo) {
console.log(todo)
this.isDone = true
}
}
})
.done {
text-decoration: line-through;
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<div class="content">
<ul class="flex">
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
></todo-item>
</ul>
</div>
</div>
First of all, your current implementation will affect all items in the list when you mark one item as done because you are associating a single isDone property to all the items and when that property becomes true, it will be applied to all the items in your list.
So to fix that, you need to find a way to associate done to each item. And because your item is an object, you just assign a new property done dynamically and set the value to true which means it is marked as done. It will be very confusing to just explain it, so I included a full example using your existing code.
See this JS Fiddle: https://jsfiddle.net/eywraw8t/205021/
In your code, which are handling with the click event is the <li> element, but your are trying to handle it in the root of your component, there're a few ways to solve this
Use native modifier
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
#click.native="markDone"
:class="{'done': isDone}"
>
</todo-item>
You can find more info here
https://v2.vuejs.org/v2/guide/migration.html#Listening-for-Native-Events-on-Components-with-v-on-changed
Emit from component the click event
Vue.component('todo-item', {
props: ['todo'],
template: '<li #click="click()">{{ todo.id + 1 }}. {{ todo.text }}</li>',
methods: {
click () {
this.$emit('click')
}
}
})
By the way, in your current code once you click in one todo all the todos will be "marked as done" as you are using just one variable for all of them.

Vue slot-scope with v-for, data changed but not re-rendered

I have a question while I'm studying vuejs2
I made an example with slot-scope && v-for, but it has an error which I can't understand.
Here is example code
https://jsfiddle.net/eywraw8t/6839/
app.vue
<template id="list-template">
<div>
<ul>
<slot name="row" v-for="item in list" v-bind=item />
</ul>
</div>
</template>
<div id="app">
<list-component :list="list">
<li slot="row" slot-scope="item">
{{item.name}}
</li>
</list-component>
</div>
Vue.component('list-component', {
template: '#list-template',
props: {
list: {
type: Array
}
}
});
new Vue({
el: '#app',
data () {
return {
list: [
{id: 1, name: 'Apple'},
{id: 2, name: 'Banana'},
{id: 3, name: 'Cherry'},
{id: 4, name: 'Durian'},
{id: 5, name: 'Eggplant'}
]
}
},
methods: {
h (item) {
item.name = item.name.toUpperCase()
console.log('Changed!')
console.log(item)
}
}
});
Strange thing is, the method 'h' is triggered and then, the console said 'Changed!' and data also changed but, the view is not re-rendered.
What am I missing? I think slot-scoped object data is not referencing the original object data.
What should I do to modify the original data?
Thanks for reading this question.
You are directly trying to modify the value of an item in an array. Due to some limitations vue cannot detect such array modifications.
So update your code to use vm.$set() to make chamges to the array item.
methods: {
h (item) {
let i = this.items.findIndex((it) => it.id === item.id);
this.$set(this.list, i, {...item, name: item.name.toUpperCase()});
console.log(item.id)
}
Here is the updated fiddle

VueTwo Way Data Binding with Nested Components

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>

In vue2 v-for nested component props aren't updated after element is removed in parent

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