Are Vue component slots not considered children? - vue.js

Lets say that I have a parent component, Tabs, which takes in child Tab Components as slots.
So the setup is like this:
Tabs (Parent)
<div>
<slot name="tabChild">
</div>
Tab (Child)
<div>
Tab {{name}}
</div>
MyApp
<Tabs>
<Tab slot="tabChild" name="1" ></Tab>
<Tab slot="tabChild" name="1" ></Tab>
</Tabs>
However, in the Tabs (Parent) component, when I try to programmatically access its children, like this:
Tabs Component
mounted(){
let childTabs = this.$children //this is empty??
childTabs = this.$slots //this is correctly the child Tab Components
}
Moreover, in the Tab (Child) component, when I try to access its parent, which I thought was the Tabs component (since they are slotted within it), it is not:
Tab Component
mounted(){
let parentTab = this.$parent //this is MyApp (grandfather), NOT Tabs
}
Why are the tab child components slotted within the greater Tabs component not its children?

Well, $parent will always refer to the "direct" parent/wrapper a particular component is nested inside, so it's not quite reliable when the need to refer to the "root" parent arises.
Here are some good excerpts from the official docs:
In most cases, reaching into the parent makes your application more difficult to debug and understand, especially if you mutate data in the parent. When looking at that component later, it will be very difficult to figure out where that mutation came from. [1]
Use $parent and $children sparingly as they mostly serve as an escape-hatch. Prefer using props and events for parent-child communication. [2]
Implicit parent-child communication: Props and events should be preferred for parent-child component communication, instead of this.$parent or mutating props.
An ideal Vue application is props down, events up. Sticking to this convention makes your components much easier to understand. [3]
Unfortunately, using the $parent property didn’t scale well to more deeply nested components. That’s where dependency injection can be useful, using two new instance options: provide and inject. [4]
Here's a quick example that sort of demonstrates the above recommendations:
const Parent = Vue.extend({
template: `
<ul :style="{ columnCount: colCount }">
<slot></slot>
</ul>
`,
provide() {
return {
biologicalParent: this
}
},
props: ['value', 'colCount', 'message'],
methods: {
sayIt() {
const
num = this.$children.indexOf(this.value) + 1,
message = [
`I am child no. ${num}`,
`among ${this.$children.length} of my siblings`,
`in ${this.colCount} different lineages.`
]
.join(' ');
this.$emit('update:message', message);
}
},
watch: {
value: 'sayIt'
}
});
const Child = Vue.extend({
template: `<li v-text="name" #click="setChild"></li>`,
props: ['name'],
inject: ['biologicalParent'],
methods: {
setChild() {
this.biologicalParent.$emit('input', this);
}
}
});
new Vue({
el: '#demo',
data: () => ({
lineage: 3,
childCount: 10,
message: 'Click on a child for the associated message.',
lastChild: undefined
}),
components: {
Parent,
Child
}
});
input {
width: 4em;
}
li {
cursor: pointer;
}
.message {
background-color: beige;
border: 1px solid orange;
padding: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<table>
<tr>
<td>Child count:</td>
<td><input type="number" v-model="childCount" /></td>
</tr>
<tr>
<td>Lineage:</td>
<td><input type="number" v-model="lineage" /></td>
</tr>
</table>
<parent
v-model="lastChild"
:col-count="lineage"
:message.sync="message">
<child
v-for="(n, i) of Array.apply(null, {length: childCount})"
:key="i"
:name="`child #${i+1}`">
</child>
</parent>
<p class="message">{{ message }}</p>
</div>
The provide options allows us to specify the data or methods we want to provide to descendant components. In the above example, that's the Parent instance.
Notice how the Child index/number is evaluated at runtime (when clicked) instead of compile time (rendering phase).

Related

Vue: All components rerender upon unrelated data property change only if a prop comes from a method that returns an object or array

(Vue 3, options API)
The problem: Components rerender when they shouldn't.
The situation:
Components are called with a prop whose value comes from a method.
The method cannot be replaced with a computed property because we must make operations on the specific item (in a v-for) that will send the value processed for that component.
The method returns an Array. If it returned a primitive such as a String, components wouldn't rerender.
To reproduce: change any parent's data property unrelated to the components (such as showMenu in the example below).
Parent
<template>
<div>
<div id="menu">
<div #click="showMenu = !showMenu">Click Me</div>
<div v-if="showMenu">
Open Console: A change in a property shouldn't rerender child components if they are not within the props. But it does because we call myMethod(chart) within the v-for, and that method returns an array/object.
</div>
</div>
<div v-for="(chart, index) in items" :key="index">
<MyComponent :table="myMethod(chart)" :title="chart.title" />
</div>
</div>
</template>
<script>
import MyComponent from './MyComponent.vue';
export default {
components: {
MyComponent,
},
data: function () {
return {
showMenu: false,
items: [{ value: 1 }, { value: 2 }],
};
},
methods: {
myMethod(item) {
// Remove [brackets] and it doesn't rerender all children
return ['processed' + item.value];
}
}
};
</script>
Child
<template>
<div class="myComponent">
{{ table }}
</div>
</template>
<script>
export default {
props: ['table'],
beforeUpdate() {
console.log('I have been rerendered');
},
};
</script>
<style>
.myComponent {
width: 10em;
height: 4em;
border: solid 2px darkblue;
}
</style>
Here's a Stackblitz that reproduces it https://stackblitz.com/edit/r3gg3v-ocvbkh?file=src/MyComponent.vue
I need components not to rerender. And I don't see why they do.
Thank you!
To avoid this unnecessary rerendering which is the default behavior try to use v-memo directive to rerender the child component unless the items property changes :
<div v-for="(chart, index) in items" :key="index" v-memo="[items]">
<MyComponent :table="myMethod(chart)" :title="chart.title" />
</div>

Share data between two child components Vue js

i am trying to send and render some data from a child component to another child component & both components are rendered using one main component , how can i pass the data between two child components (in my vue app)?
exg
I have two child components A & B , "B" has click add to cart click event and have data of cart items , & i have template for cart item in component "A"
In this situation, as both components share the same parent, it's common to move whatever shared state you need into the parent component and pass to the children as props.
Where components don't shared a direct parent, you can use an event bus (for very small apps) or Vuex. Vuex lets you share state between components in your app without having to pass props down through multiple levels.
There are a lot of ways to achieve this. I am explaining with global even bus concept. Following is an example. Here, child A and child B are communicating through event bus
const eventBus = new Vue ()
Vue.component('ChildB',{
template:`
<div id="child-b">
<h2>Child B</h2>
<pre>data {{ this.$data }}</pre>
<hr/>
</div>`,
data() {
return {
score: 0
}
},
created () {
eventBus.$on('updatingScore', this.updateScore) // 3.Listening
},
methods: {
reRender() {
this.$forceUpdate()
},
updateScore(newValue) {
this.score = newValue
}
}
})
Vue.component('ChildA',{
template:`
<div id="child-a">
<h2>Child A</h2>
<pre>data {{ this.$data }}</pre>
<hr/>
<button #click="changeScore">Change Score</button>
<span>Score: {{ score }}</span>
</div>`,
props: ["score"],
methods: {
changeScore() {
this.score +=200;
eventBus.$emit('updatingScore', this.score+ 200)
}
}
})
Vue.component('ParentA',{
template:`
<div id="parent-a">
<h2>Parent A</h2>
<pre>data {{ this.$data }}</pre>
<hr/>
<child-a :score="score"/>
<child-b/>
</div>`,
data() {
return {
score: 100
}
}
})
Vue.component('GrandParent',{
template:`
<div id="grandparent">
<h2>Grand Parent</h2>
<pre>data {{ this.$data }}</pre>
<hr/>
<parent-a/>
</div>`,
})
new Vue ({
el: '#app',
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<grand-parent/>
</div>
Ideally, you should use Vuex as state management pattern.
But if your application is very simple and you don't need to do those operations often, you can pass data in emit payload from child to parent, and then parent component should pass it to another child via props

VueJS: Why parent's data property not updated when custom event emitted on child fires?

In the following code, codePen demo here
child component emits a custom event changedMsg to parent which changes msg data property on the parent component. Not sure, why changedMsg does not work. It does not modify msg property of parent.
Note: In a single file setup this works, but not in the setup below which is using template tag. Not sure, why?
VueJS
var child = {
template: '#child',
props: ['parentMsg'],
methods: {
changeParentMsg() {
console.log(this.parentMsg)
this.parentMsg = 'Message was changed by CHILD'
this.$emit('changedMsg', this.parentMsg)
}
}
}
new Vue({
el: '#parent',
data() {
return {
msg: 'Hello World'
}
},
components: {
child
},
methods: {
changeMsg() {
this.msg = 'Changed Own Msg'
}
},
})
HTML
<div>
<h4>Parent</h4>
<p>{{ msg }}</p>
<button #click="changeMsg">Change Own Message</button>
<br>
<div class="child">
<h4>Child</h4>
<child :parentMsg="msg" #changedMsg= "msg = $event"></child>
</div>
</div>
</div>
<template id="child">
<div>
<button #click="changeParentMsg">Change Parnets msg</button>
</div>
</template>
CSS
#parent {
background-color: lightblue;
border: 2px solid;
width: 300px;
}
.child {
background-color: lightgray;
border: 2px solid;
margin-top: 20px
}
Thanks
Note: In a single file setup this works, but not in the setup below which is using template tag. Not sure, why?
This is explained in the docs:
Event Names
Unlike components and props, event names will never be used as variable or property names in JavaScript, so there’s no reason to use camelCase or PascalCase. Additionally, v-on event listeners inside DOM templates will be automatically transformed to lowercase (due to HTML’s case-insensitivity), so v-on:myEvent would become v-on:myevent – making myEvent impossible to listen to.
For these reasons, we recommend you always use kebab-case for event names.
So, it's not a good practice to make the logic on the listem event directive, i made this way and worked:
<child :parentMsg="msg" #changed-msg="msgChanged"></child>
</div>
</div>
on the child tag i changed the #changedMsg to kebab-case so now it's #changed-msg and made it call a function instead of do the logic on the child tag, function called msgChanged, so you need to create it on your methods section:
methods: {
changeMsg() {
this.msg = 'Changed Own Msg'
},
msgChanged(param) {
console.log(param)
}
}
hope that helps

How split vue single components template section in to smaller subtemplates

My application is being build on vuejs#2 has multiple forms most of the share same html template with add and reset button. As well as same method, resetForm nullifies the "item" property and resets the form, and create method sends the item to the backend.
<div class="row">
<div class="action">
<button class="btn btn-white" #click="create()">✎ Add</button>
<button class="btn btn-white" #click="resetForm()">❌ Reset</button>
</div>
</div>
I can share methods via mixins with each component but I can't share "template partial" same way. How to you approach such scenario?
I tried to create component create-reset-buttons, but I have no way to trigger parent method as each component encapsulates its functionality and does not allow to modify props from the child. Which need to be done in order to reset the parent form.
Components are not allowed to modify the props, but there are ways child can communicate to parent as explained here in detail.
In Vue.js, the parent-child component relationship can be summarized as props down, events up. The parent passes data down to the child via props, and the child sends messages to the parent via events. Let’s see how they work next.
How to pass props
Following is the code to pass props to chile element:
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>
How to emit event
HTML:
<div id="counter-event-example">
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
JS:
Vue.component('button-counter', {
template: '<button v-on:click="increment">{{ counter }}</button>',
data: function () {
return {
counter: 0
}
},
methods: {
increment: function () {
this.counter += 1
this.$emit('increment')
}
},
})
new Vue({
el: '#counter-event-example',
data: {
total: 0
},
methods: {
incrementTotal: function () {
this.total += 1
}
}
})

how to expose property on component in vue.js 2.0

I am thinking how to implement validation on vue.js component.
The initial idea is that component validates and return an error code, like: "require", "min", "max" etc. Another component will display full text message according to this error code.
Because error message might not always display inside component's template. I need two separated components.
pseudo code is like this.
<div>
<mycomponent ref="salary" errcode="ErrorCode"></mycomponent>
</div>
.....
<div>
<errormessage watchcomponent="salary" message="salaryErrorMessages"></errormessage>
</div>
salaryErrorMessages is a hash of codes and messages. for example:
{"require":"please enter salary",
"min": "minimum salary value is 10000"}
Vue has ref attribute on component, . I don't know if I can use ref to reference a component in attribute.
Other solutions I considered:
add an error object in model of the page, and pass into using :sync binding. can monitor same object.
This requires to declare error messages in model.
If I consider the requirement that page also needs to know if there is an error before post back. a global error object might be necessary.
use event bus or Vuex.
This seems official solution, but I don't know .
When a page has multiple instances of , they will trigger same event. all instances will monitor same event.
You should definitely use Vuex. Not only it solves your problem, but it gives you huge scalability in your project.
You're working literally backwards. Vue is about passing data from parent to child, not exposing child properties to parent components.
add an error object in model of the page, and pass into using :sync binding. can monitor same object. This requires to declare error messages in model.
sync is gone from Vue v2, but the idea is fundamentally sort of correct: a parent component holds one object, child components get passed chunks of it as props, parent object automagically gets updated in the parent when it is changed in a child. It doesn't have to be the root component.
use event bus or Vuex. This seems official solution, but I don't know .
Vuex is pretty much always the Correct solution if you have an application that needs to manage a lot of state-related shenanigans.
When a page has multiple instances of , they will trigger same event. all instances will monitor same event.
Vue will very frequently pollute your console with warnings that data must be a function. This is why!
Here is an abridged version really shameful hackjob I inflicted on innocen made for an acquaintance recently:
In my defense, putting things in __proto__ is a very quick way to make them non-enumerable.
Vue.component('error', {
template: '#error',
props: ['condition', 'errorMessage']
})
Vue.component('comp', {
template: '#comp',
props: ['errorMessage', 'model'],
})
Vue.component('app', {
template: '#app',
data() {
return {
models: {
nameForm: {
firstName: '',
lastName: '',
__proto__: {
validator: function(value) {
return _(value).every(x => x.length > 2)
}
}
},
otherForm: {
notVeryInterestingField: 'There are words here',
veryImportantField: 0,
__proto__: {
validator: function(value) {
return value.veryImportantField > 20
}
}
},
__proto__: {
validator: function(value) {
return _(value).every(x => x.validator(x))
}
}
}
}
}
})
const vm = new Vue({
el: '#root'
})
.error {
background: orange
}
.alright {
background: mediumaquamarine
}
section > div, pre {
padding: 6px;
}
section {
flex: 1;
}
#root {
display: flex;
flex-direction: column;
}
<script src="https://unpkg.com/vue#2.1.6/dist/vue"></script>
<script src="https://unpkg.com/lodash"></script>
<template id="comp">
<div :class="[model.validator(model) ? 'alright' : 'error']" style="display: flex; border-bottom: 2px solid rgba(0,0,0,0.4)">
<section>
<div v-for="(field, fieldName) in model">
{{_.startCase(fieldName)}}:
<input v-model="model[fieldName]">
<br>
</div>
<error :condition="!model.validator(model)" :error-message="errorMessage"></error>
</section>
<section>
<pre>props: {{$options.propsData}}</pre>
</section>
</div>
</template>
<template id="error">
<div v-if="condition">
{{ errorMessage || 'Oh no! An error!' }}
</div>
</template>
<template id="app">
<div :class="[models.validator(models) ? 'alright' : 'error']">
<comp :model="model" error-message="Mistakes were made." v-for="model in models"></comp>
<pre>data: {{$data}}</pre>
</div>
</template>
<div id="root">
<app></app>
</div>