Truly Nested Components in Vue.js? - javascript

Is it possible to create semantically nested elements with Vue.js?
Example: let's say I'm building an 'accordion' element. Accordions are made up of a 'heading' and a 'content' section. The content can be toggled in and out of view by clicking the header. The final html for an element like this would be something like this:
<div class="accordion">
<div class="heading">
My Accordion
</div>
<div class="content">
Accordions are fun! Loren ipsum dolor sit amet.
This can be extensive text, include pictures, etc.
</div>
</div>
I would like to be able to create such elements in my html using syntax like this:
<accordion>
<heading>My Accordion</heading>
<content>
Accordions are fun! Loren ipsum dolor sit amet.
This can be extensive text, include pictures, etc.
</content>
</accordion>
The 'heading' and 'content' elements are not generic, and should only exist within the context of the parent 'accordion' element, so I believe they should be declared within the parent component's definition.
I know that in order to capture the innerHTML content of an element, we must use a <slot> element, so I tried using the following templates:
<template id="heading">
<div class="heading">
<slot></slot>
</div>
</template>
<template id="content">
<div class="content">
<slot></slot>
</div>
</template>
<template id="accordion">
<div class="accordion">
<heading></heading>
<content></content>
</div>
</template>
<div id="app">
<accordion>
<heading>My Accordion</heading>
<content>
Accordions are fun. Lorem ipsum dolor sit amet.
I could add a lot more text here, or other elements.
</content>
</accordion>
</div>
And the Vue javascript...:
Vue.component('accordion', {
template: '#accordion',
components: {
heading: {
template: '#heading'
},
content: [
template: '#content'
}
}
});
Vue({
el: '#app'
});
Unfortunately, it doesn't work. I've read the official documentation several times, and within the 'Components' section, when it talks about <slot> elements, it seems to indicate we should be able to do it - but I can't for the life of me work out how... The docs actually mention an element with a structure like this:
<app>
<app-header></app-header>
<app-footer></app-footer>
</app>
...but it doesn't give simple, concrete examples of how to do it.
The way the information is passed from parent to child element is confusing, and I have been unable to find any tutorials online that show how to setup a nested element like this. Any guidance would be greatly appreciated.

so <heading> and <content> should also be Vue components?
then it should look like this:
<div id="app">
<h5>Accordion test</h5>
<accordion>
<heading slot="heading">Heading Text</heading>
<content slot="content">Content Text</content>
</accordion>
<template id="accordion">
<div class="header">
<slot name="heading"></slot>
</div>
<div class="content">
<slot name="content"></slot>
</div>
</template>
<template id="heading">
<div class="template-heading">
<slot></slot>
</div>
</template>
<template id="content">
<div class="template-content">
<slot></slot>
</div>
</template>
</div>
and the JS:
var Heading = Vue.extend({
template: '#heading'
})
var Content = Vue.extend({
template: '#content'
})
var Accordion = Vue.extend({
template: '#accordion',
components: {
heading: Heading,
content: Content
}
})
Vue.component('heading', Heading)
Vue.component('content', Content)
Vue.component('accordion', Accordion)
var App = new Vue({
el: '#app',
data() {
return {
test: 'Test'
}
}
})
So:
The content of the <heading> element goes into the slot inside of
<heading>'s template.
And the whole template of <heading> goes into the <slot name="heading">inside the template of <accordion>
Working fiddle: https://jsfiddle.net/Linusborg/ud9a614o/

Related

Dynamic nesting of v-for loops in Vue.js

Not sure if this is possible, but worth a shot.
I'm trying to build a system where a set of components could be dynamically rendered, in the same component, in a dynamiclly nested set of v-for loops.
To give an example.
let's say I have a series of <div/>s and a json in this structure
list : [
['Test','Test],
['Test2', 'Test2],
['Test3','Test3],
]
This would render:
<div>
<div>
<div>
Test
</div>
<div>
Test
</div>
</div>
<div>
<div>
Test2
</div>
<div>
Test2
</div>
</div>
<div>
<div>
Test3
</div>
<div>
Test3
</div>
</div>
</div>
However, if I added another set of nesting,
e.g.
list : [
['Test','Test],
['Test2', 'Test2],
['Test3',['Test4', 'Test4']],
]
It would render like this,
<div>
<div>
<div>
Test
</div>
<div>
Test
</div>
</div>
<div>
<div>
Test2
</div>
<div>
Test2
</div>
</div>
<div>
<div>
Test3
</div>
<div>
<div>
Test4
</div>
<div>
Test4
</div>
</div>
</div>
</div>
I know how this could be acomplished with a fixed level of nesting
e.g.
<div>
<div v-for="(item,index) in list" :key="index">
<div v-for="(subItem, subIndex) in item :key="subIndex">
{{subItem}}
</div>
</div>
</div>
However I am unsure on how I could make it react to a dynamic level of nesting if it had to be interpreted at runtime.
If anyone has any ideas on this I would appreciate the help.
Edit: to clarify, the solution I would be looking for would work for a unkown amoint of nesting.
Create two components <list> and <list-item> and in <list-item> call <list> recursively if item props is an array else render a normal <div> tag.
Like this:
Vue.component('list', {
props:["items"],
functional: true,
render: function(createElement,{ props, children }){
return props.items.map((item)=>createElement('list-item',{ props: {item} }))
}
});
Vue.component('list-item', {
props:["item"],
template: '<div v-if="isArray(item)"><list v-bind:items="item"> </list> </div>'+
'<div v-else> {{item}} </div>',
methods:{
isArray:function(item){
return Array.isArray(item);
}
}
});
var vm = new Vue({
el: '#app',
data:{
items:[
['Test','Test'],
['Test2', 'Test2'],
['Test3',['Test4', 'Test4']],
],
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>
<list v-bind:items="items"></list>
</div>
</div>
In order to achieve this..you may try out the use of components and callback with own components for nested looping.
Is just an idea I not sure its work or not.

How to render component children at parent

I'm familiar with ReactJS, but not with VueJS.
Where can I place the component children at the parent component.
I have this example in ReactJS, how can I create the same using VueJs:
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
What is the {props.children} in VueJS ??
The Vue analogy to the React "children" concept is the slots.
https://v2.vuejs.org/v2/guide/components.html#Content-Distribution-with-Slots
https://v2.vuejs.org/v2/guide/components-slots.html
Slots can be used like:
// FancyBorder.vue
<div class="FancyBorder">
<slot/>
</div>
// App.vue
<FancyBorder>
Contents!
</FancyBorder>
Vue slots also have an advantage over the React children, as you can have multiple "groups" of elements, using named slots, this can make the code less reliant on "magic" classnames:
// FancyBorder.vue
<div class="FancyBorder">
<h1 className="Dialog-title">
<slot name="header"></slot>
</h1>
<slot/>
</div>
// App.vue
<FancyBorder>
<template slot="header">
Header
</template>
Contents!
</FancyBorder>

Pass component as a property and respect its named slots in Vue.js

I want my container component to render contents based on child's slot names. Here's an example markup:
<div id="app">
<Container>
<component :is="childComponent"></component>
</Container>
</div>
<script>
import Container from './Container'
import someComponent from './someComponent'
export default {
components: {
Container,
someComponent
},
data() {
childComponent: 'someComponent'
}
}
</script>
// Container.vue
<template>
<div>
<header>
<slot name="head"></slot>
</header>
<div class="ContainerBody">
<slot name="body"></slot>
</div>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
// Some child component
<template>
<div>
<h1 slot="head">Child Title</h1>
<div slot="body" class="body"><div>Child Body</div></div>
<footer slot="footer">Child Footer</footer>
</div>
</template>
How do I make it that Vue respects slot names and renders child contents in accordingly named slots, so the result would look like this:
<div>
<header>
Child Title
</header>
<div class="ContainerBody">
<div>Child Body</div>
</div>
<footer>
Child Footer
</footer>
</div>
Right now it will only render my child component in an unnamed slot:
<slot></slot>
The idea is to render child component differently when it's loaded as a child of Container. I would like it to work with .Vue files and allow child components to still behave as usual when they're not a child of a container.
I don't think you can do exactly what you want because each component has to have a single root element, which precludes it being plugged in as three separate slots.
What I was able to do was to turn the problem inside-out, making the top-level component the childComponent and having it take a container prop which it uses to set the :is of its root element.
// "importing" async component definitions
const vueContainerComponent = () => new Promise((resolve) => resolve({
template: '#container-template'
}));
const vueChildComponent = () => new Promise((resolve) => resolve({
template: '#child-template',
props: ['container']
}));
new Vue({
el: '#app',
components: {
someComponent:() => vueChildComponent().then((spec) => ({
extends: spec,
components: {
Container: vueContainerComponent
}
}))
},
data: {
container: 'Container',
childComponent: 'someComponent'
}
});
header {
background-color: lightgray;
}
footer {
background-color: darkslategray;
color: white;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<component :is="childComponent" :container="container"></component>
</div>
<template id="container-template">
<div>
<header>
<slot name="head"></slot>
</header>
<div class="ContainerBody">
<slot name="body"></slot>
</div>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<template id="child-template">
<div :is="container">
<h1 slot="head">Child Title</h1>
<div slot="body" class="body"><div>Child Body</div></div>
<footer slot="footer">Child Footer</footer>
</div>
</template>

Rendering <ng-content> in angular 2 more times

I have code like this
<template *ngIf='mobile'>
<div class='wrapper'>
<div class='nav'>
<ng-content></ng-content>
</div>
</div>
</template>
<template *ngIf='desktop'>
<ng-content></ng-content>
</template>
However, Angular 2 renders ng-content just one time. Is there a way to get this case working properly without too much hacking?
update Angular 5
ngOutletContext was renamed to ngTemplateOutletContext
See also https://github.com/angular/angular/blob/master/CHANGELOG.md#500-beta5-2017-08-29
original
You can pass the content as a template, then you can render it multiple times.
<parent>
<template>
projected content here
</template>
</parent>
In parent
<ng-container *ngIf='mobile'>
<div class='wrapper'>
<div class='nav'>
<template [ngTemplateOutlet]="templateRef"></template>
</div>
</div>
</ng-container>
<template *ngIf='desktop'>
<template [ngTemplateOutlet]="templateRef"></template>
</template>
export class ParentComponent {
constructor(private templateRef:TemplateRef){}
}
You can also pass data to the template to bind with the projected content.
<ng-container *ngIf='mobile'>
<div class='wrapper'>
<div class='nav'>
<template [ngTemplateOutlet]="templateRef" [ntOutletContext]="{ $implicit: data}"></template>
</div>
</div>
</ng-container>
<ng-container *ngIf='desktop'>
<template [ngTemplateOutlet]="templateRef" [ntOutletContext]="{ $implicit: data}"></template>
</ng-container>
export class ParentComponent {
#Input() data;
constructor(private templateRef:TemplateRef){}
}
and then use it like
<parent [data]="data">
<template let-item>
projected content here {{item}}
</template>
</parent>
See also My own Angular 2 Table component - 2 way data binding

Does Aurelia support transclusion?

I'm familiar with the concept of ngTransclude via AngularJS and this.props.children via ReactJs, however does Aurelia support transclusion, that is, the notion of inserting, or transcluding, arbitrary content into another component?
Transclusion in AngularJS (https://plnkr.co/edit/i3STs2MjPrLhIDL5eANg)
HTML
<some-component>
Hello world
</some-component>
JS
app.directive('someComponent', function() {
return {
restrict: 'E',
transclude: true,
template: `<div style="border: 1px solid red">
<div ng-transclude>
</div>`
}
})
RESULT
Transclusion in ReactJs (https://plnkr.co/edit/wDHkvVJR3FL09xvnCeHE)
JS
const Main = (props) => (
<SomeComonent>
hello world
</SomeComonent>
);
const SomeComonent = ({children}) => (
<div style={{border: '1px solid red'}}>
{children}
</div>
);
RESULT
Several ways to do transclusion: Official docs
1. content slot <slot></slot>
The <slot> element serves as a placeholder in a component's template for arbitrary content. Example:
some-component.html
<template>
<div style="border: 1px solid red">
<slot></slot>
</div>
</template>
usage:
<template>
<require from="some-component"></require>
<some-component>
hello world
</some-component>
</template>
result:
2. named slots
A component can contain several replaceable parts. The user of the component can replace some or all of the parts. Parts that aren't replaced will display their default content.
blog-post.html
<template>
<h1>
<slot name="header">
Default Title
</slot>
</h1>
<article>
<slot name="content">
Default content
</slot>
</article>
<div class="footer">
<slot name="footer">
Default footer
</slot>
</div>
</template>
usage:
<template>
<require from="blog-post"></require>
<blog-post>
<template slot="header">
My custom header
</template>
<template slot="content">
My custom content lorem ipsum fla fla fla
</template>
<template slot="footer">
Copyright Megacorp
</template>
</blog-post>
</template>
3. template parts
The slots spec has limitations: http://aurelia.io/hub.html#/doc/article/aurelia/templating/latest/templating-content-projection/5
Use template-parts for dynamically generated slots: https://github.com/aurelia/templating/issues/432
Yes, Aurelia supports the concept of transclusion through use of the <content /> component. Per the below, any content nested within <some-component> be it HTML, a String, or another component, will be rendered within this component.
app.js
export class App {}
app.html
<template>
<require from="some-component"></require>
<some-component>
hello world
</some-component>
</template>
some-component.js
export class SomeComponent {}
some-component.html
<template>
<div style="border: 1px solid red">
<content />
</div>
</template>
RESULT
UPDATE: USE SLOT INSTEAD OF CONTENT
// Actual component
<your-component>
Just some content
</your-component>
// Template of the component
<template>
<div class="some-styling">
<slot></slot> // <-- "Just some content" will be here!!
</div>
</template>

Categories

Resources