This is part 2 of 3 in a series titled "How do we React?" in which I discuss how Aurelia handles common React scenarios.
The other week Sebastian Markbåge posted the following tweet :
You may have noticed that most other frameworks don't have HoCs, render props or anything like React.Children. These account for a lot the differences between React and other frameworks. How would you solve these use cases if you had to switch to [other framework]?
In this post, we'll cover Render Props. For a discussion of how Aurelia handles typical HoC scenarios, please see part 1 or for information on how we handle React.Children, see part 3.
Render Props
The React site defines render prop as follows:
The term "render prop" refers to a technique for sharing code between React components using a prop whose value is a function.
In React practice, the consumer of a component passes a function along to the component which will later be called by that component to customize rendering inside that component. Ultimately, this is a functional technique similar to the OOP template method pattern that is used to allow end users to customize how a component renders.
So, how does Aurelia support templatization of component rendering?
Slots
In Aurelia, a component is typically made up of two parts: a plain JS class that forms the view-model of the component and a web standards-based HTML template that forms its view. Because Aurelia adopts and champions the use of web standards wherever possible, its templates support Shadow DOM slots.
In case you aren't familiar with or haven't worked with slot-based composition before, I'll provide a brief explanation. Let's say you want to create a "dialog" component. It's a simple component that displays a modal dialog UI, wrapping arbitrary content, and providing an optional header. Our view model might look something like this:
import { bindable } from 'aurelia-framework';
export class ModalDialog {
@bindable title;
}
This view model would be paired with the following view:
<template>
<div>
<div if.bind="title">${title}</div>
<div class="content">
<slot></slot>
</div>
</div>
</template>
And you would use it like this:
<modal-dialog title="Aurelia">
<span>Any HTML you want goes here, including custom components.</span>
</modal-dialog>
Now, when I use modal-dialog
, the content of the element, in this case the span
, will be rendered at the location of the slot
. The two DOM trees are affectively composed together into the following visual tree:
<modal-dialog>
<div>
<div>Aurelia</div>
<div class="content">
<span>Any HTML you want goes here, including custom components.</span>
</div>
</div>
</modal-dialog>
If you're familiar with React, then you know that one way this type of thing can be accomplished is through a Render prop. In React's case, the modal-dialog
component would expect a function-type prop to be passed which it would then invoke during its render method, to provide the custom content. With Aurelia, it's all declarative HTML. Simply use a slot
element to mark the location where the consumer's content should be rendered, and the consumer just uses your element like a normal HTML element. Any content placed inside its tag automatically gets "projected" to the slot's location.
In React, an alternative approach to handling this scenario is through props.children
. We'll be covering that in part 3 of this series.
Shadow DOM slots (and thus Aurelia) can do much more than this though. What if we wanted the end user to be able to provide arbitrary HTML for the title
as well, but still allow for a simple text property to be set? We just change the template to use a named slot with fallback content.
<template>
<div>
<slot name="title">
<div if.bind="title">${title}</div>
</slot>
<div class="content">
<slot></slot>
</div>
</div>
</template>
We could then use it like this:
<modal-dialog>
<span slot="title">Any HTML for the title goes here.</span>
Any HTML for the content goes here, including custom components.
</modal-dialog>
With the above, we can render custom HTML content into the "title" slot. However, if we don't provide "title" slot HTML, the Aurelia template will fallback to rendering the default title div
which has its content bound to the title
property.
All of this is standards-based, accomplishable through declarative HTML, and merely scratches the surface of what slots can do. By combining default and named slots, fallback content, and even the ability to project slots through to other slots, the possibilities are virtually limitless.
Influences
It's not quite correct to say that Aurelia is influenced by Shadow DOM. Rather, Aurelia has adopted the Shadow DOM standard as a core feature of its component composition model. We've worked hard to provide slots in their pure form, not altering their behavior from the standard as a couple other frameworks do. This means that if you learn how slots work in Aurelia, you're also correctly learning web standards, investing in your long-term technical knowledge and growth, which is great for your career, even if you aren't working with Aurelia yet in your day job.
It's worth mentioning Xaml-based UI frameworks, such as Windows Presentation Foundation, Silverlight, and Microsoft's UWP platform, have an earlier manifestation of this same concept. The Xaml
ContentPresenter
functions almost like a default slot, designating where in the visual tree any element content should be rendered. It even has the notion of a ContentSource
, which provides a mechanism similar to named slots. Similarly, the
ItemsPresenter
handles rendering multiple content elements, including the ability to determine their layout and even wrap them with custom wrapper templates. The Web's Shadow DOM spec doesn't have something quite like this, but Aurelia does, which leads me to template parts...
Template Parts
Shadow DOM slot projection is powerful, but it doesn't handle all the scenarios an app might require. For example, imagine that instead of projecting content, you want to pass a template through to another element, so that this other element can use your template to render conditionally or repeatedly. This is something that React uses render props for, but Aurelia accomplishes this with declarative template parts.
As an example, let's imagine we're building a custom drop-down component. This component will display a list of items, based on data, and then track which item is selected by the user. To make things reusable, we want the consumer of our drop-down to be able to provide a template that can be used to render each item in the list. Here's an abbreviated version of what our drop-down component's view-model might look like:
export class DropDown {
@bindable items;
@bindable selectedItem;
listIsOpen = false;
// behavior elided
}
We pair that with this HTML view:
<template>
<div>
..elided...
<div if.bind="listIsOpen">
<ul class="options">
<li repeat.for="item of items">
<div replaceable part="list-item-template">
${item.toString()}
</div>
</li>
</ul>
</div>
</div>
</template>
Now, if we wanted to use this to render a drop down list of people, we would do it like this:
<drop-down items.bind="people"
selected-item.bind="selectedPerson">
<template replace-part="list-item-template">
<span>${item.lastName}, ${item.firstName}</span>
</template>
</drop-down>
Normally, the drop-down list would render a div
for each person, with that div
's content set to whatever toString()
resulted in for each person. However, because we marked the div
with the attribute replaceable
and then gave it a part
name, the consumer of the drop-down can easily provide their own replacement for that part of the component's view by specifying a template and telling it to replace the part with the same name. Nice!
You can have any number of template parts in an Aurelia component, as long as you give each a unique name. Furthermore, each template part gets access to the binding scope of the part it's replacing (e.g. the current item during a list render) along with the lexical scope in which the replacement part is declared. This allows for providing list item templates that have interaction behaviors that are also supplied by the consumer. All that's required is a couple of HTML attributes.
Influences
The chief inspiration for this again comes from Xaml. It's partially inspired by what the above-mentioned ItemsPresenter
can do, but also by a feature of the same name, template parts, in Xaml itself, which was designed to enable designers to completely re-skin components with a new view, without having to re-write all the behavior.
Collectively, Shadow DOM slots and Aurelia template parts handle nearly all the scenarios that Render props are typically used for. But, Aurelia has a few more related items that are worth mentioning.
The Call Binding
As it turns out, you can always directly pass a function into an Aurelia component as well, to do just about anything you need. We do this with the call
binding. One interesting use case for this can be seen in Aurelia's
virtual repeater
. This is an official Aurelia plugin that enables efficient rendering of hundreds of thousands of data elements, by utilizing UI virtualization techniques. This plugin can also handle infinite scrolling scenarios. All you have to do is provide it with a function to "call" whenever it needs to load your next set of data elements. Here's how it's used:
<template>
<div virtual-repeat.for="person of people" infinite-scroll-next.call="getMore($scrollContext)">
${$index}. ${person.lastName}, ${person.firstName}
</div>
</template>
In this case, we display the index of each person, along with their last and first names. We may have thousands of elements in our database, but we probably only want to load the first few dozen to populate the people
list. However, as the user scrolls, we want to load successive pages. The virtual-repeat
handles all the complexities and the call
binding enables the consumer to pass a function reference to the virtual-repeat
which it can then call whenever it needs the next set of items. Notice also that the virtual-repeat
can define custom variables, such as $scrollContext
which it can pass to your function, to give you the proper contextual information you need to load the right page of data.
The Compose Element
I wanted to share one more related feature of Aurelia: the compose
element. This feature could have been discussed in our post on how Aurelia handles React HoC scenarios, but I wanted to wait until now, so our readers could see how compose
can enable both HoC and Render Prop scenarios.
So, what is this compose
thing? Well, it's a special component that Aurelia ships with out-of-the-box, that enables dynamic rendering of other components, based on data. Over the years, I've sometimes referred to this as "polymorphic UI composition".
Imagine that you've got a heterogeneous list of data items that you want to render, but each one of them needs to be rendered with a different component. Perhaps which component renders each item is dependent on the type of the data. For many programmers, their first inclination is to reach for an if/else or switch/case, but that leads to a system that isn't naturally open for extension. What this means is that any time you add a new type to your data model, you've got to go modify the existing set of if/else or switch/case statements. Generally speaking, you should build systems that enable you to avoid ever modifying working code. Making changes to things that work is a fantastic way to introduce bugs and regressions.
With compose, you can simply loop over the heterogeneous list, "composing" each item in the list, which allows it to polymorphically render itself, based on type and convention. Let's say we have an app with a list of shapes like this:
export class App {
shapes = [
{
type: 'circle',
radius: '10px'
},
{
type: 'rectangle',
width: '10px',
height: '5px'
}
];
}
We could render it polymorphically by using compose
in its view, like this:
<template>
<ul>
<li repeat.for="shape of shapes">
<compose view-model="shapes/${shape.type}" model.bind="shape"></compose>
</li>
</ul>
</template>
Now, if we've got a shape with type "circle" we'll render it with the "circle" component in the "shapes" folder. If we've got a shape with a type of "rectangle", then that gets rendered with a "rectangle" component. Each component also gets passed the shape data model instance. To extend the system, we never need to modify this code; we only need to add new components for new types. Hopefully you can see this as a nice example of the open-closed principle . It's particularly handy for building dashboard apps, where the user has a configurable set of "widgets" which are loaded from a database, but I've found this to be an elegant solution for many seeming complex challenges.
Influences
Aurelia's compose
element is based on a feature from an earlier framework of mine called Caliburn, which I first started working on in 2005. Caliburn was a WPF framework that had a special Xaml attached property called View.Model
which you could bind to any object. Based on the object's type, it would polymorphically render the data using the correct view or view/view-model pair. It was at this time that I started to leverage the MVVM pattern extensively by composing view models upon view models. The net effect of this was that you could write your entire application in a "headless" way, with no dependency on the front-end framework or UI toolkit. You would end up with a plain set of classes/objects that represented everything the app could do, all composed together. I've sometimes referred to this as "hierarchical view models" or "composite presentation model". A side benefit of this approach is that it's amazingly easy to test and you wind up with a full automation API and plugin model for your app as a side-effect. The architecture also scales linearly to any application and team size. The View.Model
property of Caliburn and the compose
element of Aurelia are the primary framework features that have enabled this architectural style over the years. Discovering these patterns in the early days of WPF completely changed the way I think about building front-ends.
Wrapping Up
Shadow DOM slots, template parts, the call
binding, and the compose
element are all tools that enable Aurelia developers to handle the same scenarios as React render props (and HoCs too). I've consistently used these and similar techniques for over a decade (along with thousands of other engineers) to build countless front-ends, both simple and extremely complex, and they've held up time and time again.
I hope you've enjoyed seeing how Aurelia provides its own approaches to the scenarios typically handled by React render props. If you did, check out Part 3 of this series, where we discuss Aurelia's approach to React.Children. I think you'll enjoy it.
See ya next time!