Hybrid Headless Approach
Heads Up!
This article is several years old now, and much has happened since then, so please keep that in mind while reading it.
Modular Implementation
Design
A truly modular design consists of blocks/modules/widgets, where each item may be placed on any page in any combination. As usual, we need to consider the backoffice editor experience, how the CMS models render, and also how interactive modules behave on the page. As part of this, the chosen javascript framework also needs to be flexible and modular, where components can be added multiple times in any order on the page.
VueJS is a lean javascript framework which is nice to use for interactive components. The library is relatively easy to understand, meets HTML standards, and the Vue components match up with how we want to build a modular UI. When UI developers work on individual components, they create “templates” as .vue files. The UI developer uses vue-loader and vue-template-compiler in Webpack to compile the .vue files to a javascript file.
In the following example, Vue components are small chunks of markup. The framework replaces these chunks with the Vue template’s HTML when Vue executes. The code and functionality can be isolated to the individual components, which means that Vue’s components can be repeated in any order and combination as required.
<div>
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
For more examples, check out the VueJS documentation: https://vuejs.org/v2/guide/components.html#Reusing-Components
The Backoffice Experience
To ensure that the editor experience is intuitive and consistent, it is important to use consistent property editors. The contenders for Umbraco 8 (at the time of writing) were Nested Content, and the Grid. Both of these Backoffice data components use Umbraco’s Element types, which allow us to create a modular experience for the editors.
In our implementation, we settled on Nested Content. Each page is given a field named `body`, of type Nested Content. The added benefit of using Nested Content is that Models Builder can be used to map the module content to strongly typed Models, and hence resulting in cleaner MVC partials. Below is an example of our implemented modules. The modules that are added to the corresponding Nested Content data type each correspond to an MVC partial and Vue component.
A Hybrid Headless Approach
VueJS Component Data
The integration of Vue components and MVC views is one of the most interesting parts of this approach. To hydrate the Vue components with data, we decided to render JSON inside a data attribute in the Vue component. This approach is concise and clean, and allowed us to avoid a round-trip ajax request to get the data. In the example below, the Model is actually string, which comprises of raw JSON which was serialised from a strongly typed model.
<navigation-data :data='@(Html.Raw(Model))'></navigation-data>
The VueJS component is very readable and elegant. Since the Vue component’s data is provided via rendered JSON, we call this a Hybrid Headless approach. A fully headless approach would supply the data via ajax API requests. One benefit of this hybrid approach means that we do not require the creation of APIs to be consumed. The same Vue component with it's rendered JSON can be seen below.
<navigation-data :data="{
'logo': {'text': 'SiteName','href': '/'
},
'nodes': [
{ 'label': 'First menu item', 'href': '/first-page', 'nodes': []
},
{ 'label': 'Second menu item', 'href': '/second-page', 'nodes': []
},
{ 'label': 'Third menu item', 'href': '/third-page', 'nodes': [
{ 'label': 'First sub page item', 'href': '/third-page/sub-page-1', 'nodes': []
},
{ 'label': 'Second sub page item', 'href': '/third-page/sub-page-2', 'nodes': []
},
{ 'label': 'Third sub page item', 'href': '/third-page/sub-page-3', 'nodes': [
{ 'label': 'External site', 'href': 'https://external.site.com/', 'target': '_blank', 'nodes': []
},
{ 'label': 'Detailed page', 'href': '/third-page/sub-page-3/detailed-page', 'nodes': []
}
]
},
{ 'label': 'Fourth sub page item', 'href': '/third-page/sub-page-4', 'nodes': []
}
]
}
]
}">
One of the biggest advantages of this approach is that the integration with Umbraco is very quick. Traditionally backend developers would need to copy and paste various chunks of HTML into MVC partial views. This becomes problematic when parts of the markup need to change. In our approach this is dramatically reduced.
When working with Vue, the UI developers work only on their .vue files. Subsequent changes to a component’s corresponding Vue file will be built into javascript. Therefore the backend developers will not need to touch the “already implemented” MVC partial views.
Generic MVC View For Droppable Modules
Using strongly typed Models Builder models means that corresponding MVC partials can be created which house the related Vue components. Since the modules may be dropped on any page in any order, we need our MVC rendering to facilitate this in a non-verbose way. This mapping is very intuitive, however the challenge was to ensure that all partial views could be rendered in any combination.
In our solution, we use a naming convention to determine the modules to be rendered. We then use a single generic MVC partial view on the page.
Here is the MVC partial view. It is very clean.
<main role="main">
@foreach (var module in Model.Body)
{
Html.RenderAction($"Get{module.ContentType.Alias}", "Module", new { content = module });
}
</main>
The end result is some generic MVC View code which calls a child action, which in turn calls the actual MVC partial for the module. The MVC partial in turn renders a Vue component with the required JSON data. Here is an example child action method:
[ChildActionOnly]
public ActionResult GetModuleSimpleQuote(ModuleSimpleQuote content)
{
return PartialView("~/Views/Modules/_SimpleQuote.cshtml", content.Text);
}
On a side note, you may decide that non-interactive components are standard MVC views.
Strange View Models
One strange thing about this approach is that our View Models are not actually traditional view models per se. The line is a little blurry, as the content was being rendered as JSON on MVC partials. To keep the Views clean, we serialise the View Models before we pass them (as a string) to the View. So there is some murkiness here. If this was a totally headless approach, then we would call these Data Models.
Another oddity is that sometimes your frontend Vue component data field names will not match up totally with the backoffice Element type names. Therefore the view models might need to be attributed with JsonProperty[“<insert name>”]. You may even consider attributing all of your model properties for consistency, however this really makes the model more of a Data Model, rather than a View Model. An example of this is below.
public class LinkWrapper
{
[JsonProperty("text")]
public string Label { get; set; }
[JsonProperty("href")]
public string Url { get; set; }
[JsonProperty("target")]
public string Target { get; set; }
}
View Model Hydration
The construction of view models can sometimes be a little complex. In our approach, we gain clean MVC views, however the tradeoff is potentially complex or unconventional view model hydration. One such scenario is where the Vue component requires content from the CMS node, and dictionary items.
In the following two code snippets, the controller uses a service to create the view model, then another class is used to hydrate dictionary items.
Controller:
public override ActionResult Index(ContentModel model)
{
var viewModel = _viewModelService.GetViewModel<ExampleViewModel>(model.Content);
_dictionaryItemHydrator.Hydrate(viewModel, Umbraco);
return View("Example", viewModel);
}
Dictionary Hydrator:
public ExampleViewModel Hydrate(ExampleViewModel viewModel, UmbracoHelper umbracoHelper)
{
var findOutMoreText = umbracoHelper.GetDictionaryValue("Global_CTA_Text");
foreach (var item in viewModel.Content.Items)
{
if (item.Url != null)
{
item.Url.Label = findOutMoreText;
}
}
return viewModel;
}
Note: The problem can become more complex if you have nesting of models which also require dictionary items.
Final Thoughts
In our approach, a totally modular design was realised by using a combination Nested Content, Models Builder, a generic MVC partial, VueJS components, and content rendered as JSON. This resulted in very clean MVC views, where the actual component HTML is maintained only as .vue files by UI developers.
There are some oddities in this approach. Namely the fact that our view models don’t really seem like traditional MVC view models. They feel more like data models which would typically be consumed via a WebAPI.
One unintended side effect of this approach is an incredibly fast time to first byte. We found that our pages can typically respond in under 100ms, without any caching. This is due to the fact that server side rendering of HTML is very limited, hence the response time is incredibly fast.
We look forward to improving this approach, and may look into Vue server-side rendering in the future.
Anthony Dang
Anthony is on Twitter as @AnthonyDotNet
John Sheard
John is on Twitter as @jsheard02