Vue.js use slot from children's children - javascript

So I have the following example:
Here is my child component. Every other ones of the following components is based upon this.
<template>
<div class="content-box">
<div class="boxtitlecontainer titleColor">
<slot name="title">Title</slot>
</div>
<div class="insidebox boxColor">
<slot></slot>
</div>
</div>
</template>
This is one of the children.
<template>
<div class="example">
<box>
<div slot="title"><slot name="title">Title</slot></div>
<slot></slot>
</box>
</div>
</template>
This component is directly used in my App.vue. To use <slot>s, the only way i found is this one above.
My question is: Is there a more elegant way of doing this and to not stack up div-Boxes unnecessary? I mean, I can do it with no named slots. I guess that the <slot> can be showed recursivly like content -> slot(1st children) -> slot(2nd children) but i have no idea about how to do it with named slots.
Thanks in advance for any help.

instead of depending on slot for passing your content, why not use props instead?
<template>
<div class="example">
<box>
<div v-text="title">Title</div>
<slot></slot>
</box>
</div>
</template>
<script>
export default {
props: ['title']
}
</script>

Related

Vue: Mysterious ghost props?

In my project, I came across the following code:
Parent component - <ParameterModal>:
<template>
<modal-wrapper props="...">
<!-- ... other templates similar to this... -->
<template v-else-if="modalTemplate === 'bitmask_set'">
<template slot="header">
<h4 class="center-text">{{title}}</h4>
</template>
<div v-if="errorMessage" class="error-message">
{{errorMessage}}
</div>
<ModalBitmaskSet
v-bind="modalMeta"
:setErrorMessage="setErrorMessage"
:clearErrorMessage="clearErrorMessage"
/>
</template>
<!-- ... -->
<div v-else>
Warning: unmapped modal template!
</div>
<!-- ... -->
</modal-wrapper>
</template>
Ok, cool, this is using a regular slot and named slot to display a component called <ModalBitmaskSet>. So I look inside modal-wrapper to find the outlets...
Child component - <modal-wrapper>
<template>
<!-- some container and wrapper elements and then... -->
<div class="modal-header">
<slot name="header" />
</div>
<div
:class="['modal-body', 'display-flex', 'flex-direction-column', modalTemplate]"
>
<div
id="escape_message"
style="text-align: center; display: none; padding-bottom: 10px;"
>
You have unsaved changes.<br />Please click Save or Cancel to proceed.
</div>
<md-content v-if="modalContent">{{modalContent}}</md-content>
<slot />
</div>
<!-- end containers and wrappers -->
</template>
Also cool, there is where the slots are coming out... but how are props being passed to <ModalBitmaskSet>? When I look in Vue DevTools, I can see that props are somehow being passed to this component that don't exist in the parent. On top of this, when I add new components to <ParameterModal>, they sometimes don't get passed props that other components seem to be getting! This is very weird!
As you can see from the photo, this component is somehow getting passed props that aren't listed in the code! Specifically, the props colIndex, fieldSet, indexOffset, methodIndex and rowIndex in this case, although other components on this <ParameterModal> component appear to get different props.
Am I missing something? Where could these ghostly props be coming from?
This line seems the likely cause, though without seeing the code for modalMeta it's difficult to be sure:
v-bind="modalMeta"
This is using the object v-bind syntax, so whatever properties exist in the modalMeta object will be passed as props to the component.
See:
https://v2.vuejs.org/v2/guide/components-props.html#Passing-the-Properties-of-an-Object
https://v2.vuejs.org/v2/api/#v-bind

Change the root element of a component

VueJS allows only one root element per component. In most case that's fine to wrap the component inside a div tag but it can sometimes cause unexpected behaviors.
For instance, when using Bootstrap, if you put a div between two elements (like <b-row> and <b-col>), the layout get totally broken.
Many other elements in the framework needs to follow a specific order and that's why having one root element could be problematic.
Is there a way to dynamically set the root element?
To illustrate what i'm saying, take a look at this example:
If a have a component my-h1 like this:
<template>
<div>
<h1>Hello world</h1>
</div>
</template>
which get called here:
<div id="my-app">
<my-h1 />
</div>
The code above will output:
<div id="my-app">
<div>
<h1>Hello world</h1>
</div>
</div>
How can i get this output:
<div id="my-app">
<p>
<h1>Hello world</h1>
</p>
</div>
and at another place, this one:
<div id="my-app">
<a>
<h1>Hello world</h1>
</a>
</div>
(I know these tags doesn't make any sense, it's only for the purpose of this exemple)
I hope you see what i mean. Still have one root element, but setting it to be different, with a prop or something else :)
Just use the special <component> component which allows you to choose the component dynamically:
MyComponent.vue
<template>
<component :is="is" v-bind="props">
Hello world
</component>
<template>
export default {
props: ['is', 'props'],
}
Usage is like so:
<my-component is="div"/>
<my-component is="p"/>
With slots you will have to replace the entire component, which I don't think you want.
The other solution will be to pass a type property to your component and do a switch (I don't know if it is compliant with vue general philosophy)
But here is an example about what I was thinking:
<template>
<template v-if="type === 'div'">
<div>
<h1>Hello world</h1>
</div>
</template>
<template v-else-if="type === 'a'">
<a>
<h1>Hello world</h1>
</a>
</template>
<template v-else-if="type === 'p'">
<p>
<h1>Hello world</h1>
</p>
</template>
<template v-else>
<i>
This is a default
<b>ITALIC TEXT</b>
</i>
</template>
</template>
<script>
export default {
props: {
type: String
}
};
</script>
Then you call your component
<component type="div|p|a|whatever"/>

Vue js error: Component template should contain exactly one root element

I don't know what the error is, so far I am testing through console log to check for changes after selecting a file (for uploading).
When I run $ npm run watch, i get the following error:
"Webpack is watching the files…
95% emitting
ERROR Failed to compile with 1 errors
19:42:29
error in ./resources/assets/js/components/File.vue
(Emitted value instead of an instance of Error) Vue template syntax
error:
Component template should contain exactly one root element. If you
are using v-if on multiple elements, use v-else-if to chain them
instead.
# ./resources/assets/js/components/AvatarUpload.vue 5:2-181 #
./resources/assets/js/app.js # multi ./resources/assets/js/app.js
./resources/assets/sass/app.scss"
My File.vue is
<template>
<div class="form-group">
<label for="avatar" class="control-label">Avatar</label>
<input type="file" v-on:change="fileChange" id="avatar">
<div class="help-block">
Help block here updated 4 🍸 ...
</div>
</div>
<div class="col-md-6">
<input type="hidden" name="avatar_id">
<img class="avatar" title="Current avatar">
</div>
</template>
<script>
export default{
methods: {
fileChange(){
console.log('Test of file input change')
}
}
}
</script>
Any ideas on how to solve this? What is actually the error?
Note This answer only applies to version 2.x of Vue. Version 3 has lifted this restriction.
You have two root elements in your template.
<div class="form-group">
...
</div>
<div class="col-md-6">
...
</div>
And you need one.
<div>
<div class="form-group">
...
</div>
<div class="col-md-6">
...
</div>
</div>
Essentially in Vue you must have only one root element in your templates.
For a more complete answer: http://www.compulsivecoders.com/tech/vuejs-component-template-should-contain-exactly-one-root-element/
But basically:
Currently, a VueJS template can contain only one root element (because of rendering issue)
In cases you really need to have two root elements because HTML structure does not allow you to create a wrapping parent element, you can use vue-fragment.
To install it:
npm install vue-fragment
To use it:
import Fragment from 'vue-fragment';
Vue.use(Fragment.Plugin);
// or
import { Plugin } from 'vue-fragment';
Vue.use(Plugin);
Then, in your component:
<template>
<fragment>
<tr class="hola">
...
</tr>
<tr class="hello">
...
</tr>
</fragment>
</template>
You need to wrap all the html into one single element.
<template>
<div>
<div class="form-group">
<label for="avatar" class="control-label">Avatar</label>
<input type="file" v-on:change="fileChange" id="avatar">
<div class="help-block">
Help block here updated 4 🍸 ...
</div>
</div>
<div class="col-md-6">
<input type="hidden" name="avatar_id">
<img class="avatar" title="Current avatar">
</div>
</div>
</template>
<script>
export default{
methods: {
fileChange(){
console.log('Test of file input change')
}
}
}
</script>
if, for any reasons, you don't want to add a wrapper (in my first case it was for <tr/> components), you can use a functionnal component.
Instead of having a single components/MyCompo.vue you will have few files in a components/MyCompo folder :
components/MyCompo/index.js
components/MyCompo/File.vue
components/MyCompo/Avatar.vue
With this structure, the way you call your component won't change.
components/MyCompo/index.js file content :
import File from './File';
import Avatar from './Avatar';
const commonSort=(a,b)=>b-a;
export default {
functional: true,
name: 'MyCompo',
props: [ 'someProp', 'plopProp' ],
render(createElement, context) {
return [
createElement( File, { props: Object.assign({light: true, sort: commonSort},context.props) } ),
createElement( Avatar, { props: Object.assign({light: false, sort: commonSort},context.props) } )
];
}
};
And if you have some function or data used in both templates, passed them as properties and that's it !
I let you imagine building list of components and so much features with this pattern.
Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.
The right approach is
<template>
<div> <!-- The root -->
<p></p>
<p></p>
</div>
</template>
The wrong approach
<template> <!-- No root Element -->
<p></p>
<p></p>
</template>
Multi Root Components
The way around to that problem is using functional components, they are components where you have to pass no reactive data means component will not be watching for any data changes as well as not updating it self when something in parent component changes.
As this is a work around it comes with a price, functional components don't have any life cycle hooks passed to it, they are instance less as well you cannot refer to this anymore and everything is passed with context.
Here is how you can create a simple functional component.
Vue.component('my-component', {
// you must set functional as true
functional: true,
// Props are optional
props: {
// ...
},
// To compensate for the lack of an instance,
// we are now provided a 2nd context argument.
render: function (createElement, context) {
// ...
}
})
Now that we have covered functional components in some detail lets cover how to create multi root components, for that I am gonna present you with a generic example.
<template>
<ul>
<NavBarRoutes :routes="persistentNavRoutes"/>
<NavBarRoutes v-if="loggedIn" :routes="loggedInNavRoutes" />
<NavBarRoutes v-else :routes="loggedOutNavRoutes" />
</ul>
</template>
Now if we take a look at NavBarRoutes template
<template>
<li
v-for="route in routes"
:key="route.name"
>
<router-link :to="route">
{{ route.title }}
</router-link>
</li>
</template>
We cant do some thing like this we will be violating single root component restriction
Solution
Make this component functional and use render
{
functional: true,
render(h, { props }) {
return props.routes.map(route =>
<li key={route.name}>
<router-link to={route}>
{route.title}
</router-link>
</li>
)
}
Here you have it you have created a multi root component, Happy coding
Reference for more details visit: https://blog.carbonteq.com/vuejs-create-multi-root-components/
In addition to Bert and blobmaster responses:
If you need to remove the root element from the DOM you can exploit css and use display: value on the root element.
Bit of a misleading error.
What fixed it on my side was the fact that I had an additional </div> without an opening <div>.
I spotted it using Find/Replace on "div" which gave an odd number.
Wrap everything in one div and it will resolve the issue.
For example,
div
----div
----/div>
----div>
----/div>
/div
It is similar concept to React.js
For vue 3 they removed this constraint in template syntax :
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
but it's still existing in JSX syntax :
Incorrect ❌
setup(props,{attrs}) {
return ()=>(
<header>...</header>
<main {..attrs}>...</main>
<footer>...</footer>
)
}
Correct ✔
setup(props,{attrs}) {
return ()=>(
<>
<header>...</header>
<main {..attrs}>...</main>
<footer>...</footer>
</>
)
}
I experienced this kind of issue and the issue was fixed by adding a main parent div tag or section if it is a section type of component.
<div class="list-of-friends">
<h3>Hello World</h3>
</div>
I was confused as I knew VueJS should only contain 1 root element and yet I was still getting this same "template syntax error Component template should contain exactly one root element..." error on an extremely simple component. Turns out I had just mispelled </template> as </tempate> and that was giving me this same error in a few files I copied and pasted. In summary, check your syntax for any mispellings in your component.
instead of using this
Vue.component('tabs', {
template: `
<div class="tabs">
<ul>
<li class="is-active"><a>Pictures</a></li>
<li><a>Music</a></li>
<li><a>Videos</a></li>
<li><a>Documents</a></li>
</ul>
</div>
<div class="tabs-content">
<slot></slot>
</div>
`,
});
you should use
Vue.component('tabs', {
template: `
<div>
<div class="tabs">
<ul>
<li class="is-active"><a>Pictures</a></li>
<li><a>Music</a></li>
<li><a>Videos</a></li>
<li><a>Documents</a></li>
</ul>
</div>
<div class="tabs-content">
<slot></slot>
</div>
</div>
`,
});
Just make sure that you have one root div and put everything inside this root
<div class="root">
<!--and put all child here --!>
<div class='child1'></div>
<div class='child2'></div>
</div>
and so on

Vue component unknown despite being registered

I am trying to compose a component inside another like this:
<prompt :users="users">
...
<dataset v-for="ds in users" :user="user"></dataset>
...
</prompt>
But apparently I'm not registering it properly:
[Vue warn]: Unknown custom element: <dataset> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
(found in root instance)
Here's how I'm trying to register it:
app.js
Vue.component('prompt', {
props: ['userdata', 'users'],
template: '#prompt-template',
components: {
'dataset': {
props: ['userdataset', 'user'],
template: '#dataset-template',
}
}
});
Finally, the templates:
<template id="dataset-template">
<li>{{ user}}</li>
</template>
<template id="prompt-template">
<transition name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">
default header
</slot>
</div>
<div class="modal-body">
<slot name="body">
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button class="modal-default-button" #click="$emit('close')">
OK
</button>
</slot>
</div>
</div>
</div>
</div>
</transition>
</template>
Are there any steps I'm missing? I can't figure out how the component isn't being registered.
The problem is that you use the dataset component as a slot within the prompt component. While the Vue vm tries to figure out the component tree it will recognize the dataset component which it does not know. Subcomponents are used in the component template but not within slots. You have to register the dataset component within the Vue vm like you did for the prompt component. Try this
Vue.component('promp', { ... })
Vue.component('dataset', { ... })
It also make sense to register the components on the same level since the templates of the components are also registered on the same level (next to each other).
Compare it to the example you mentioned in another answers comment: Here the subcomponent axis-label is only used within the template of the parent polygraph component. This is valid since now the component is in contract to figure out it sub components not the vue-vm.
In other words:
It should be possible to pass components into the slot of any component A which are not subcomponents of A. Therefore all components passed to slots of a component should be available to the vue vm.
Thumb rule could be
if a component does not appear within another components template, it is not a subcomponent.

Vue.js fragment instance

I seriously don't know, why the fragments is the problem.
<template>
<div id="page">
</div>
<div class="some">
</div>
</template>
[Vue warn]: Attribute "id" is ignored on component "div"
You need to wrap the contents of your template in another div. When it comes to render it, it needs the single root element to replace
<template>
<div>
<div id="page">
</div>
<div class="some">
</div>
</div>
</template>
Vue v3 now supports multi-root templates. Your code should work out-of-the-box.
https://v3-migration.vuejs.org/new/fragments.html

Categories

Resources