Understanding components nesting in VueJS - vue.js

learning VueJS and going through some of the tutorials online including the guide on vuejs.org and I'm having one hell of a time understanding how components can be nested and have them communicating through props.
Simple example seem to be ok but the following code (slightly tweaked but pretty much out of the VueJS guide) is giving me trouble.
I cannot seem to be able to have 'blog-item' nested within 'blog-items.
I would appreciate it if someone could explain the big picture of how components could be nested along with the use the v-for directive.
I have gone through many tutorials and everything seems to work one the component is nested inside the top-level 'app' component that's supplying the data but I cannot seem to be able to translate that into the scenario below.
As a newbie, I could be missing a key concept or totally off the rails understanding Vue :)
hope you can help.
thanks
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Components Basics - from vuejs.org</title>
<!-- development version, includes helpful console warnings -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<!-- This works. I get it. -->
<div id="components-demo">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
<hr>
<!-- This works too and I get it. -->
<div id="blog-post-demo-simple">
<blog-post-simple title="My journey with Vue"></blog-post-simple>
<blog-post-simple title="Blogging with Vue"></blog-post-simple>
<blog-post-simple title="Why Vue is so fun"></blog-post-simple>
</div>
<hr>
<!-- This is where I'm totally confused -->
<!-- How do I structure this to make sure blog-items is binding the 'post' -->
<!-- correctly? What is not clear to me is where the directives should be placed -->
<!-- Vue keeps complainig with the following: -->
<!-- Property or method "posts" is not defined on the instance but referenced during render -->
<blog-items>
<blog-item
v-for="post in posts"
v-bind:key="post.id"
v-bind:post="post">
</blog-item>
</blog-items>
<hr>
</div>
<script>
// Define a new component called button-counter. Cool. No problem here.
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
// This is also clear.
Vue.component('blog-post-simple', {
template:
'<h3>{{title}}</h3>',
props: {
title: {
type: String,
required: true
}
}
})
Vue.component('blog-items', {
data() { return {
posts: [
{ id: 1, title: '1. My journey with Vue' },
{ id: 2, title: '2. Blogging with Vue' },
{ id: 3, title: '3. Why Vue is so fun' }
]
}
}
})
Vue.component('blog-item', {
template:
'<h2>{{post.title}}</h2>',
props: ['post']
})
var app = new Vue({
el: '#app'
})
</script>
</body>
</html>

Remember when you access a property in a template you're fetching that property from the component that uses that template. In this case it's your root #app component. Since that component doesn't have a property or method with the name posts then Vue is going to complain. What you need to do is move that part within the blog-items component's template since that component is holding your posts.
So what you need to do is this ..
<!-- This is where I'm totally confused -->
<!-- How do I structure this to make sure blog-items is binding the 'post' -->
<!-- correctly? What is not clear to me is where the directives should be placed -->
<!-- Vue keeps complainig with the following: -->
<!-- Property or method "posts" is not defined on the instance but referenced during render -->
<blog-items></blog-items>
Vue.component('blog-items', {
template: `
<div>
<blog-item v-for="post in posts" v-bind:key="post.id" v-bind:post="post" />
</div>
`,
data() {
return {
posts: [
{ id: 1, title: '1. My journey with Vue' },
{ id: 2, title: '2. Blogging with Vue' },
{ id: 3, title: '3. Why Vue is so fun' }
]
}
}
})
Otherwise you'll have to resort to using Scoped Slots, which allow you to expose properties/methods from the child component's scope to the parent ..
<blog-items>
<template slot-scope="{ posts }">
<blog-item v-for="post in posts" v-bind:key="post.id" v-bind:post="post">
</blog-item>
</template>
</blog-items>
Vue.component('blog-items', {
template:`
<div>
<slot :posts="posts"></slot>
</div>`,
data() {
return {
posts: [
{ id: 1, title: '1. My journey with Vue' },
{ id: 2, title: '2. Blogging with Vue' },
{ id: 3, title: '3. Why Vue is so fun' }
]
}
}
})
I found this especially helpful in understanding how scoped slots work.

Related

Getting data from component VueJS

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
}

In VueJS using v-for, is this structure possible?

Given a collection of objects in the data of a component:
data: function () {
return [
{ id: 1, name: "foo", br: false },
{ id: 1, name: "bar", br: true },
{ id: 1, name: "baz", br: false }
]
}
...is it possible to render a structure like so...
<div id="1">foo</div>
<div id="2">bar</div><div class="break" />
<div id="3">baz</div>
In a nutshell, I need to have another div conditionally rendered at the same level as the items in the list. If it matters or helps, the individual items in the list are also components. I know how to set up the rest of the data and properties - it's just getting that additional HTML rendered in the list that I need to accomplish.
I want to avoid creating another item in the list and additional component to represent the break. No need to add the overhead of the additional Vue objects for the simple HTML div. This list may have > 100 items and "breaks" and it can add up quickly.
Yes. You should loop through the items like so:
<template v-for="item in items">
<div :id="item.id">
{{ item.name }}
</div>
<div class="break" v-if="item.br">
</div>
</template>
You can do it with a normal v-for and a normal v-if for your optional div
<html>
<head>
<script type = "text/javascript" src = "https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js">
</script>
</head>
<body>
<div id="app">
<div v-for="item in items">
<div :id="item.id">{{item.name}}</div>
<div v-if="item.br" class="break">
</div>
</div>
<script type = "text/javascript">
var vue_det = new Vue({
el: '#app',
data: {
items: [
{ id: 1, name: "foo", br: false },
{ id: 2, name: "bar", br: true },
{ id: 3, name: "baz", br: false }
]}
});
</script>
</body>
</html>
You should not be afraid of 100 divs or around so, a library like Vue is made to manage efficiently thousands of components

How to get Vue to catch event?

Edited to correct unreported syntax error (see comments). It now works as desired.
I'm having trouble getting my event handler to fire in the following Vue code.
As you see, there are two components, posts and post, and a root Vue instance. The button in the post template should fire the remove event, which is captured by the v-on:remove handler in posts which calls posts.deleteItem with the index of the post. Can someone give me a hint what I'm doing wrong?
<!DOCTYPE html>
<html lang="en">
<head>
<title>Posts</title>
<!--link href="../css/bootstrap.css" rel="stylesheet" /-->
<script src="../vue.js"></script>
</head>
<body>
<div id="app">
<posts></posts>
</div>
<script>
window.onload = function() {
// A post
Vue.component('post-item', {
props: ['post'],
data: function() {
return {
editing: false,
_cachedItem: ''
}
},
methods: {
deleteItem(postId) {
debugger
this.$emit('remove', event.target.value);
},
},
template: `
<div v-on:remove="deleteItem">
<li v-show="!editing">
<p v-html="post.text"></p>
<button v-on:click="$emit('remove')">Delete</button>
</li>
</div>
`
})
Vue.component('posts', {
data: function() {
return {
posts: [
{id: 0, text: "Day at beach"},
{id: 1, text: "Carving the canyons"},
{id: 2, text: "Kickin' it"}
],
};
},
methods: {
deleteItem(index) {
debugger
this.posts.splice(index, 1);
}
},
template: `
<div>
<ol>
<post-item
v-for="(post, index) in posts"
v-bind:post="post"
v-bind:key="post.id"
v-on:remove="deleteItem(index)" />
</ol>
</div>
`
});
// Root Vue instance
new Vue({
el: '#app'
});
}
</script>
</body>
</html>
Looks like you're getting a little confused with the event creation and handling.
Events are emitted up to parent components. You don't typically add an event listener within the same component.
All you really need in your post-item component is to emit the remove event with the appropriate data (ie, the post object)
<div>
<li v-show="!editing">
<p v-html="post.text"></p>
<button #click="$emit('remove', post)">Delete</button>
</li>
</div>
Then in your parent component (posts), listen for this event on the post-item component and assign the event handler
<post-item v-for="post in posts" :key="post.id" :post="post" #remove="deleteItem" />
and handle the event with post payload
methods: {
deleteItem (post) {
this.posts.splice(this.posts.indexOf(post), 1)
}
}
The post object emitted by the post-item component should be the very same object passed in to its prop which is why you can directly use this.posts.indexOf(post). There's no need to go searching for matching id properties.

Why v-bind is one-way data binding while v-for can update data from children component in vue.js?

guys. I am reading the book of The Majesty Of Vue.js 2. I am confused with one example in the book.
My question is - why upvote button can modify data of Vue instance which displayed in pre tag while favorite button can not?
It is said that favorite is bound via v-bind directive, which is one way data binding means that children are not able to sync data with parent. But how did story get updated? Two way data binding like v-model?
Here is the code example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello Vue</title>
</head>
<body>
<div v-cloak id="app">
<div class="container">
<h1>Let's hear some stories!</h1>
<ul class="list-group">
<story v-for="story in stories" :story="story" :favorite="favorite"></story>
</ul>
<pre>{{ $data }}</pre>
</div>
</div>
</body>
<template id="story-template">
<li class="list-group-item">
{{ story.writer }} said "{{ story.plot }}"
Story upvotes {{ story.upvotes }}.
<button v-show="!story.voted" #click="upvote" class="btn btn-default">Upvote</button>
<button v-show="!isFavorite" #click="setFavorite" class="btn btn-primary">Favorite</button>
</li>
</template>
<script src="../../vue.js"></script>
<script type="text/javascript">
Vue.component('story', {
template: "#story-template",
props: ['story', 'favorite'],
methods: {
upvote: function () {
this.story.upvotes += 1;
this.story.voted = true;
},
setFavorite: function () {
this.favorite = this.story;
}
},
computed: {
isFavorite: function () {
return this.story === this.favorite
}
}
});
window.app = new Vue({
el: '#app',
data: {
stories: [
{
plot: 'My horse is amazing.',
writer: 'Mr. Weebl',
upvotes: 28,
voted: false
},
{
plot: 'Narwhals invented Shish Kebab.',
writer: 'Mr. Weebl',
upvotes: 8,
voted: false
},
{
plot: 'The dark side of the Force is stronger.',
writer: 'Darth Vader',
upvotes: 49,
voted: false
},
{
plot: 'One does not simply walk into Mordor',
writer: 'Boromir',
upvotes: 74,
voted: false
}
],
favorite: {}
}
})
</script>
</html>
This has to do with how objects work in Javascript. When stored in a variable you sir a reference to that object. So when you pass it around, you're actually only passing the reference. Meaning that altering an object (not overwriting!), alters is on all places.
What happens in your example is that you modify the story object. You alter its keys but do not overwrite the object itself. Seeing the app has the same reference to the story object. The changes are shown.
In the case of the favorite however. You get passed the reference of the favorite object. But as soon as you click the favorite button. It swaps the variable to reference the story, but only locally. The app still holds the old reference. This is because you only pass the reference itself and not the parent object.
This is where state managers like Vuex come in to place.

VueJs: Accessing data from within embedded component using inline-template

I've got this component, which looks something like this
<template>
<form-wrapper
v-show="show"
:request-action="action"
inline-template
v-cloak
>
<form #submit.prevent="submit()" novalidate>
<html-editor
v-model="fields.body"
id="body"
:contents-css="editorStyles"
></html-editor>
</form>
</form-wrapper>
</template>
<script>
export default {
props: {
show: {
type: Boolean,
required: true
},
action: {
type: String,
required: true
},
editorStyles: {
type: String,
required: true
}
}
}
</script>
As you can see the main component (file of which content you can see) has 2 other components embedded: form-wrapper and html-editor - second one being inside of the form-wrapper, which uses inline-template.
The problem I have is that editorStyles is not accessible from within the form-wrapper inline template.
My question is - how can I make this property available within the inline-template of the form-wrapper component (other than adding it as a property of form-wrapper).
So, this does appear to work. Feels a little icky, but there you have it :)
console.clear()
Vue.component("html-editor", {
props:["contentsCss"],
template:`<div>From Parent: {{contentsCss}}</div>`
})
Vue.component("form-wrapper", {
})
new Vue({
el:"#app",
data:{
editorStyles: "Hello World"
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.js"></script>
<div id="app">
<form-wrapper inline-template>
<html-editor :contents-css="$parent.editorStyles"></html-editor>
</form-wrapper>
</div>