Nuxt - The template root requires exactly one element - vue.js

I recently updated a few dependencies in a Nuxt based project I had a developer work on for me (I'm a designer with a very basic JS/vue knowledge-base). Now the build is spitting out the 'template root requires exactly one element' error. From searching other threads I can see the principle of what I need to change (contain everything in one element) but I'm just unsure how to do that with this files particular structure (v-if arrangement). I've included the offending file below and wondered if anyone could point me in the right direction? Much appreciated!
<template>
<nuxt-link
v-if="to"
:class="classes"
:to="to"
v-bind="inheritedProps"
v-on="$listeners"
>
<slot />
</nuxt-link>
<a
v-else-if="href"
:class="classes"
:href="href"
v-bind="inheritedProps"
v-on="$listeners"
>
<slot />
</a>
<button
v-else
:class="classes"
:type="type"
v-bind="inheritedProps"
v-on="$listeners"
>
<slot />
</button>
</template>
<script>
export default {
name: 'BaseButton',
props: {
block: {
type: Boolean,
default: false
},
variant: {
type: String,
default: () => {}
},
href: {
type: String,
default: () => {}
},
to: {
type: String,
default: () => {}
},
type: {
type: String,
default: () => {}
}
},
computed: {
inheritedProps () {
return {
...this.$props,
...this.$attrs
}
},
classes () {
return [
'btn',
{
'btn--block': this.block
}
].concat(this.modifiers)
},
modifiers () {
const modifiersArray = this.variant && this.variant.split(' ')
return this.variant ? modifiersArray.map(modifier => `btn--${modifier}`) : false
}
}
}
</script>

I'm a little surprised that you're seeing this error as Vue doesn't normally complain if you're using v-if/v-else-if/v-else like that. The template is guaranteed to output a single element when it runs, so usually Vue allows it. It may shed more light on what's going on if you include the exact error message in the question.
I suggest checking you aren't running into the problem discussed below, caused by incompatible library versions, which incorrectly reports this error:
https://github.com/vuejs/eslint-plugin-vue/issues/986
I really don't think there's anything wrong with your code, so I suggest investigating library versions before making any code changes.
Further, if it is just the linter that's complaining you could consider suppressing that rule. The Vue template compiler will shout soon enough if there's a real problem with multiple root nodes.
That said, if you really can't make the error message go away...
The simplest solution is just to wrap everything in an extra element at the root.
If you don't want to use a wrapper element (possibly because it interferes with your layout) you can use is to reduce down your template:
<template>
<component
:class="classes"
v-bind="childProps"
v-on="$listeners"
>
<slot />
</component>
</template>
<script>
export default {
// ... other stuff ...
computed: {
childProps () {
const childProps = {...this.inheritedProps}
if (this.to) {
childProps.is = 'nuxt-link'
childProps.to = this.to
} else if (this.href) {
childProps.is = 'a'
childProps.href = this.href
} else {
childProps.is = 'button'
childProps.type = this.type
}
return childProps
}
}
}
</script>
That said, you're almost in render function territory doing it this way.

you should wrap your all content under template into on root tag,just for example i have used div to wrap all html content under template. you can use any other tag based on your requirement.
you can use below solution
<template>
<div>
<nuxt-link
v-if="to"
:class="classes"
:to="to"
v-bind="inheritedProps"
v-on="$listeners"
>
<slot />
</nuxt-link>
<a
v-else-if="href"
:class="classes"
:href="href"
v-bind="inheritedProps"
v-on="$listeners"
>
<slot />
</a>
<button
v-else
:class="classes"
:type="type"
v-bind="inheritedProps"
v-on="$listeners"
>
<slot />
</button>
</div>
</template>

Related

How to register an array of objects with v-model? Vue 2

The first time that I dive into making this type of form with Vue, the issue is that I can't think of how to save the data inside the foreach that I generate with axios.
Where I would like to save the ID and the option selected with the input select as an object in order to make faster the match in the backend logic.
<template>
<div class="row" v-else>
<div class=" col-sm-6 col-md-6 col-lg-6" v-for="(project, index) in projects" :key="index">
<fieldset class="border p-2 col-11">
<legend class="w-auto col-12">Proyecto: {{project.name}}</legend>
<b-form-group
id="user_id"
label="Reemplazante"
>
<b-form-select
v-model="formProject[index].us"
:options="project.users"
value-field="replacement_user_id"
text-field="replacement_user_name"
#change="addReemplacemet($event,project.id)"
>
<template v-slot:first>
<b-form-select-option value="All">Seleccione</b-form-select-option>
</template>
</b-form-select>
<input type="hidden" name="project" v-model="formProject[index].proj">
</b-form-group>
</fieldset>
</div>
</div>
</template>
<script>
import skeleton from './skeleton/ProjectUserSkeleton.vue'
export default {
name: 'ProjectsUser',
components: { skeleton },
props:{
user: { type: String },
},
data() {
return {
user_id: null,
showProject: false,
projects: [],
loadingProjects: true,
formProject: [
{
us: 'All',
proj: null
}
]
}
},
watch: {
user: function() {
this.viewProjects(this.user)
}
},
methods: {
async getProjects(salesman){
this.loadingProjects = true
await axios.get(route('users.getProjects'),{
params: {
filter_user: salesman
}
})
.then((res)=>{
this.projects = res.data.data
setTimeout(() => {
this.loadingProjects = false
}, 800);
})
},
This is the form:
This is the message error:
At first glance, this looks like an issue with data. Your error suggests that you're trying to read an undefined variable.
It's often undesirable to use an index from iterating one array on another. In your Vue code, the index is projects, but you use it to access the variable formProject. This is usually undesirable because you can no longer expect that index to reference a defined variable (unless you're referencing the array you are currently iterating).
The easiest solution, for now, is to make sure the arrays are of the same length. Then utilizing v-if or other methods to not render the snippet if the variable is not defined.
A more complicated but better solution is restructuring your data such that formProject exists in projects

VueJS component ref component is not accessible at all

I don't understand the refs when using in Vue component. It is not working properly.. I have two files
show.vue
<template>
<div>
<b-container fluid class="bg-white" v-if="$refs.chart">
<b-row class="topTab types">
<b-col
:class="{ active: currentTab === index }"
v-for="(tabDisplay, index) in $refs.chart.tabDisplays"
:key="index"
>
<router-link
:to="{ query: { period: $route.query.period, tab: index } }"
>
{{ tabDisplay }}
</router-link>
</b-col>
</b-row>
</b-container>
<component v-bind:is="currentGame" ref="chart" />
</div>
</template>
<script>
export default {
computed: {
currentGame() {
return () =>
import(
`#/components/Trend/example/Charts/${this.group}/${this.id}/Base.vue`
);
},
}
};
</script>
Base.vue
<template>
<div>
dadsas
</div>
</template>
<script>
export default {
data: function() {
return {
tabDisplays: {
1: "example1",
2: "example2",
3: "example3",
4: "example4"
}
};
}
};
</script>
Take note that the second file renders properly showing the dasdas but the $refs.chart.tabDisplays is not. It will only show when I change something inside the <script> tag like adding 5: "example5" in the tabDisplays data then if I refresh it will be gone again. Basically, I just want to access the computed property of my child component. I am very aware I can use vuex but I want to try accessing a component's computed property via ref. What is wrong with my $.refs.chart?
As I noted in my comment, refs are only populated after rendering, so you won't have access to them during rendering. This is mentioned in the docs, see https://v2.vuejs.org/v2/api/#ref. The child component doesn't exist at the point you're trying to access it. The rendering process is responsible for creating the child components, it all gets a bit circular if you try to access them during that rendering process.
It looks like you've already made several key design decisions here about how to structure your application, such as component boundaries and data ownership, and those decisions are making it difficult to get where you want to be. It's not easy to make concrete suggestions about how to fix that based purely on the code provided.
So instead I will attempt to suggest a minimal change that should fix the immediate problem you're having.
To access the property of the child you're going to need the parent component to render twice. The first time it will create the chart and the second time it will have the relevant property available. One way to do this would be to copy the relevant property to the parent after rendering.
<template>
<div>
<b-container fluid class="bg-white" v-if="tabDisplays">
<b-row class="topTab types">
<b-col
:class="{ active: currentTab === index }"
v-for="(tabDisplay, index) in tabDisplays"
:key="index"
>
<router-link
:to="{ query: { period: $route.query.period, tab: index } }"
>
{{ tabDisplay }}
</router-link>
</b-col>
</b-row>
</b-container>
<component v-bind:is="currentGame" ref="chart" />
</div>
</template>
<script>
export default {
data () {
return { tabDisplays: null };
},
computed: {
currentGame() {
return () =>
import(
`#/components/Trend/example/Charts/${this.group}/${this.id}/Base.vue`
);
},
},
mounted () {
this.tabDisplays = this.$refs.chart.tabDisplays;
},
updated () {
this.tabDisplays = this.$refs.chart.tabDisplays;
}
};
</script>
In the code above I've introduced a tabDisplays property and that is then being synced with the child in mounted and updated. Within the template there's no reference to $refs at all.
While this should work I would repeat my earlier point that the 'correct' solution probably involves more significant changes. Syncing data up to a parent like this is not a normal Vue pattern and strongly suggests an architectural failure of some kind.
This is based on #skirtle's answer. I just made a few tweaks in his answer to produce what I really want
show.vue
<template>
<div>
<b-container fluid class="bg-white" v-if="chart">
<b-row class="topTab types">
<b-col
:class="{ active: currentTab === index }"
v-for="(tabDisplay, index) in chart.tabDisplays"
:key="index"
>
<router-link
:to="{ query: { period: $route.query.period, tab: index } }"
>
{{ tabDisplay }}
</router-link>
</b-col>
</b-row>
</b-container>
<component v-bind:is="currentGame" ref="chart" />
</div>
</template>
<script>
export default {
data: function() {
return {
chart: undefined
};
},
computed: {
currentGame() {
return () =>
import(
`#/components/Trend/高频彩/Charts/${this.group}/${this.id}/Base.vue`
);
},
},
updated() {
this.chart = this.$refs.chart;
}
};
</script>

Render a child component in the global app component

I wrote a dialog component (global) to show modal dialogs with overlays like popup forms.
Right now the dialog gets rendered inside the component where it is used. This leads to overlapping content, if there is something with position relative in the html code afterwards.
I want it to be rendered in the root App component at the very end so I can force the dialog to be always ontop of every other content.
This is my not working solution:
I tried to use named slots, hoping, that they work backwards in the component tree too. Unfortunately they don't seem to do that.
Anybody a solution how to do it?
My next idea would be to render with an extra component that is stored in the app component and register the dialogs in the global state. But that solution would be super complicated and looks kinda dirty.
The dialog component:
<template v-slot:dialogs>
<div class="dialog" :class="{'dialog--open': show, 'dialog--fullscreen': fullscreen }">
<transition name="dialogfade" duration="300">
<div class="dialog__overlay" v-if="show && !fullscreen" :key="'overlay'" #click="close"></div>
</transition>
<transition name="dialogzoom" duration="300">
<div class="dialog__content" :style="{'max-width': maxWidth}" v-if="show" :key="'content'">
<slot></slot>
</div>
</transition>
</div>
</template>
<script>
export default {
name: "MyDialog",
props: {show: {
type: Boolean,
default: false
},
persistent: {
type: Boolean,
default: true
},
fullscreen: {
type: Boolean,
default: false
},
maxWidth: {
type: String,
default: '600px'
}
},
data: () => ({}),
methods: {
close() {
if(!this.persistent) {
this.$emit('close')
}
}
}
}
</script>
The template of the app component:
<template>
<div class="application">
<div class="background">
<div class="satellite"></div>
<div class="car car-lr" :style="{ transform: `translateY(${car.x}px)`, left: adjustedLRLeft + '%' }" v-for="car in carsLR"></div>
</div>
<div class="content">
<login v-if="!$store.state.user"/>
<template v-else>
<main-menu :show-menu="showMainMenu" #close="showMainMenu = false"/>
<router-view/>
</template>
<notifications/>
<div class="dialogs"><slot name="dialogs"></slot></div>
</div>
</div>
</template>
Another possibility is to use portals. These provide a way to move any element to any place in the dom. Checkout the following library: https://github.com/LinusBorg/portal-vue
You can just place the dialog component directly in the app component and handle the dialog logic/which dialog to display in that component?
In case you want to trigger these dialogs from other places in your app, this would would be a good use case for vuex! That, combined with dynamic webpack imports is how I handle this.
With the help of the guys from the vuetify2 project, I found the solution. The dialog component gets an ref="dialogContent" attribute and the magic happens inside the beforeMount function.
<template>
<div class="dialog" ref="dialogContent" :class="{'dialog--open': show, 'dialog--fullscreen': fullscreen }">
<transition name="dialogfade" duration="300">
<div class="dialog__overlay" v-if="show && !fullscreen" :key="'overlay'" #click="close"></div>
</transition>
<transition name="dialogzoom" duration="300">
<div class="dialog__content" :style="{'max-width': maxWidth}" v-if="show" :key="'content'">
<slot></slot>
</div>
</transition>
</div>
</template>
<script>
export default {
name: "MyDialog",
props: {
show: {
type: Boolean,
default: false
},
persistent: {
type: Boolean,
default: true
},
fullscreen: {
type: Boolean,
default: false
},
maxWidth: {
type: String,
default: '600px'
}
},
data: () => ({}),
methods: {
close() {
if (!this.persistent) {
this.$emit('close')
}
}
},
beforeMount() {
this.$nextTick(() => {
const target = document.getElementById('dialogs');
target.appendChild(
this.$refs.dialogContent
)
})
},
}
</script>

Conditional link behavior in VueJS

Couldn't find a proper name for the title, will be glad if someone figures out a better name.
I have a component which represents a product card. The whole component is wrapped in <router-link> which leads to product page.
However I have another case, when I do not need the component to lead to a product page, but instead I need to do some other action.
The only solution I found is to pass a callback function as a prop, and based on this, do something like:
<router-link v-if="!onClickCallback">
... here goes the whole component template ...
</router-link>
<div v-if="onClickCallback" #click="onClickCallback">
... here again goes the whole component template ...
</div>
How can I do this without copy-pasting the whole component? I tried to do this (real code sample):
<router-link class="clothing-item-card-preview"
:class="classes"
:style="previewStyle"
:to="{ name: 'clothingItem', params: { id: this.clothingItem.id }}"
v-on="{ click: onClick ? onClick : null }">
However I got this: Invalid handler for event "click": got null
Plus not sure if it's possible to pass prevent modificator for click and this just looks weird, there should be a better architectural solution
Commenting on the error, you could use an empty function instead of null, in the real code snippet
<router-link class="clothing-item-card-preview"
:class="classes"
:style="previewStyle"
:to="{ name: 'clothingItem', params: { id: this.clothingItem.id }}"
v-on="{ click: onClick ? onClick : null }">
This should works (replace a for "router-link" then insert right properties)
Further infos :
https://fr.vuejs.org/v2/guide/components-dynamic-async.html
v-bind is simply an Object where each keys is a props for your component, so here, I programmatically defined an object of properties depending on the wrapper (router link or a simple div). However we cannot do this for events (of course we could create our own event listener but it's a little bit tricky) so I simply but an handle method.
new Vue({
el: "#app",
data: {
products : [{onClickCallback : () => { alert("callback"); return true;}}, {}, {}]
},
methods : {
handleClick(product, event) {
if (!product.onClickCallback) return false
product.onClickCallback()
return true
},
getMyComponentName(product) {
if (product.onClickCallback) return "div"
return "a"
},
getMyComponentProperties(product) {
if (product.onClickCallback) return {is : "div"}
return {
is : "a",
href: "!#"
}
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<component
v-for="(product, index) in products"
:key="index"
v-bind="getMyComponentProperties(product)"
#click="handleClick(product, $event)"
>
<div class="product-card">
<div class="product-card-content">
<span v-show="product.onClickCallback">I'm a callback</span>
<span v-show="!product.onClickCallback">I'm a router link</span>
</div>
</div>
</component>
</div>
Do you have to use a <router-link>? If it can safely be a <div>, you could use something like
<div #click="handleClick" ...>
<!-- component template -->
</div>
and
methods: {
handleClick (event) {
if (this.onClickCallback) {
this.onClickCallback(event)
} else {
this.$router.push({ name: 'clothingItem', ... })
}
}
}
See https://router.vuejs.org/guide/essentials/navigation.html

Conditional <router-link> in Vue.js dependant on prop value?

Hopefully this is a rather simple question / answer, but I can't find much info in the docs.
Is there a way to enable or disable the anchor generated by <router-link> dependent on whether a prop is passed in or not?
<router-link class="Card__link" :to="{ name: 'Property', params: { id: id }}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
If there's no id passed to this component, I'd like to disable any link being generated.
Is there a way to do this without doubling up the content into a v-if?
Thanks!
Assuming you want to disable anchor tag as in not clickable and look disabled the option is using CSS. isActive should return true by checking prop id.
<router-link class="Card__link" v-bind:class="{ disabled: isActive }" :to="{ name: 'Property', params: { id: id }}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
<style>
.disabled {
pointer-events:none;
opacity:0.6;
}
<style>
If you want to just disable the navigation , you can use a route guard.
beforeEnter: (to, from, next) => {
next(false);
}
If you need to use it often, consider this:
Create new component
<template>
<router-link
v-if="!disabled"
v-bind="$attrs"
>
<slot/>
</router-link>
<span
v-else
v-bind="$attrs"
>
<slot/>
</span>
</template>
<script>
export default {
name: 'optional-router-link',
props: {
params: Object,
disabled: {
type: Boolean,
default: false,
},
},
};
</script>
Optional, register globally:
Vue.component('optional-router-link', OptionalRouterLink);
Use it as follows:
<optional-router-link
:disabled="isDisabled"
:to="whatever"
>
My link
</optional-router-link>
The problem is that router-link renders as an html anchor tag, and anchor tags do not support the disabled attribute. However you can add tag="button" to router-link:
<router-link :to="myLink" tag="button" :disabled="isDisabled" >
Vue will then render your link as a button, which naturally supports the disabled attribute. Problem solved! The downside is that you have to provide additional styling to make it look like a link. However this is the best way to achieve this functionality and does not rely on any pointer-events hack.
I sometimes do stuff like this:
<component
:is="hasSubLinks ? 'button' : 'router-link'"
:to="hasSubLinks ? undefined : href"
:some-prop="computedValue"
#click="hasSubLinks ? handleClick() : navigate"
>
<!-- arbitrary markup -->
</component>
...
computed: {
computedValue() {
if (this.hasSubLinks) return 'something';
if (this.day === 'Friday') return 'tgif';
return 'its-fine';
},
},
But I basically always wrap router-link, so you can gain control over disabled state, or pre-examine any state or props before rendering the link, with something like this:
<template>
<router-link
v-slot="{ href, route, navigate, isActive, isExactActive }"
:to="to"
>
<a
:class="['nav-link-white', {
'nav-link-white-active': isActive,
'opacity-50': isDisabled,
}]"
:href="isDisabled ? undefined : href"
#click="handler => handleClick(handler, navigate)"
>
<slot></slot>
</a>
</router-link>
</template>
<script>
export default {
name: 'top-nav-link',
props: {
isDisabled: {
type: Boolean,
required: false,
default: () => false,
},
to: {
type: Object,
required: true,
},
},
data() {
return {};
},
computed: {},
methods: {
handleClick(handler, navigate) {
if (this.isDisabled) return undefined;
return navigate(handler);
},
},
};
</script>
In my app right now, I'm noticing that some combinations of #click="handler => handleClick(handler, navigate)" suffer significantly in performance.
For example this changes routes very slow:
#click="isDisabled ? undefined : handler => navigate(handler)"
But the pattern in my full example code above works and has no performance issue.
In general, ternary operator in #click can be very dicey, so if you get issues, don't give up right away, try many different ways to bifurcate on predicates or switch over <component :is="" based on state. navigate itself is an ornery one because it requires the implicit first parameter to work.
I haven't tried, but you should be able to use something like Function.prototype.call(), Function.prototype.apply(), or Function.prototype.bind().
For example, you might be able to do:
#click="handler => setupNavigationTarget(handler, navigate)"
...
setupNavigationTarget(handler, cb) {
if (this.isDisabled) return undefined;
return this.$root.$emit('some-event', cb.bind(this, handler));
},
...
// another component
mounted() {
this.$root.$on('some-event', (navigate) => {
if (['Saturday', 'Sunday'].includes(currentDayOfTheWeek)) {
// halt the navigation event
return undefined;
}
// otherwise continue (and notice how downstream logic
// no longer has to care about the bound handler object)
return navigate();
});
},
You could also use the following:
<router-link class="Card__link" :to="id ? { name: 'Property', params: { id: id }} : {}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
If id is undefined the router won't redirect the page to the link.
I've tried different solutions but only one worked for me, maybe because I'm running if from Nuxt? Although theoretically nuxt-link should work exactly the same as router-link.
Anyway, here is the solution:
<template>
<router-link
v-slot="{ navigate }"
custom
:to="to"
>
<button
role="link"
#click="onNavigation(navigate, $event)"
>
<slot></slot>
</button>
</router-link>
</template>
<script>
export default {
name: 'componentName',
props: {
to: {
type: String,
required: true,
},
},
methods: {
onNavigation(navigate, event) {
if (this.to === '#other-action') {
// do something
} else {
navigate(event);
}
return false;
},
};
</script>