I have a single file component like this:
<template>
<div>
<template v-if="offers.length > 3">
View all offers here
</template>
<template v-else-if="offers.length > 1">
<offer v-for="offer in offers" :data="offer"></offer>
</template>
<template v-else-if="offers.length == 1">
<offer :title="The offer" :data="offers[0]"></offer>
</template>
</div>
</template>
Based on the number of offers, I choose how many to render.
Question: How do I efficiently get/count the number of <offer> components? I also need that number to be reactive.
There's no clean way how.
You could count the children of the current instance that are of a specific type. But you would have to call the "recount" logic on update hook (as well as mounted).
Example:
Vue.component('offer', {
name: 'Offer',
template: '<span> offer </span>'
})
new Vue({
el: '#app',
data: {
offers: [1, 2],
offerCount: 0
},
methods: {
updateOfferCount() {
this.offerCount = this.$children.filter(child => child.constructor.options.name === 'Offer').length;
}
},
updated() {
this.updateOfferCount()
},
mounted() {
this.updateOfferCount()
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<div>
<template v-if="offers.length > 3">
View all offers here
</template>
<template v-else-if="offers.length > 1">
<offer v-for="offer in offers" :data="offer"></offer>
</template>
<template v-else-if="offers.length == 1">
<offer :data="offers[0]"></offer>
</template>
</div>
<br>
<button #click="offers.push(123)">Add Offer</button> offerCount: {{ offerCount }}
</div>
I'm answering this based solely on the idea that you want to count instantiations and destructions of Offer components. I'm not sure why you don't just count offers.length. Maybe other things can trigger instantiations.
Have the component emit events on creation and destruction and have the parent track accordingly.
Alternatively (and maybe overkill) you could use Vuex and create a store that the Offer commits to on creation and destruction. This means that you don't have to manually attach #offer-created/destroyed directives every time you put an <offer> in your markup.
Both methods are included in the following example:
const store = new Vuex.Store({
strict: true,
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
}
});
const Offer = {
props: ["data"],
template: "<div>{{data.name}}</div>",
created() {
console.log("Created");
this.$emit("offer-created");
this.$store.commit("increment");
},
destroyed() {
console.log("Destroyed");
this.$emit("offer-destroyed");
this.$store.commit("decrement");
}
};
const app = new Vue({
el: "#app",
store,
components: {
offer: Offer
},
data() {
return {
offers: [],
offerCount: 0
};
},
computed: {
offerCountFromStore() {
return this.$store.state.count;
}
},
methods: {
offerCreated() {
this.offerCount++;
},
offerDestroyed() {
this.offerCount--;
},
addOffer() {
this.offers.push({
name: `Item: ${this.offers.length}`
});
},
removeOffer() {
this.offers.pop();
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.min.js"></script>
<div id="app">
<div>Offer instances: {{offerCount}}</div>
<div>Offer instances (from store): {{offerCountFromStore}}</div>
<div>
<div v-if="offers.length > 3">
View all offers here
</div>
<div v-else-if="offers.length > 1">
<offer #offer-created="offerCreated" #offer-destroyed="offerDestroyed" v-for="offer in offers" :data="offer"></offer>
</div>
<div v-else-if="offers.length == 1">
<offer #offer-created="offerCreated" #offer-destroyed="offerDestroyed" :data="offers[0]"></offer>
</div>
</div>
<div>
<button #click.prevent="addOffer">Add</button>
<button #click.prevent="removeOffer">Remove</button>
</div>
</div>
The problem with trying to use $children is that it is, inherently, not reactive:
The direct child components of the current instance. Note there’s no
order guarantee for $children, and it is not reactive. If you find
yourself trying to use $children for data binding, consider using an
Array and v-for to generate child components, and use the Array as
the source of truth.
Related
developers,
I have a little problem I would like to reuse a component created using Vuejs but all created instances are still linked between each other I mean for example when I click on instance one all other instances are affected I need to separate between each one must be unique on the page
The technologies I'm using
- VueJS 2
- twig
- Symfony
and also **VueMeetingSelector** is a component I would like to use it
my code
<br>
**app2.vue**
<template>
<div id="app2">
<div v-for="(item,index) in items" class="listing-item" style="width: 100%">
<span style="display: inline-block;width: 65%;">
<vue-meeting-selector
:id="someth"
className="simple-example__meeting-selector"
v-model="meeting"
:date="date"
:loading="loading"
:class-names="classNames"
:meetings-days="meetingsDays"
#next-date="nextDate"
#previous-date="previousDate"
/>
<p>meeting Selected: {{ meeting ? meeting : "No Meeting selected" }}</p>
</span>
</div>
</div>
</template>
<script>
import VueMeetingSelector from 'vue-meeting-selector';
import axios from "axios"
export default {
name: "App2",
components: {
VueMeetingSelector,
},
data() {
return {
date: new Date(),
meetingsDays: [],
meeting: {date: "2021-01-15T08:00:00.000Z"},
loading: true,
nbDaysToDisplay: 1,
multi:true,
idDocteur: 0,
items:5
};
},
methods: {
// #click on button-right
async nextDate()
{
axios
.get('https://127.0.0.1:8000/adsApi')
.then(response => (this.meetingsDays = response.data ))
console.log("nextDate");
},
// #click on button-left
async previousDate() {
console.log("previousDate");
},
},
async created() {
this.meetingsDays = [
{
"date": "2021-11-22T06:00:00.000000Z",
"slots": [
{ "date": "2021-11-22T06:00:00.000000Z" },
{ "date": "2021-11-22T06:30:00.000000Z" },
{ "date": "2021-11-22T06:00:00.000000Z" },
{ "date": "2021-11-22T06:30:00.000000Z" },
{ "date": "2021-11-22T06:00:00.000000Z" },
{ "date": "2021-11-22T06:30:00.000000Z" },
{ "date": "2021-11-22T06:00:00.000000Z" },
{ "date": "2021-11-22T06:30:00.000000Z" }
]
}
];
this.loading = false;
},
};
</script>
<br>
**app.js**
<pre>
import Vue from "vue";
import App2 from "./js/App2";
new Vue({
el: '#app2',
render: h => h(App2)
});
</pre>
<br>
**test.twig.html**
<br>
<div id="wrapper">
<!-- Content-->
<div class="content">
<!--section -->
hello this is a test page
<div id="app2">
<App2></App2>
</div>
</div>
</div>
<script>
import App2 from "../assets/js/App2";
export default {
components: {App2}
}
</script>
that's because all your components use the same values, you don't even use item, index vars anywhere in your loop.
Give them different meeting objects.
Your v-model value to every component is the same. They should be different. Try this.
<vue-meeting-selector
className="simple-example__meeting-selector"
v-model="meeting[index]"
:date="date"
:loading="loading"
:class-names="classNames"
:meetings-days="meetingsDays"
:key="index"
#next-date="nextDate"
#previous-date="previousDate"
/>
And
data() {
return {
...
meeting: ["2021-01-15T08:00:00.000Z", "2021-01-16T08:00:00.000Z", "2021-01-17T08:00:00.000Z"],
....
};
},
Please note that your index should be numeric which I am assuming it is.
loops should have a key directive.
<div v-for="(item,index) in items" class="listing-item" style="width: 100%" :key="index">
<span style="display: inline-block;width: 65%;">
<vue-meeting-selector
:id="someth"
className="simple-example__meeting-selector"
v-model="meeting"
:date="date"
:loading="loading"
:class-names="classNames"
:meetings-days="meetingsDays"
#next-date="nextDate"
#previous-date="previousDate"
/>
<p>meeting Selected: {{ meeting ? meeting : "No Meeting selected" }}</p>
</span>
</div>
I'm trying to build a simple page builder which has a row elements, its child column elements and the last the component which I need to call. For this I designed and architecture where I'm having the dataset defined to the root component and pushing the data to its child elements via props. So let say I have a root component:
<template>
<div>
<button #click.prevent="addRowField"></button>
<row-element v-if="elements.length" v-for="(row, index) in elements" :key="'row_index_'+index" :attrs="row.attrs" :child_components="row.child_components" :row_index="index"></row-element>
</div>
</template>
<script>
import RowElement from "../../Components/Builder/RowElement";
export default {
name: "edit",
data(){
return{
elements: [],
}
},
created() {
this.listenToEvents();
},
components: {
RowElement,
},
methods:{
addRowField() {
const row_element = {
component: 'row',
attrs: {},
child_components: [
]
}
this.elements.push(row_element)
}
},
}
</script>
Here you can see I've a button where I'm trying to push the element and its elements are being passed to its child elements via props, so this RowElement component is having following code:
<template>
<div>
<column-element v-if="child_components.length" v-for="(column,index) in child_components" :key="'column_index_'+index" :attrs="column.attrs" :child_components="column.child_components" :row_index="row_index" :column_index="index"></column-element>
</div>
<button #click="addColumn"></button>
</template>
<script>
import ColumnElement from "./ColumnElement";
export default {
name: "RowElement",
components: {ColumnElement},
props: {
attrs: Object,
child_components: Array,
row_index: Number
},
methods:{
addColumn(type, index) {
this.selectColumn= false
let column_element = {
component: 'column',
child_components: []
};
let component = {}
//Some logic here then we are emitting event so that it goes to parent element and there it can push the columns
eventBus.$emit('add-columns', {column: column_element, index: index});
}
}
}
</script>
So now I have to listen for event on root page so I'm having:
eventBus.$on('add-columns', (data) => {
if(typeof this.elements[data.index] !== 'undefined')
this.elements[data.index].child_components.push(data.column)
});
Now again I need these data accessible to again ColumnComponent so in columnComponent file I have:
<template>
//some extra div to have extended features
<builder-element
v-if="!loading"
v-for="(item, index) in child_components"
:key="'element_index_'+index" :column_index="column_index"
:element_index="index" class="border bg-white"
:element="item" :row_index="row_index"
>
</builder-element>
</template>
<script>
export default {
name: "ColumnElement",
props: {
attrs: Object,
child_components: Array,
row_index: Number,
column_index: Number
},
}
</script>
And my final BuilderElement
<template>
<div v-if="typeof element.component !== 'undefined'" class="h-10 w-10 mt-1 mb-2 mr-3 cursor-pointer font-bold text-white rounded-lg">
<div>{{element.component}}</div>
<img class="h-10 w-10 mr-3" :src="getDetails(item.component, 'icon')">
</div>
<div v-if="typeof element.component !== 'undefined'" class="flex-col text-left">
<h5 class="text-blue-500 font-bold">{{getDetails(item.component, 'title')}}</h5>
<p class="text-xs text-gray-600 mt-1">{{getDetails(item.component, 'desc')}}</p>
</div>
</template>
<script>
export default {
name: "BuilderElement",
data(){
return{
components:[
{id: 1, title:'Row', icon:'/project-assets/images/row.png', desc:'Place content elements inside the row', component_name: 'row'},
//list of rest all the components available
]
}
},
props: {
element: Object,
row_index: Number,
column_index: Number,
element_index: Number,
},
methods:{
addElement(item,index){
//Some logic to find out details
let component_element = {
component: item.component_name,
attrs: {},
child_components: [
]
}
eventBus.$emit('add-component', {component: component_element, row_index: this.row_index, column_index: this.column_index, element_index: this.element_index});
},
getDetails(component, data) {
let index = _.findIndex(this.components, (a) => {
return a.component_name === component;
})
console.log('Component'+ component);
console.log('Index '+index);
if(index > -1) {
let component_details = this.components[index];
return component_details[data];
}
else
return null;
},
},
}
</script>
As you can see I'm again emitting the event named add-component which is again listened in the root component so for this is made following listener:
eventBus.$on('add-component', (data) => {
this.elements[data.row_index].child_components[data.column_index].child_components[data.element_index] = data.component
});
which shows the data set in my vue-devtools but it is not appearing in the builder element:
Images FYR:
This is my root component:
This is my RowComponent:
This is my ColumnComponent:
This is my builder element:
I don't know why this data not getting passed to its child component, I mean last component is not reactive to props, any better idea is really appreciated.
Thanks
The issue is with the way you're setting your data in the addComponent method.
Vue cannot pick up changes when you change an array by directly modifying it's index, something like,
arr[0] = 10
As defined in their change detection guide for array mutations,
Vue wraps an observed array’s mutation methods so they will also
trigger view updates. The wrapped methods are:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
So you can change.
this.elements[data.row_index].child_components[data.column_index].child_components[data.element_index] = data.component
To,
this.elements[data.row_index].child_components[data.column_index].child_components.splice(data.element_index, 1, data.component);
I've an issue in this code
let bus = new Vue();
Vue.component('building-inner', {
props: ['floors', 'queue'],
template: `<div class="building-inner">
<div v-for="(floor, index) in floors" class="building-floor" :ref="'floor' + (floors - index)">
<h3>Floor #{{floors - index }}</h3>
<button type="button" class="up" v-if="index !== floors - floors">up</button>
<button type="button" class="down" v-if="index !== floors - 1">down</button>
</div>
</div>`,
beforeMount(){
bus.$emit('floors', this.$refs);
}
})
Vue.component('elevator', {
data: {
floorRefs: null
},
props: ['floors', 'queue'],
template: `<div class="elevator" ref="elevator">
<button type="button" v-for="(btn, index) in floors" class="elevator-btn" #click="go(index + 1)">{{index + 1}}</button>
</div>`,
beforeMount() {
bus.$on('floors', function(val){
this.floorRefs = val;
console.log(this.floorRefs)
})
},
methods: {
go(index) {
this.$refs.elevator.style.top = this.floorRefs['floor' + index][0].offsetTop + 'px'
}
}
})
new Vue({
el: '#building',
data: {
queue: [],
floors: 5,
current: 0
}
})
<div class="building" id="building">
<elevator v-bind:floors="floors" v-bind:queue="queue"></elevator>
<building-inner v-bind:floors="floors" v-bind:queue="queue"></building-inner>
</div>
I tried to access props inside $refs gets me undefined, why?
You should use a mounted hook to get access to the refs, because on "created" event is just instance created not dom.
https://v2.vuejs.org/v2/guide/instance.html
You should always first consider to use computed property and use style binding instead of using refs.
<template>
<div :style="calculatedStyle" > ... </div>
</template>
<script>
{
//...
computed: {
calculatedStyle (){
top: someCalculation(this.someProp),
left: someCalculation2(this.someProp2),
....
}
}
}
</script>
It's bad practice to pass ref to another component, especially if it's no parent-child relationship.
Refs doc
Computed
I wanted to create a tree view from an XML file, and I did this. However, when I decided to make it more flexible I encountered some problems.
Here are my components:
Vue.component('elname', {
props: ['text'],
template: '<span>{{ text }}</span>'
})
Vue.component('recursive', {
props: ['d', 'liname', 'openclose'],
template: '#recursive',
data: function() {
return {
seen: true
}
}
}
)
and the Vue object looks like this:
var appp = new Vue({
el: '#here',
data: function(){
return {
friends: '',
}
},
beforeMount() {
parser = new DOMParser();
var response = "<scope><friend><name>Alex</name><hobbies><h>music</h><h>salsa</h></hobbies></friend><friend><name>Natasha</name><hobbies><h>hiking</h></hobbies></friend></scope>";
xml = parser.parseFromString(response, 'text/xml');
children = xml.getElementsByTagName('scope')[0];
this.friends = children;
}
})
I have this variable seen in recursive component
Vue.component('recursive', {
props: ['d', 'liname', 'openclose'],
template: '#recursive',
data: function() {
return {
seen: true // <-- here it is
}
}
}
)
It must change its value #click event to hide a nested list (please, see the JSfiddle), but when it changes it updates its value IN SEVERAL components.
How to make its value be updated only in a particular component?
Here is a template:
<div id="here">
<recursive :d="friends" openclose="[-]"></recursive>
</div>
<template id="recursive">
<div>
<ul v-if="d.children.length != 0">
<li v-for="n in d.childNodes" #click="seen = !seen">
<elname :text="n.tagName"></elname>
{{ openclose }}
{{seen}} <!-- it is just for testing purposes to illustrate how seen var changes -->
<recursive :d="n" openclose="[-]"></recursive>
</li>
</ul>
<ul v-else>
<elname :text="d.textContent"></elname>
</ul>
</div>
</template>
You have two issues:
You need to use click.stop so that the click event doesn't propagate to parents
You need a component inside your recursive to handle the toggling
Vue.component('elname', {
props: ['text'],
template: '<span>{{ text }}</span>'
});
Vue.component('recursive', {
props: ['d', 'openclose'],
template: '#recursive',
components: {
toggler: {
data() {
return {
seen: true
}
},
methods: {
toggle() {
this.seen = !this.seen;
}
}
}
}
});
var appp = new Vue({
el: '#here',
data: function() {
return {
friends: '',
}
},
beforeMount() {
parser = new DOMParser();
var response = "<scope><friend><name>Alex</name><hobbies><h>music</h><h>salsa</h></hobbies></friend><friend><name>Natasha</name><hobbies><h>hiking</h></hobbies></friend></scope>";
xml = parser.parseFromString(response, 'text/xml');
children = xml.getElementsByTagName('scope')[0];
this.friends = children;
}
})
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.min.js" integrity="sha256-Ab5a6BPGk8Sg3mpdlsHzH6khPkniIWsvEuz8Fv/s9X8=" crossorigin="anonymous"></script>
<div id="here">
<recursive :d="friends" openclose="[-]"></recursive>
</div>
<template id="recursive">
<div>
<ul v-if="d.children.length != 0">
<li is="toggler" v-for="n in d.childNodes" inline-template>
<div #click.stop="toggle">
<elname :text="n.tagName"></elname>
{{ openclose }}
<recursive v-if="seen" :d="n" openclose="[-]"></recursive>
</div>
</li>
</ul>
<ul v-else>
<elname :text="d.textContent"></elname>
</ul>
</div>
</template>
Currently you have 1 seen variable on an element, which controls the state for all child-elements. So a click on any child will change the seen value in the parent and show/hide all children of this parent.
Solution 1
Change the type of your seen variable to an array - with the same length as the children array. And change your handler to #click="seen[i] = !seen[i]"
Solution 2
Move the click listener to the children. So put #click="seen = !seen" on your outermost div in the template and render the whole list only on v-if="d.children.length && seen"
Vue.component( 'recursive-list', {
props: ["d"],
data: () => ({ expand: true }),
template: `<div style="margin: 5px">
<div v-if="Array.isArray(d)"
style="border: 1px solid black">
<button #click="expand = !expand">Show/Hide</button>
<template v-show="expand">
<recursive-list v-for="e in d" :d="e" />
</template>
<p v-show="!expand">...</p>
</div>
<p v-else>{{d}}</p>
</div>`
} )
new Vue({
el: '#main',
data: { d: ["Text", ["a","b","c"],[[1,2,3],[4,5,6],[7,8]]]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.js"></script>
<div id='main'>
<h3>List:</h3>
<recursive-list :d="d"></recursive-list>
</div>
I've some modifications on your structure, maybe it's not exactly what you need but I think will became more clear.
<template id="tree">
<div>
<ul v-for="(tree, k, idx) in tree.childNodes">
<node :tree="tree" :idx="idx"></node>
</ul>
</div>
</template>
<template id="node">
<li>
<div v-if="tree.childNodes.length">
<span #click="seen = !seen">{{ tree.tagName }}</span>
<span>{{ seen }}</span>
<ul v-for="(node, k, id) in tree.childNodes">
<node :tree="node" :idx="id"></node>
</ul>
</div>
<div v-else>{{ tree.textContent }}</div>
</li>
</template>
https://jsfiddle.net/jonataswalker/Lw52t2dv/
After tried many variations, I don't know how to properly style a component's slot or partial code within <template></template> section.
Is there a way to check if props <counter :recent="true"></counter> from parent level exists, so in a Counter.vue in section <template></template> i would show a special html markup for it ?
=== UPDATED ===
Vue.component('counter', {
template: `
<span class="counter" :number="21" v-text="number">
<span v-if="recent">
since VARTIME
</span>
</span>
`,
data: function(){
return{
count: this.number + 1
}
},
props: {
number: Number,
recent: {
type: Boolean,
default: false
}
},
computed: {
},
created(){
if( this.recent === true ){
console.log('mounted recent true');
}
}
});
new Vue({
el: "#app",
data: {
count: ''
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.min.js"></script>
<div id="app">
<counter :number="20" :recent="true"></counter>
</div>
Here the default value for the recent will be false and if the recent is passed from the parent it will get set in the child.
Just use the detailed props definition as mentioned here.
Remove the v-text="number" as it overrides the internal content of the span and therefore the v-if will never executes.
This is a working example
Vue.component('counter', {
template: `
<span class="counter" :number="21">
<span v-if="recent"> since VARTIME </span>
</span>
`,
data: function() {
return {
count: this.number + 1
}
},
props: {
number: Number,
recent: {
type: Boolean,
default: false
}
},
computed: {},
created() {
if ( this.recent === true ) {
console.log('mounted recent true');
}
}
});
new Vue({
el: "#app",
data: {
count: ''
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.min.js"></script>
<div id="app">
<counter :number="20" :recent="true"></counter>
</div>
You should add a condition class binding, that you eventually style from css/sass/stylus/less.
This can be done as follows:
<template>
<span class="counter" v-text="count" :class="{ cssClassName: recent}">
<slot></slot>
<span v-if="recent">
since VAR_DATETIME <i class="fa fa-refresh" #click="updateList"></i>
</span>
</span>
</template>
Notice that vuejs will automatically combine multiple class declarations on the same element without problems, as described in the manual.