Is there any way to do this kind of conditional nesting with Vue?
(Apparently <component is="template"> outputs a non parsed <template> tag into the DOM but does not render anything)
<component :is="condition ? 'div' : 'template'">
<!-- 2 elements here -->
</component>
The purpose is to avoid unneeded markup or repeating my 2 elements code twice in a v-if v-else.
Also having a sub component with the 2 elements would not help as Vue components need only 1 root, so a wrapper would be needed there too.
What I am looking for is an equivalent to:
<div v-if="condition">
<span>element 1</span>
<span>element 2</span>
</div>
<template v-else>
<span>element 1</span>
<span>element 2</span>
</template>
but without rewriting twice the span elements.
(Also posted it on Vue.js forum https://forum.vuejs.org/t/how-to-conditionally-nest-elements/95384)
Thanks for any help!
Using Vue 2:
There is no straight forward solution to this using Vue 2, but you can use Functional Components for this purpose, as functional components do not have the single-root limitation.
So first, create a my-span functional component which will be rendered in DOM with multiple nodes like:
<span>element 1</span>
<span>element 2</span>
using:
Vue.component('my-span', {
functional: true,
render: function (createElement, context) {
const span1 = createElement('span', 'element 1');
const span2 = createElement('span', 'element 2');
return [span1, span2]
},
})
You can create as many nodes you want, with any element you want and simply return that as an array.
In Vue 2.5.0+, if you are using single-file components, template-based functional components can be declared with:
<template functional>
</template>
Next, create a component just to wrap the <my-span> above like:
Vue.component('my-div', {
template: '<div><my-span /></div>'
})
Then using Vue’s <component> element with the is special attribute, we can dynamically switch between the <my-div> and <my-span> components like:
<component :is="condition ? 'my-div' : 'my-span'"></component>
This will result in the desired behaviour you are looking for. You can also inspect the rendered DOM to verify this.
Working Demo:
Vue.component('my-span', {
functional: true,
render: function (createElement, context) {
const span1 = createElement('span', 'element 1');
const span2 = createElement('span', 'element 2');
return [span1, span2]
},
})
Vue.component('my-div', {
template: '<div><my-span /></div>'
})
new Vue({
el: "#myApp",
data: {
condition: true
},
methods: {
toggle() {
this.condition = !this.condition;
}
}
})
#myApp{padding:20px}
#myApp div{padding:10px;border:2px solid #eee}
#myApp span{padding:5px;margin:5px;display:inline-flex}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="myApp">
<button #click="toggle">Toggle</button><br>
<component :is="condition ? 'my-div' : 'my-span'"></component>
</div>
Using Vue 3:
In Vue 3, it would ve very easy to implement as we can have multiple root nodes in Vue 3, as you can see MySpan component has a template with multiple spans:
const MySpan = { template: '<span>element 1</span><span>element 2</span>' };
Working Demo:
const { createApp, ref } = Vue;
const MySpan = { template: '<span>element 1</span><span>element 2</span>' };
const MyDiv = {
components: { MySpan },
template: '<div><my-span /></div>'
};
const App = {
components: { MyDiv, MySpan },
setup() {
const condition = ref(true);
const toggle = () => {
condition.value = !condition.value;
};
return { condition, toggle };
}
};
createApp(App).mount("#myApp");
#myApp{padding:20px}
#myApp div{padding:10px;border:2px solid #eee}
#myApp span{padding:5px;margin:5px;display:inline-flex}
<script src="//unpkg.com/vue#next"></script>
<div id="myApp">
<button #click="toggle">Toggle</button><br>
<component :is="condition ? 'my-div' : 'my-span'"></component>
</div>
Related
There is a similar question for Vue2 and recommendation is to use $options.
But it seems that it does't work for Vue 3.
First of all, Vue 3 documentation say, that $options is read only.
So when I am trying to initialize tooltip in instance when component mounted, I get very strange behavior, when tooltips are shown from last created component, so it seem that $options are somehow "global" ?
When put tooptip to data everything works fine, but obviously tooltip should not be reactive and I'd like to put it outside the data.
<template>
<i
:class="['bi ', icon, hover && 'text-primary']"
class="bg-body"
#mouseover="hover = true; $options.tooltip.show();"
#mouseleave="hover = false; $options.tooltip.hide();"
#click="$options.tooltip.hide();"
style="cursor: pointer"
:title="title"
ref="icon"
/>
</template>
<script>
import {Tooltip} from "bootstrap";
export default {
props: ["icon", "title"],
tooltip: null,
data() {
return {
hover: false
}
},
mounted() {
this.$options.tooltip = new Tooltip(this.$refs.icon,{
placement: 'bottom',
trigger: 'manual',
title: this.title || ''
});
},
}
</script>
You can attach non-reactive properties directly to the component instance in the mounted() hook:
<script>
export default {
// tooltip: null,
mounted() {
// this.$options.tooltip = new Tooltip(...)
this.tooltip = new Tooltip(...)
},
}
</script>
<template>
<!-- BEFORE -->
<i
#mouseover="hover = true; $options.tooltip.show();"
#mouseleave="hover = false; $options.tooltip.hide();"
#click="$options.tooltip.hide();"
ref="icon"
/>
<!-- AFTER -->
<i
#mouseover="hover = true; tooltip.show();"
#mouseleave="hover = false; tooltip.hide();"
#click="tooltip.hide();"
ref="icon"
/>
</template>
demo
I'm trying to make a functional component that renders a component or another depending on a prop.
One of the output has to be a <v-select> component, and I want to pass it down all its slots / props, like if we called it directly.
<custom-component :loading="loading">
<template #loading>
<span>Loading...</span>
</template>
</custom-component>
<!-- Should renders like this (sometimes) -->
<v-select :loading="loading">
<template #loading>
<span>Loading...</span>
</template>
</v-select>
But I can't find a way to include the slots given to my functional component to the I'm rendering without adding a wrapper around them:
render (h: CreateElement, context: RenderContext) {
// Removed some logic here for clarity
return h(
'v-select',
{
props: context.props,
attrs: context.data.attrs,
on: context.listeners,
},
[
// I use the `slot` option to tell in which slot I want to render this.
// But it forces me to add a div wrapper...
h('div', { slot: 'loading' }, context.slots()['loading'])
],
)
}
I can't use the scopedSlots option since this slot (for example) has no slot props, so the function is never called.
return h(
'v-select',
{
props: context.props,
attrs: context.data.attrs,
on: context.listeners,
scopedSlots: {
loading(props) {
// Never called because no props it passed to that slot
return context.slots()['loading']
}
}
},
Is there any way to pass down the slots to the component i'm rendering without adding them a wrapper element?
I found out it's totally valid to use the createElement function to render a <template> tag, the same used to determine which slot we are on.
So using it like this fixes my problem:
render (h: CreateElement, context: RenderContext) {
// Removed some logic here for clarity
return h(
'v-select',
{
props: context.props,
attrs: context.data.attrs,
on: context.listeners,
},
[
// I use the `slot` option to tell in which slot I want to render this.
// <template> vue pseudo element that won't be actually rendered in the end.
h('template', { slot: 'loading' }, context.slots()['loading'])
],
)
}
In Vue 3 it's a way easier.
Check the docs Renderless Components (or playground)
An example from the docs:
App.vue
<script setup>
import MouseTracker from './MouseTracker.vue'
</script>
<template>
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
</template>
MouseTracker.vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>
<slot :x="x" :y="y"/>
</template>
Just to mention, you can easily override CSS of the component in the slot as well.
This is my child element
<template lang="html">
<div class="row">
<div class="col-lg-8 col-md-8 col-sm-12 col-xs-12">
<bar-chart :v-if="this.barChartReadyToBeRendered" :chart-data='null' :height="340"></bar-chart>
</div>
<div class="flex-col-docs col-lg-3">
<div class="column" style="height: 150px">
<div class="col">
<q-select dark stack-label="Show Targets" class="select-notification"
v-model="selectTargetNotification"
:options="this.getTargetChangeOptions"
/>
</div>
<div class="col">
<q-select dark stack-label="Agency" class="select-notification"
v-model="selectOrgNotification"
:options="this.getOrganisationOptions"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import BarChart from '../../components/BarChart'
export default {
components: {
BarChart
},
.
.
/* Other code */
mounted () {
console.log('OUTSIDE MOUNTED')
this.$nextTick(() => {
console.log(this.$el)
let ctx = document.getElementById('bar-chart')
console.log('WWWWWWWWWWWWWWWWWW')
console.log(ctx)
console.log(this.$el)
this.createChart('bar-chart')
})
}
</script>
The bar chart chartjs is
<script>
import { Bar, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins
export default {
extends: Bar,
mixins: [reactiveProp],
props: ['options'],
mounted () {
this.renderChart(this.chartData, this.options)
}
}
</script>
<style>
</style>
In my parent element, the template is
<template>
<q-page padding class="row justify-center">
<div style="width: 80vw; max-width: 100vw;">
<div class="flex-row-docs">
<div class="doc-container">
<q-list no-border>
<div class="row justify-start">
<div class="col-6">
<target-changes-agency></target-changes-agency>
</div>
</div>
<div class="q-mb-md q-mt-md q-headline">Full coverage</div>
<span v-if="!isNewsByIdLoaded" class="row justify-center">
<q-spinner-mat :size="36" style="color: #027be3ff; text-align: justify; margin: 2rem;" />
</span>
<div class="row">
<article-cluster :isNewsByIdLoaded="isNewsByIdLoaded"></article-cluster>
</div>
</q-list>
</div>
</div>
</div>
</q-page>
</template>
I am expecting to console.log(ctx) and console.log(this.$el), however the output of those 2 is null and <!-- --> respectively.
I thought mounted and this.$nextTick() will allow me to have access to the DOM. What am i missing here? please help thank you
Why are you assuming that document.getElementById('bar-chart') would return any element? There is no element with that ID being created. What you're rather looking for is document.getElementsByTagName('bar-chart'), but that will also yield no result, because Vue does not internally create Web Components, but inserts the component's root element in place instead. So, what you can do is give your bar-chart component an id attribute, which will be passed to the root element automatically.
The next issue is that your bar-chart component is only visible when the condition in v-if is truthy. That's probably not the case when the component is first being loaded. In this working minimal example, I simply set v-if="false".
const { Bar, mixins } = VueChartJs
const { reactiveProp } = mixins
const BarChart = Vue.component('bar-chart', {
extends: Bar,
mixins: [reactiveProp],
props: ['options'],
mounted () {
//this.renderChart(this.chartData, this.options)
this.$nextTick(() => {
console.log('mounted bar-chart component:');
console.log(this.$el)
});
}
});
Vue.component('example-component', {
template: `<div><bar-chart v-if="false" id="barchart" chart-data="null" height="340"></bar-chart></div>`,
components: [BarChart],
mounted () {
this.$nextTick(() => {
console.log('mounted child component:');
let ctx = document.getElementById('barchart')
console.log(ctx)
console.log(this.$el)
})
}
});
// create a new Vue instance and mount it to our div element above with the id of app
var vm = new Vue({
el: '#app'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script src="https://unpkg.com/vue-chartjs#3.5.0/dist/vue-chartjs.min.js"></script>
<div id="app">
<example-component></example-component>
</div>
(The stack snippet console actually hides the <!-- -->, but you can see it in this codepen. Vue automatically inserts this empty HTML comment as a placeholder for a component that is not currently being displayed.)
The output is actually expected, as the bar-chart component is not being rendered, therefore this.$el (referring to the child component, not the bar-chart component) is empty.
Now here ist the same snippet with v-if="true" on the bar-chart component:
const { Bar, mixins } = VueChartJs
const { reactiveProp } = mixins
const BarChart = Vue.component('bar-chart', {
extends: Bar,
mixins: [reactiveProp],
props: ['options'],
mounted () {
//this.renderChart(this.chartData, this.options)
this.$nextTick(() => {
console.log('mounted bar-chart component:');
console.log(this.$el)
});
}
});
Vue.component('example-component', {
template: `<div><bar-chart v-if="true" id="barchart" chart-data="null" height="340"></bar-chart></div>`,
components: [BarChart],
mounted () {
this.$nextTick(() => {
console.log('mounted child component:');
let ctx = document.getElementById('barchart')
console.log(ctx)
console.log(this.$el)
})
}
});
// create a new Vue instance and mount it to our div element above with the id of app
var vm = new Vue({
el: '#app'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script src="https://unpkg.com/vue-chartjs#3.5.0/dist/vue-chartjs.min.js"></script>
<div id="app">
<example-component></example-component>
</div>
As you can see, the logs now return the correct elements, also in the mounted() hook of the bar-chart component.
Of course, you shouldn't use the id attribute in your component if you ever plan to have multiple instances of this component, because it would result in multiple elements having the same ID, which is invalid HTML and might lead to unexpected interferences. So, this was only for demonstration purposes in this minimal example. In your real code, you could use Vue's ref attribute instead, which you can then refer to via this.$refs inside the parent component.
There are two other issues in your code:
You don't need the colon in front of v-if, because it automatically binds to the expression given as its value.
You don't need this. in your expressions, you're in the components context automatically and can simply use the properties' names directly.
<template>
<div id="app">
<Loading></Loading>
<Content></Content>
</div>
</template>
<script>
import Loading from './Loading.vue'
import Content from './Content.vue'
export default {
name: 'App',
components: {
Loading,
Content
}
}
</script>
What is the best and elegant way to handle a loading component and remove it (or vue component or change styles) when all page is loaded?
I tried with v-cloack, but I think its not working beyond data stuff.
I tried with mounted, but doesn't seems to work.
v-cloak is to hide un-compiled mustache bindings until the Vue instance is ready. So you can use v-if to show/hide loading component.
var child1 = Vue.extend({
template: "<div>Loading...</div>"
});
var child2 = Vue.extend({
template: "<div>After Component loaded</div>",
});
var app = new Vue({
el: "#vue-instance",
data: {
loading: true
},
mounted() {
var vm = this;
setTimeout(function() {
vm.loading = false;
}, 1000);
},
components: {
child1,
child2
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.1/vue.js"></script>
<div id="vue-instance">
<child1 :name="name" v-if="loading"></child1>
<child2 :name="name" v-if="!loading"></child2>
</div>
I'm new using Vue.js and I had a difficulty creating a Button component.
How can I program this component to conditional rendering? In other words, maybe it should be rendering as a router-link maybe as a button? Like that:
<Button type="button" #click="alert('hi!')">It's a button.</Button>
// -> Should return as a <button>.
<Button :to="{ name: 'SomeRoute' }">It's a link.</Button>
// -> Should return as a <router-link>.
You can toggle the tag inside render() or just use <component>.
According to the official specification for Dynamic Components:
You can use the same mount point and dynamically switch between multiple components using the reserved <component> element and dynamically bind to it's is attribute.
Here's an example for your case:
ButtonControl.vue
<template>
<component :is="type" :to="to">
{{ value }}
</component>
</template>
<script>
export default {
computed: {
type () {
if (this.to) {
return 'router-link'
}
return 'button'
}
},
props: {
to: {
required: false
},
value: {
type: String
}
}
}
</script>
Now you can easily use it for a button:
<button-control value="Something"></button-control>
Or a router-link:
<button-control to="/" value="Something"></button-control>
This is an excellent behavior to keep in mind when it's necessary to create elements that may have links or not, such as buttons or cards.
You can create a custom component which can dynamically render as a different tag using the v-if, v-else-if and v-else directives. As long as Vue can tell that the custom component will have a single root element after it has been rendered, it won't complain.
But first off, you shouldn't name a custom component using the name of "built-in or reserved HTML elements", as the Vue warning you'll get will tell you.
It doesn't make sense to me why you want a single component to conditionally render as a <button> or a <router-link> (which itself renders to an <a> element by default). But if you really want to do that, here's an example:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
Vue.component('linkOrButton', {
template: `
<router-link v-if="type === 'link'" :to="to">I'm a router-link</router-link>
<button v-else-if="type ==='button'">I'm a button</button>
<div v-else>I'm a just a div</div>
`,
props: ['type', 'to']
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<link-or-button type="link" to="/"></link-or-button>
<link-or-button type="button"></link-or-button>
<link-or-button></link-or-button>
</div>
If you're just trying to render a <router-link> as a <button> instead of an <a>, then you can specify that via the tag prop on the <router-link> itself:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<router-link to="/">I'm an a</router-link>
<router-link to="/" tag="button">I'm a button</router-link>
</div>
You can achieve that through render functions.
render: function (h) {
if(this.to){ // i am not sure if presence of to props is your condition
return h(routerLink, { props: { to: this.to } },this.$slots.default)
}
return h('a', this.$slots.default)
}
That should help you start into the right direction
I don't think you'd be able to render a <router-link> or <button> conditionally without having a parent element.
What you can do is decide what to do on click as well as style your element based on the props passed.
template: `<a :class="{btn: !isLink, link: isLink}" #click="handleClick"><slot>Default content</slot></a>`,
props: ['to'],
computed: {
isLink () { return !!this.to }
},
methods: {
handleClick () {
if (this.isLink) {
this.$router.push(this.to)
}
this.$emit('click') // edited this to always emit
}
}
I would follow the advice by #Phil and use v-if but if you'd rather use one component, you can programmatically navigate in your click method.
Your code can look something like this:
<template>
<Button type="button" #click="handleLink">It's a button.</Button>
</template>
<script>
export default {
name: 'my-button',
props: {
routerLink: {
type: Boolean,
default: false
}
},
methods: {
handleLink () {
if (this.routerLink) {
this.$router.push({ name: 'SomeRoute' })
} else {
alert("hi!")
}
}
}
}
</script>