06 March 2020
Umbraco 7: Simple Angular Single Page Application (SPA) with Umbraco as a Content Service.
As with all topics related to software development, there is always more than one solution to a problem. The following proposition is simply one possible solution and not intended to be definitive.
Summary:
- Create a SPA Helper Class which converts an IPublishedContent into JSON
- Create an Umbraco Template that returns a JSON representation of the Content Model
- Create a "catch-all" Angular route for Umbraco Urls
- Create an Angular Component to retrieve and display the Umbraco content
It's important to understand that the following code is incomplete. You'll definitely need to enhance and extend the pattern to accommodate a production system. For example, membership authentication, individual template layouts per Umbraco Document Type and support for contact forms are clearly not covered. That said, this baseline approach is definitely enough to kick-start a complete Angular SPA client with Umbraco as a content service (aka Headless Umbraco).
Umbraco, by design, is a Model, View and Controller (MVC) framework and Content Management System (CMS). The deeper you dive into Umbraco development and customisation, the more you'll realise how interdependent the Views, Controllers and Models are when it comes to delivering content. Particularly related to Macros, Umbraco needs to run-through its entire pipeline in-order to render correctly. So, it's logical that you don't fight the system, use the system as it is and take control after Umbraco has finished doing what it needs to do.
With the above in mind, a workable approach to converting Umbraco into a content service (or headless Umbraco) is to use a Template/View to deliver a JSON response.
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage @using UmbSpa.Utils; @using Newtonsoft.Json; @{ Layout = null; Response.ContentType = "application/json"; string domain = Model.Content.AncestorOrSelf(1).UrlAbsolute().TrimEnd('/'); var data = UmbSpaContentParser.GetNodeProperties(Model.Content, domain); @Html.Raw(JsonConvert.SerializeObject(data)) }
Simple and concise, all you need to do is change the response ContentType to JSON and convert the Document Model into something easily parsed into JSON exported to the consumer/client.
The real trick to leveraging all that Umbraco offers, without losing any of the CMS's most powerful content features, is to take a dynamic approach to converting all of Umbraco's custom properties into JSON. No matter how the Umbraco developer has structured the Document Types, we should be able to serve that data easily.
By creating a helper class, which can be placed within the App_Code folder, or contained within a separate project or assembly, you can pass in the Umbraco Model as an argument and parse all its dynamic properties into a standardised Dictionary object.
using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models; using Umbraco.Web; namespace UmbSpa.Utils { public class UmbSpaContentParser { public static Dictionary<string, object> GetNodeProperties(IPublishedContent node, string baseUrl = "") { var data = new Dictionary<string, object>(); // Add the default Node Properties data.Add("id", node.Id); data.Add("name", node.Name); data.Add("url", node.Url); data.Add("documentType", node.DocumentTypeAlias); data.Add("createDate", node.CreateDate.ToString("yyyy-MM-dd")); data.Add("sortOrder", node.SortOrder); // Append the custom properties var propList = node.Properties.Where(p => p.HasValue); foreach (IPublishedProperty prop in propList) { var propType = node.ContentType.GetPropertyType(prop.PropertyTypeAlias).PropertyEditorAlias; switch (propType) { case "Umbraco.MediaPicker2": { IPublishedContent media = node.GetPropertyValue(prop.PropertyTypeAlias); if (media != null) { data.Add(prop.PropertyTypeAlias, baseUrl + media.Url); } } break; case "Umbraco.TinyMCEv3": {
// todo: Probably need to parse href, src, action into absolute URLs var content = prop.Value.ToString(); data.Add(prop.PropertyTypeAlias, content); } break; case "Umbraco.ContentPicker2": { // Recursively build the Child Node List IPublishedContent linkNode = node.GetPropertyValue(prop.PropertyTypeAlias); if (linkNode != null) { Dictionary<string, object> dataItem = new Dictionary<string, object>(); dataItem.Add("id", linkNode.Id); dataItem.Add("name", linkNode.Name); dataItem.Add("url", linkNode.Url); data.Add(prop.PropertyTypeAlias, dataItem); } } break; case "Umbraco.NestedContent": case "Umbraco.MultiNodeTreePicker2": { IEnumerable nodes = node.GetPropertyValue<IEnumerable>(prop.PropertyTypeAlias); List<Dictionary<string, object>> list = new List<Dictionary<string, object>>(); foreach (var n in nodes) { Dictionary<string, object> dataItem = new Dictionary<string, object>(); dataItem.Add("id", n.Id); dataItem.Add("name", n.Name); dataItem.Add("url", n.Url); list.Add(dataItem); } data.Add(prop.PropertyTypeAlias, list); } break; case "Umbraco.RelatedLinks2": { Umbraco.Web.Models.RelatedLinks rlinks = node.GetPropertyValue(prop.PropertyTypeAlias); var linksData = new List<Dictionary<string, string>>(); foreach (var item in rlinks) { Dictionary<string, string> dataItem = new Dictionary<string, string>(); dataItem.Add("title", item.Caption); dataItem.Add("url", item.Link); var linkTarget = (item.NewWindow) ? "_blank" : null; dataItem.Add("target", linkTarget); linksData.Add(dataItem); } data.Add(prop.PropertyTypeAlias, linksData); } break; case "Umbraco.MultipleTextstring": { string[] tags = node.GetPropertyValue<string[]>(prop.PropertyTypeAlias); data.Add(prop.PropertyTypeAlias, tags); } break; case "Umbraco.Date": { DateTime dt = node.GetPropertyValue(prop.PropertyTypeAlias); data.Add(prop.PropertyTypeAlias, dt.ToString("yyyy-MM-dd")); } break; case "Umbraco.Integer": case "Umbraco.Textbox": case "Umbraco.TextboxMultiple": default: { data.Add(prop.PropertyTypeAlias, prop.Value); } break; } } return data; } } }
The above method is incomplete, it doesn't accommodate all Umbraco's Property Types or, obviously, any third party Property Types. However, it should be fairly obvious how to extend the system to support whatever property you use within your own project.
Moving on to the Angular client, assuming that you'll utilise Umbraco's URL structure, and accept that content editors may change their page URLs so need to remain flexible, we'll create a "catch-all" route for Angular using a single Component to handle every request.
export const AppRouting = RouterModule.forRoot( [ // Default Catch All for Umb URLs { path: '**', component: UmbContentComponent } ] );
The following example Angular Component takes the current route and, using an HTTP request, sends the route directly to Umbraco as a GET request to retrieve the JSON data delivered by the template at that Umbraco URL path.
It's important to subscribe to the Angular Router service because, since we're using the same Component for all routes, we need to trigger a new content request when the route changes.
@Component({ templateUrl: 'umb-content.component.html' }) export class UmbContentComponent implements OnDestroy { protected subscription: Subscription; public umbContent: any; constructor( protected platformLocation: PlatformLocation, protected http: HttpClient, protected router: Router, ) { // Since this component is used for all Umbraco Requests, we need to be sure // the content is retrieved each time the route changes this.subscription = this.router.events.subscribe(event => { if (event instanceof NavigationEnd) { // Note: If you host the client separate from the Umb service // you will have to replace the domain with the service domain var href = this.platformLocation.href; return this.http.get(href) .pipe(first()) .subscribe((umbContent: any) => { this.umbContent = umbContent; }, (error: any) => { // todo }); } }); } public ngOnDestroy() { this.subscription.unsubscribe(); } }
I would suggest that you create a UmbContent Model within Angular to which you can cast the Umbraco data. This'll help leverage all the nice features that TypeScript offers.
Once the Umbraco document data has been acquired, you can pass the object through to the Component Template and render the data.
<div class="umb-content"> <h1 class="umb-name">
{{ umbContent.name }}<sup>{{ umbContent.id }}</sup>
</h1> <p>
<a class="umb-url">{{ umbContent.url }}</a>
</p> <div class="umb-content">
{{ umbContent.content }}
</div> </div>
As a final thought, within the Angular UmbContentComponent template, you could create some conditional loading based on the Umbraco DocumentType and deliver different layouts.
<div class="umb-content"> <app-doc-home-page *ngIf="umbContent.documentType == 'homePage'" [content]="umbContent"></app-doc-home-page> <app-doc-article-page *ngIf="umbContent.documentType == 'articlePage'" [content]="umbContent"></app-doc-article-page> <app-doc-landing-page *ngIf="umbContent.documentType == 'landingPage'" [content]="umbContent"></app-doc-landing-page> </div>
PurcellYoon are a team of Angular Experts and Umbraco Developers with a passion for creating exceptional digital experiences. We are committed to delivering superior Umbraco Websites for all our partners and clients.