Vue3 Manually Render Slot Content - javascript

I'm trying to parse out the contents of a slot and render the contents in multiple places. The idea is to create a wizard that allows each step to be contained in a single component. Something like this:
Wizard Component Definition:
<ul>
<li v-for="icon in icons">{{icon}}<li>
</ul>
<section>
<ul>
<li v-for="body in bodies">{{body}}</li>
</ul>
</section>
Wizard Component Script
import {ref} from "vue";
export default {
setup(props, {slots}) {
const icons = ref([]);
const bodies = ref([]);
for (let item of slots.default()) {
// not sure if I need to call these, ex: item.children.icon()
icons.value.push(item.children.icon);
bodies.value.push(item.children.body);
}
return {icons, bodies};
}
}
Wizard Component Usage:
<wizard>
<wizard-page>
<template #icon>someIcon</template>
<template #body>someBody</template>
</wizard-page>
<wizard-page>
<template #icon>someIcon2</template>
<template #body>someBody2</template>
</wizard-page>
</wizard>
The obvious problem here is that everything is VNodes and doesn't just render nicely to the DOM. I've tried using render() and h() but those don't seem to be what I'm looking for. Also tried the v-html binding, but again, that isn't expecting a VNode.
I'm not sure if there's a better way to do this, or if I'm missing something simple here, but I'm not seeing an easy way to split apart a slot and render the contents in different places.

VUE 3
<component :is="body">
You can implement it in the following way
<ul>
<li v-for="body in bodies" :key="uniquekey">
<component :is="body" />
</li>
</ul>
link to docs:
https://vuejs.org/guide/essentials/component-basics.html#dynamic-components

Related

What's the best way to pass markdown to an Astro component as a prop

What I'm trying to do
A simple way to render the content of a markdown file when it's passed as a string to another component using .compiledContent (or even using .rawContnent)? Or even a better way than this as obviously usually in Astro we can use the <Content /> Component, but from my knowledge, I can't pass a component or this functionality to another component without using a <slot /> in the parent component.
I have some JS for the parent component and using a <slot/> instead of passing the props to the component would change things, so hopefully looking for a solution with using this.
My setup
Data stored in /src/data/experience as markdown files with a year and a description formatted as markdown in the content section of each file
A component called Tabs.astro which takes props of headings and contents which are both lists of strings
A page /src/pages/experience.astro with the Tabs component in it which is displaying this data
I take the below code to get the data from the markdown files and pass the years and descriptions to the Tab component.
experience.astro
---
import Tabs from "../components/Tabs.astro";
const jobs = await Astro.glob("../data/experience/*.md");
const years = jobs.map((job) => job.frontmatter.year);
const descriptions = jobs.map((job) => job.compiledContent);
---
<!-- My component taking the data to be rendered -->
<Tabs headings={years} contents={descriptions} />
Tabs.astro
And the component renders the info like so
<!-- Tabs -->
<div class="tabs">
<ul class="tabs-header">
{
headings.map((heading) => (
<li>{heading}</li>
))
}
</ul>
<ul class="tabs-content">
{contents.map((content) => <li class="tab">{content}</li>)}
</ul>
</div>
My current solution
At the moment using .compiledContent gets me the correct HTML, however it is all in a string so the HTML doesn't actually render.
What I'm looking for
Is there a native way in Astro to pass markdown as a prop to a component?
If not is there a manual and recommended way in Astro to convert a markdown string and sanitise it to protect against XSS attacks? (if this is a risk in Astro when rendered statically?)
If not what are your most recommended ways to render markdown and sanitise it in JS?
Thanks so much for your time and help! I'm loving using Astro
p.s Also happy to concede and just use a <slot/> in my component if needed... ;)
Astro has a set:html directive you can use in combination with a Fragment like this
<Fragment set:html={post.compiledContent()}/>
After a bit of struggling with this myself, the current solution from the Astro docs for a single file without looping is the following.
Import your file with {Content as YourAliasName} from '../yourPath/yourFileName.md'
Then just use it as a tag <YourAliasName />
Example from the docs for reference:
---
import {Content as PromoBanner} from '../components/promoBanner.md';
---
<PromoBanner />
https://docs.astro.build/en/guides/markdown-content/#the-content-component

Vue slot not working - child elements show as empty array

I have the following code where I am trying to pass individual tab components into a tabs component via slots. However, the tabs do not seem to be getting passed. Putting {{ tabs }} on the template only shows an empty array.
<template>
<article>
{{ tabs }} // empty array
<header class="tabs">
<ul>
<li v-for="(tab, index) in tabs" :key="index">
<div class="nav-item"
:class="{ 'is-active': tab.isActive }"
#click="selectTab(tab)">
{{ tab.name }}
</div>
</li>
</ul>
</header>
<section class="tabs-details">
<slot></slot>
</section>
</article>
</template>
<script>
export default {
data: () => {
return {
tabs: []
}
},
methods: {
selectTab(selectedTab) {
this.tabs.forEach(tab => {
tab.isActive = tab.name === selectedTab.name;
});
}
},
created() {
console.log('created[Tabs.vue]-> ', this.$children) // nothing here
this.tabs = this.$children; // not working
}
}
</script>
I have my components registered in the parent like so:
import Tab from '#/components/Tab'
import Tabs from '#/components/Tabs'
export default {
components: {
Datepicker,
Tab,
Tabs
},
...
The template is pretty straight forward. Note, the contents of the individual tabs display fine when selected="true"
<Tabs>
<Tab name="hours" selected="true">Contents here...</Tab>
<Tab name="pay"></Tab>
</Tabs>
I have checked in the browser console and, although the nav-item <li> elements are being created, they have no content.
I'm new to slots so maybe I am missing something obvious or the code syntax I am using is outdated. Any help is much appreciated.
Try to use this.$slots :
this.tabs = this.$slots.default();//returns vnodes, that can be rendered via render function
$children has been deprecated in Vue 3:
In 3.x, the $children property is removed and no longer supported. Instead, if you need to access a child component instance, we recommend using template refs.
If you're trying to get children of <slot></slot>, use this.$slots.default(), as suggested in Boussadjra's answer.
In Vue2, the syntax for getting the default slot contents is this.$slots.default (not a function).
Notes:
you'll get back an array of VNodes, not actual DOM,
you can only access $slots after component has been mounted (e.g: in mounted() hook)
If you want specific data from the vNodes, you'll have to log them and see where exactly it is in your version of Vue (vNodes have had minor changes over time - they're considered internal API).
My advice would be to upgrade to a newer version of 2. If not 2.7.10, at least 2.6.14. Some of the available documentation on Vue 2 describes features which were not yet added in 2.2.3. $slots had a major revamp in 2.6.
For example, I was using slots at the time, but if you ask me now how they looked like, I couldn't tell you with certainty.

Close navigation drawer after click on item?

I'm working on a University project and trying to implement a feature that allows the navigation drawer to close whenever I click on one of the items in it. However, I'm not sure how to handle this.
<template>
<div id="navigation-mobile">
<Searchbar class="search"/>
<ul v-for="item in tabs"
:key="item.path"
active-class
#click="$router.push(item.path)"
>
<li>{{item.name}}</li>
</ul>
<div class="mobile-footer">
<ul>
<li>About us</li>
<li>Contact us</li>
</ul>
</div>
</div>
</template>
And here's what I have in App.vue, which contains a part of the nav-drawer:
<template>
<v-app id="app">
<NavBarMobile v-if="mobileView"/>
<div class="content" :class="{'open': showNav}">
<div style="height:20px"></div>
<div id="navigation-icon" v-if="mobileView"
#click="showNav = !showNav">
<v-icon medium dark>menu</v-icon>
</div>
<NavBar v-if="!mobileView"></NavBar>
<v-content class="pa-0" transition="slide-x-transition"></v-content>
<Footer v-if="!mobileView"></Footer>
<router-view></router-view>
</div>
</v-app>
</template>
This is my code so far. I would like to use a #click, I think that would be the most efficient way to do that, but I don't know if I can, since I'm already using it. I'm not very good at programming. Any suggestions?
Here's the codepen: https://codesandbox.io/s/gameshelf-0209-jack-forked-zobe5
You can find the component in NavBarMobile.vue
Thanks in advance for taking the time to read this post!
There are a few ways you can achieve this. The simplest, in my opinion, would be to simply "watch" the $route object from within your App.vue:
export default {
// ...
watch: {
$route(to, from) {
this.showNav = false
}
}
}
The watch property on the Vue instance contains functions that will watch for changes to variables of the same name. Upon change, the function is run.
More on watchers and computed properties: https://v2.vuejs.org/v2/guide/computed.html
EDIT: Some extra info about reacting to Router changes: https://router.vuejs.org/guide/essentials/dynamic-matching.html#reacting-to-params-changes
Another way to achieve it is by emitting an event from the child component.
first point the click event to a method to handle your logic like so :
<ul v-for="item in tabs" :key="item.path" active-class #click="redirect(item.path)">
then add a method in your script to emit a custom event from your NavBarMobile component :
methods: {
redirect(path) {
this.$router.push(path)
this.$emit('change', false)
}
},
and finally listen for this event from the parent component:
<NavBarMobile v-if="mobileView" #change="showNav = $event"/>

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.js render function in .vue file

I'm new with vue.js so forgive me if what I write does not make sense to you. It's not totally clear to me how to use render function inside a .vue file component.
I got a component in a .vue file like this:
<template>
<transition name="slide-fade">
<div class="drop-list" v-html="items">
</div>
</transition>
</template>
<style>
</style>
<script>
export default{
name: "drop-item",
props:['items'],
data(){
return{}
},
render(createElement) {
// create the list
}
}
</script>
Basically I have 3 component that alternately sends content ("items") to this one, mi goal is to render an unordered list inside it with a "#click='doSomenthing'" directive inside every list-element and "doSomething" depends on which is the component that sent the items to this one.
Any help will be appreciated
Firstly, you do not put render functions inside components, you simply pass the data as a prop. If you need to know which component passed the list of items, then you can simply pass a prop to let your component know what action to take, here's a basic example:
<template id="my-list">
<div>
<ul>
<li v-for="item in items">{{item}}</li>
</ul>
</div>
</template>
<script type="text/javascript">
export default {
props: ['items', 'action'],
methods: {
doSomething() {
switch (this.action) {
case 1:
console.log("I'm doing action 1");
break;
case 2:
console.log("I'm doing action 2");
break;
default:
console.log("I'm doing default action");
}
}
}
}
</script>
You can then set the component up in our parent and pass an action, I'm just passing a number here:
<my-list :items="items" :action="2"></my-list>
Here's a JSFiddle: https://jsfiddle.net/uckgucds/
If you are writing complex actions then you may want to write separate components for each list type, rather than a switch statement, you can then use a mixin to create the duplicate sections.

Categories

Resources