Angular for the headless web — dynamic component rendering and content-driven apps
A mindful Angular serves your headless CMS.
In a previous story, I presented a concept and an implementation draft for rendering component trees from generic content sources. In this follow-up story, we will look at code samples and a live demo of such a content-driven app. We are going to walk over code listings step by step and discuss why a hypermedia format is a good fit for the purpose.
Thanks to Angular’s architecture, the web frontend is decoupled from the content source, making it a viable solution for integration with a headless CMS. Since a number of CMS’es are nowadays offering JSON APIs, it also means that the solution is CMS-agnostic. Whether you are running an open-source Wordpress, hosting on Contentful, Directurs, or CloudCMS, or even paying greenback dollars for proprietary silicon valley licenses — this may be of interest to you!
Hypermedia as the model of content
Let’s recall our mental model of content from the previous story. We supposed that (web) content is published and consumed in a hypermedia format.
Hypermedia includes different types of media formats such as text, images, video, audio, and many more. In that model, an hypermedia document is a container format for embedding multi-media content. Elements of content are connected with each other through hyperlinks.
Implementing Angular components
Now, when implementing Angular components in a content-driven approach, we are going to implement a dedicated component per each content type. Let’s look at that by example.
Content type “image” — ImageComponent
With that mindset, we are going to implement an ImageComponent
to render content elements of type “image”. Below is a JSON sample and the Angular component:
The JSON object is passed as values
parameter to the contentOnCreate()
lifecycle hook. In the implementation of this method, the component needs to set its properties that are bound in the HTML template.
Then, additional metadata needs to be wired up as described in the previous story. The recommended way is to create a dedicated Angular module, e.g. ContentModule
as shown in the code example below.
The mapping between the _type
property and the component's constructor is set up in the MAP
constant. The map is an object literal value and provided with the DI token CONTENT_MAPPING
.
Additionally, ImageComponent
needs to be added to declarations
and entryComponents
for dynamic component loading to work.
In this pattern, it becomes easy to add new types of content. May that be a simple HeadlineComponent
, a RichTextComponent
for rendering markdown content, or a MarketingTeaserComponent
, or even more complex things like a FeaturedStoryComponent
in a blogging application or a CheckoutComponent
in an online shop or e-commerce site.
Content as a tree structure — embedded resources
In this paragraph, rendering of a tree structure — or parent-child relations between content elements — is demonstrated by the example of a grid.
Since the grid consists of a container with multiple rows, and multiple columns attached to a row, we are going to implement a total of three Angular components: a GridComponent
, a GridRowComponent
, and a GridColumnComponent
.
Implementing three components for a grid may look a bit “over-complicated” but is actually required to represent the parent-child relation properly. As mentioned in the last story, this is probably the one biggest downside of this approach.
In the JSON source, the parent-child relation is represented by the means of the _embedded
property. In the following example, the grid has three rows (second and third row are shown in a short form) and the first row has two columns:
By implementing the ContentEmbeddable
interface GridComponent
declares that it is capable of showing embedded content.
Child components will be created and attached to a designated slot in the DOM— this slot must be a ViewContainerRef
and is returned by the contentEmbeddable()
method. Please note that a ng-container
element should be used for that purpose. A reference to that element is obtained by the ViewChild
decorator and the template-reference variable #embed
.
Now, GridRowComponent
and GridColumnComponent
are implemented in the same style:
Obviously, all three components need to be declared by ContentModule
and added to the CONTENT_MAP
as was shown above. For brevity, this part is omitted from the code listing.
A more sophisticated sample
The following example demonstrates a composition of diverse content types rendered by a HeadlineComponent
, an ImageComponent
, and a grid layout with GridComponent
, GridRowComponent
, and GridColumnComponent
.
The screen above was rendered from the following JSON source file. This shows how flexible and versatile the solution is for composing content.
Rendering another page on the screen is simply a matter of feeding a JSON source file to the application. Think of modern, so-called headless content-management systems: content is fetched via a JSON API over HTTP, then an Angular app is going to turn it into a rich visual experience.
An Implementation Detail – how ViewContainerRef works
You may have noted that the slots for embedded content are created with ng-container
elements in the DOM. There are two aspects of this that should be clarified.
First, ng-container
has no visual output in the DOM. It serves as a kind of “placeholder” and kept in a “virtual” place internally by Angular. “Grouping sibling elements with <ng-container>” is also the recommended approach in the Structural Directives guide.
Second, a ViewContainerRef
of these containers is obtained and used for creating child components.
Even though the Angular docs say at some point that a container “inserts [the component’s] Host View into this Container”, elements actually become siblings in the DOM, not children.
This behaviour is in accordance with the documentation stating that “root elements of Views attached to this container become siblings of the Anchor Element in the Rendered View” — how nebulous that phrase may be, it is what it is.
Design considerations – why HAL?
In all examples above, the so-called Hypertext Application Language (HAL) was used as the format for JSON files. Why is HAL such a good fit for content-driven apps?
HAL is a first-class citizen for hypermedia APIs, since it is designed around two reserved property names: _embedded
and _links
. HAL specifies three entities: Resources, Embedded Resources, and Links. Let’s look how these fit into the model of content.
A tree of content — embedded resources
The idea of embedded resources fits perfectly to the idea of content viewed as a tree structure. Embedded resources in a HAL document express parent-child relations. A tree of content has such parent-child relations.
Recall the samples presented here: a grid has one (or more) rows, a row has one (ore more) columns, a column has a number of arbitrary child content (e.g. text, image, video). In the HAL model, each of these content elements is represented as a resource and the tree structure is reflected by embedding child resources into parent resources. In effect, embedded resources are directly mapped to the ContentEmbeddable
interface in the implementation.
Hyperlinks — navigation and browsing for the web
In the content model described in the previous story, connections are also established through links. Hyperlinks allow navigation from one piece of content to another.
In HAL, hyperlinks are a native ingredient through the _links
property and can be set for any resource; on the root resource as well as on embedded resources. Then, each resource should have a self
URI that identifies the resource, allowing to create a connection from any resource as well as to any resource.
Summary
The concept and implementation draft from the previous story was put into live in this part. We demonstrated how to create components for individual pieces of content and how to serve generic content documents in an Angular app.
Adding content components is as simple as writing any other Angular component, plus two additional steps: implementing a lifecycle hook by means of the ContentOnCreate
interface and registering a CONTENT_MAPPINGS
provider. We also showed how to create deep-nested structures of content and how they are transformed to and rendered by a tree of Angular components.
Finally, we used the hypermedia abstraction language (HAL) as a format for content and discussed why it is such a good fit for the purpose. Source code of the proof-of-concept is attached below and hosted on GitHub!
Hyperlinking will be shown in the final part of this story! Stay tuned! In case you enjoyed reading, spread the word! ♡ ❤ 💜 💝