The official doc says the this.$children is not reactive,
The direct child components of the current instance. Note there’s no order guarantee for $children, and it is not reactive...
hence, any changes should not trigger any re-renders. [this.$children api is removed from vuejs v3, hence it works only in v2.x.]
I found this interesting... https://codepen.io/tatimblin/pen/oWKdjR
The code in sandbox above is a demonstration of tab UI implemented using slot & this.$childen api.
Initially tabs component is holding a reference to the this.$children Array, here's a log of that:
Interesting part is, the isActive prop of tab is being changed using that Array, yet it's reflected in each component, resulting in re-render..
I'm not sure what's happening here.. maybe I'm missing something.
template:
<div id="root" class="container">
<tabs>
<tab name="Services" :selected="true">
<h1>What we do</h1>
</tab>
<tab name="Pricing">
<h1>How much we do it for</h1>
</tab>
<tab name="About Us">
<h1>Why we do it</h1>
</tab>
</tabs>
</div>
JS:
Vue.component('tabs', {
template: `
<div>
<div class="tabs">
<ul>
<li v-for="tab in tabs" :class="{ 'is-active': tab.isActive }">
<a :href="tab.href" #click="selectTab(tab)">{{ tab.name }}</a>
</li>
</ul>
</div>
<div class="tabs-details">
<slot></slot>
</div>
</div>
`,
data() {
return {tabs: [] };
},
created() {
this.tabs = this.$children;
},
methods: {
selectTab(selectedTab) {
this.tabs.forEach(tab => {
tab.isActive = (tab.name == selectedTab.name);
// this is how the isActive prop is changed, using this.$children
});
}
}
});
Vue.component('tab', {
template: `
<div v-show="isActive"><slot></slot></div>
`,
props: {
name: { required: true },
selected: { default: false}
},
data() {
return {
isActive: false
};
},
computed: {
href() {
return '#' + this.name.toLowerCase().replace(/ /g, '-');
}
},
mounted() {
this.isActive = this.selected;
}
});
new Vue({
el: '#root'
});
The children are reactive because they are Vue components themselves, having the full power of reactivity already, apart from the tabs parent.
The doc quote means content that would otherwise not be reactive on its own. This distinction could maybe be a little clearer. On the other hand, tabs would not work properly if the slot content was anything other than a component with an isActive property, which forms a tight coupling. It wouldn't work properly with raw HTML slot content such as:
<tabs>
<div>
Hi, I'm not a component
</div>
</tabs>
Related
I am new to vue and a bit unaware of concepts.
I am trying to implement a reusable tab that I found on the internet and also using CDN instead of CLI for certain reasons. My vue version is v3.2.36
I am mostly using react but also curious about vuejs and wanted to learn more however instead of reading documents I like to dive into code and try to implement things myself by reading online, but mostly I saw people writing solutions on v2 instead of v3
Also would you please guide me if any of my code have better practices?
my tabs component:
export default {
data() {
return { tabs: [] };
},
mounted() {
this.tabs = this.$slots.default();
},
methods: {
selectTab(selectedTab) {
this.tabs.forEach(tab => {
console.log(tab)
tab.isActive = (tab.props.name == selectedTab.props.name);
tab.test = "test aktif"
});
}
},
template: `
<div>
<div class="tabs">
<ul>
<li v-for="tab in tabs" :class="{ 'is-active': tab.isActive }">
<a :href="tab.href" #click="selectTab(tab)">{{ tab.props.name }}</a>
</li>
</ul>
</div>
<div class="tabs-details">
<slot></slot>
</div>
</div>
`
}
my tab component
export default {
props: {
name: { required: true },
selected: { default: false },
test: ""
},
data() {
return {
isActive: false
};
},
computed: {
href() {
return '#' + this.name.toLowerCase().replace(/ /g, '-');
}
},
mounted() {
console.log("test")
this.isActive = this.selected;
},
updated() {
console.log("updated")
},
template: `
<div>{{isActive}}
<div v-show="isActive"><slot></slot></div>
</div>
`
}
my html :
<tabs>
<tab name="Services" :selected="true">
<h1>What we do</h1>
</tab>
<tab name="Pricing">
<h1>How much we do it for</h1>
</tab>
<tab name="About Us">
<h1>Why we do it</h1>
</tab>
</tabs>
the props initialization had some issues
use
test: { default: "" },
instead of test: ""
you can find the fixed version here: https://codesandbox.io/s/old-pond-opqg4v?file=/src/components/Tab.vue
Looping out a number of boxes within the same component in vuejs.
Each box has a button that reveals more text using v-on:click.
The problem is that all the boxes respond to this at the same time.
Is there a way to target the specific button being clicked if there are several buttons in a component?
Is there some way to isolate each button so they all dont activate at once?
<div class="filter-list-area">
<button #click="show =!show"> {{text}} </button>
<ul class="filter-list-item-area">
<li class="filter-list-item " v-for="(items, key) in packages">
<div>
<img class="fit_rating">
</div>
<div class="filter-list-item-info" >
<h3>{{items.partner_university}}</h3>
<p> Match: {{items.match_value}}</p>
<div v-for="(courses, key) in courses">
<transition name="courses">
<div class="courses2" v-show="show">
<p v-if="courses.Pu_Id === items.pu_Id">
{{courses.Course_name}}
</p>
</div>
</transition>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
import testdata from '../App.vue'
export default {
data (){
return{
text: 'Show Courses',
testFilter: 'Sweden',
show: false
}
},
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
testuni: Array,
list: Array,
packages: Array,
courses: Array
},
methods:{
afunction(){
console.log(this.show);
},
changeText(){
if(this.show){
this.text = 'Hide Courses'
}
else{
this.text = "Show Courses"
}
}
},
mounted() {
this.afunction();
},
watch: {
show:
function() {
this.afunction()
this.changeText()
}
},
}
EDIT: I've created this before you posted the code example, but you could use same principle:
In your data add showMoreText, which will be used to track if show more data should be shown.
I would agree with #anaximander that you should use child components here
Simple example how to show/hide
<template>
<div>
<div v-for="(box, index) in [1,2,3,4,5]">
<div>
Box {{ box }} <button #click="toggleMore(index)">More</button>
</div>
<div v-show="showMoreText[index]">
More about box {{ box }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
showMoreText: {},
}
},
methods: {
toggleMore(index) {
/*
Adds a property to a reactive object, ensuring the new property is
also reactive, so triggers view updates.
https://vuejs.org/v2/api/#Vue-set
*/
this.$set(this.showMoreText, index, ! this.showMoreText[index])
}
}
}
</script>
This sounds like an ideal situation for a new child component, which will allow each instance of the new component to have its own separate state.
The child components can emit events to the parent, if cross-component communication is necessary.
Scenario
I have a custom button component in Vue:
<custom-button type="link">Save</custom-button>
This is its template:
// custom-button.vue
<template>
<a v-if="type === 'link'" :href="href">
<span class="btn-label"><slot></slot></span>
</a>
<button v-else :type="type">
<span class="btn-label"><slot></slot></span>
</button>
</template>
You can see from the template that it has a type prop. If the type is link, instead of the <button> element, I am using <a>.
Question
You'll notice from the template that I repeated the child component, i.e. <span class="btn-label"><slot></slot></span> on both root components. How do I make it so that I won't have to repeat the child components?
In JSX, it's pretty straightforward. I just have to assign the child component to a variable:
const label = <span class="btn-label">{text}</span>
return (type === 'link')
? <a href={href}>{label}</a>
: <button type={type}>{label}</button>
In this situation, I would probably opt to write the render function directly since the template is small (with or without JSX), but if you want to use a template then you can use the <component> component to dynamically choose what you want to render as that element, like this:
Vue.component('custom-button', {
template: '#custom-button',
props: [
'type',
'href',
],
computed: {
props() {
return this.type === 'link'
? { is: 'a', href: this.href }
: { is: 'button', type: this.type };
},
},
});
new Vue({
el: '#app',
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<custom-button type="button">Button</custom-button>
<custom-button type="submit">Submit</custom-button>
<custom-button type="link" href="http://www.google.com">Link</custom-button>
</div>
<template id="custom-button">
<component v-bind="props">
<span class="btn-label"><slot></slot></span>
</component>
</template>
Well you could always create a locally registered component...
// in custom-button.vue
components : {
'label' : {template : '<span class="btn-label"><slot></slot></span>'}
}
I'm just starting out with VueJS and I was trying to port over a simple jQuery read more plugin I had.
I've got everything working except I don't know how to get access to the contents of the slot. What I would like to do is move some elements passed into the slot to right above the div.readmore__wrapper.
Can this be done simply in the template, or am I going to have to do it some other way?
Here's my component so far...
<template>
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>
<script>
export default {
name: 'read-more',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
}
</script>
You can certainly do what you describe. Manipulating the DOM in a component is typically done in the mounted hook. If you expect the content of the slot to be updated at some point, you might need to do the same thing in the updated hook, although in playing with it, simply having some interpolated content change didn't require it.
new Vue({
el: '#app',
components: {
readMore: {
template: '#read-more-template',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
mounted() {
const readmoreEl = this.$el.querySelector('.readmore__wrapper');
const firstEl = readmoreEl.querySelector('*');
this.$el.insertBefore(firstEl, readmoreEl);
}
}
}
});
.readmore__wrapper {
display: none;
}
.readmore__wrapper.active {
display: block;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id='app'>
Hi there.
<read-more>
<div>First div inside</div>
<div>Another div of content</div>
</read-more>
</div>
<template id="read-more-template">
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>
I'm very new to Vuejs and JS frameworks in general, so bear with me. I'm trying to call a method that resides in my root component from a child component (2 levels deep) by passing it as a prop, but I get the error:
Uncaught TypeError: this.onChange is not a function
at VueComponent._onChange (category.js:8)
at boundFn (vendor.js?okqp5g:361)
at HTMLInputElement.invoker (vendor.js?okqp5g:2179)
I'm not sure if I'm on the right track by assigning the prop to a method inside the child component, but see what you think:
index.js
var app = new Vue({
el: '#app',
data: function () {
return {
categories: [],
articles: []
}
},
methods: {
onChange: function () {
console.log('first one');
return function () {
console.log('second one');
}
}
},
});
The html:
<div id="app">
<sidebar :onChange=onChange :categories=categories></sidebar>
<varticles :articles=articles></varticles>
</div>
sidebar.js:
Vue.component('sidebar', {
props: ['onChange', 'categories'],
methods: {
_onChange: function () {
this.onChange();
}
},
template: `
<div class="sidebar">
<category v-for="item in categories" :onChange="_onChange" v-bind:category="item"></category>
</div>
`
});
category.js:
Vue.component('category', {
props: ['category', 'onChange'],
methods: {
_onChange: function () {
this.onChange();
}
},
template: `
<div class="category">
<h2>{{ category.name }}</h2>
<ul>
<li v-for="option in category.options">
<input v-on:change="_onChange" v-bind:id="option.tid" type="checkbox" v-model="option.checked">
<label v-bind:for="option.tid">{{ option.name }}</label>
</li>
</ul>
</div>
`
});
There's got to be simpler way to do this!
I'd suggest taking a look at this https://v2.vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case. A simplified version of your code is in this fiddle https://jsfiddle.net/z11fe07p/641/
When writing props in your templates declare them without Capital letters.
A prop declared as onChange in your props is equivalent to on-change in your html.
<sidebar :on-change=onChange :categories=categories></sidebar>
Also I would suggest looking at events and non parent-child communication if you want a link between components that are more than 1 level deep. https://v2.vuejs.org/v2/guide/components.html?#Non-Parent-Child-Communication