This article is about a piece of knowledge that I would like to share, because it cost me a couple of hours of research and I think the solution was pretty well hidden on the web, at least for my search terms.
Imaging, you are building a web-app and you use some kind of component framework, in order to split your UI into components. Furthermore, you a have a component that renders a grid with three columns.
.my-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
This child component that is rendered into the grid is relatively straight forward, unless you need to render multiple HTML elements in one component. This might happen, if the elements are semantically related and use the same data or even interact with each other.
In React, you would use a component that has React.Fragment
as root element:
import React from 'react';
export function App(props) {
return (
<div className='App' style={{display: "grid", gridTemplateColumns: "1fr 1fr 1fr"}}>
<MyCompoment/>
<MyCompoment/>
<MyCompoment/>
</div>
);
}
function MyCompoment() {
return (
<React.Fragment>
<div>a</div>
<div>b</div>
</React.Fragment>
)
}
This will render as
aba
bab
and MyComponent does not have a single root element.
In Vue.js 3.x you would just use a <template>
with multiple elements:
// App.vue
<script setup>
import MyComponent from "./MyComponent.vue"
</script>
<template>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr;">
<MyComponent />
<MyComponent />
<MyComponent />
</div>
</template>
// MyComponent.vue
<script setup></script>
<template>
<div>a</div>
<div>b</div>
</template>
This will achieve the same thing.
In Angular.js there is no out of the box solution, at least none that I could find on the web. By default, every component is wrapped by a custom HTML element that represents the component. For example, this component
<div>with host element 1</div>
<div>with host element 2</div>
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-with-host-element',
templateUrl: './with-host-element.component.html',
styleUrls: ['./with-host-element.component.css']
})
export class WithHostElementComponent {
}
will always render as
<app-with-host-element>
<div>with host element 1</div>
<div>with host element 2</div>
</app-with-host-element>
and there is no builtin way to omit the host element. Because of this, it is not possible to render multiple HTML tags with a single component, i.e. without messing up with the grid-layout.
…or is it?
It took me a while to find out what to look for: The root element of a component is called “host” element, so googling for “root” did not find helpful results. So, in order to find this, we have to ask the following questions:
I finally found this Stack Overflow Post that provided exactly the solution I needed.
<ng-template #template>
to render to contents into an NgTemplateOutlet
.@ViewChild
to gain access to that outlet from within the component.ViewContainerRef#createEmbeddedView()
to render the outlet contents directly
embedded into the parent container.display: none
style.Template:
<ng-template #template>
<div>outside host element 1</div>
<div>outside host element 2</div>
</ng-template>
Component:
import {Component, OnInit, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
import {NgTemplateOutlet} from "@angular/common";
@Component({
selector: 'app-outside-host-element',
templateUrl: './outside-host-element.component.html',
styleUrls: ['./outside-host-element.component.css']
})
export class OutsideHostElementComponent implements OnInit {
@ViewChild('template', { static: true })
template!: TemplateRef<NgTemplateOutlet>;
constructor(private viewContainerRef: ViewContainerRef) {
}
ngOnInit(): void {
this.viewContainerRef.createEmbeddedView(this.template);
}
}
CSS:
:host {
display: none;
}
I have set up an example repository on GitHub and also deployed the output to GitHub pages, if you want to take a look.
Note that I am using an older version of Angular, so it works at least with Angular 13, probably earlier versions as well.
When researching this, I was wondering if there is no native CSS solution to this. And indeed there is.
There is a display: contents style that replaces an element by a pseudo-box, and acts as if it’s child elements are directly rendered inside the parents. caniuse.com mentions that “buttons are not accessible”. It may also be a problem when JavaScript is used to modify elements, because the element is effectively still there.
For me, it did not work correctly, but it’s worth mentioning that it exists.
The following image shows the result of the different approaches:
I solved my problem with TemplateOutlets and ViewContainerRefs and I hope the mention of React. Fragment and multiple elements in Vue will make this post easier to find, this helping you find a solution to it more quickly than I did.
But there is a disclaimer: I am not an Angular expert. This solution might have caveats that I have not seen yet and there might be better solutions out there. If you know more than I do, or if this does not work out for you, please contact me, preferably by opening an issue at the example repository.