Teleporting Content In Angular
Breaking Free From The Top Down Model
Angular is a brilliantly designed framework and we’re (usually) glad of structuring our web application by its rules.
Every Angular’s application is like a tree, so, that the parent template contains the children components and every child displays its content within its view only. And that’s usually fine.
However, what if we could enable children components to break through the limits of their own views, so, for them to display some of their widgets on a parent toolbar or pieces of information on a global status bar without data binding restrictions of any sort?
That’s what I call “teleporting” content.
Our Use Case
To demonstrate this idea, let’s imagine the following use case:
The app component, at the root level, implements a simple navigation bar with common links from one side and a toolbar on the other side.
The toolbar is not implemented by the app component itself since we want every page to have its own custom toolbar. It makes use of a
wm-portal component, instead, where the toolbar content will materialize.
Each page, then, implements its own toolbar to be teleported:
In this example, our page has a counter that can be incremented, decremented and cleared by the toolbar.
The toolbar content is defined within a
ng-template using the
wmTeleport directive to target the ‘toolbar’ portal, so, whenever this page will be activated the content of the toolbar will be teleported back to the parent app.
However, the toolbar template will still remain part of this page for data inputs and event to be bound to and that’s what make this technique really powerful.
The Teleport Service
The content transfer is possible thanks to a TeleportService connecting the directives to the portals:
The service is implemented as an injectable BehaviorSubject streaming TeleportInstance(s) where the TemplateRef to transfer is paired with a key telling which portal we want the template transferred to.
activate() method pushes a new instance along while the
clearAll() method pushes null for all the portals to clear-up.
The Portal Component
Every portal has a unique name to be addressed with:
Here we make use of the
@Attribute() decorator to get the name of the portal assuming this will remain static once defined.
The component builds an observable out of the TeleportService filtering those instances targetting this very portal and mapping the instance to its contained TemplateRef.
The incoming template is then rendered making use of the ngTemplateOutlet structural directive wrapped in a
ng-container where the
template$ observable is resolved using the
async pipe (see line #3).
As the name suggests, the
context input accepts an object to be provided as the template context, so, for the portal to optionally supply the template with destination-specific variables.
Note that ngTemplateOutlet directive accepts null templates as valid, simply clearing up the view in such cases.
The Teleport Directive
The last piece of our teleporting mechanism is a directive collecting the content to transfer:
The directive is designed to work in conjunction with
ng-template pseudo-elements only.
The portal name to target comes from its primary input, so, potentially changing during the execution. That’s why we make use of the
ngOnChanges() life-cycle hook to monitor the input changes clearing-up the previous target (if any) and pushing the template to the new target portal for rendering.
Last, but not least, we clear-up the target portal within the
ngOnDestroy() life-cycle hook, so, for the teleported content to disappear together with its source container.
And this is how we created a pattern letting you define content somewhere to be rendered somewhere else. This really sounds like teleporting to me.
There are many advantages to this approach:
- Widgets are defined within the very components they need to interact with.
- The rendering can take place everywhere in the app, no matter the hierarchy.
- Angular takes care of everything for us from change detection to view update despite we are kind-of cheating its component/view model.
Try it Yourself
The code is also available in a fully functional live demo on StackBlitz you can play with: