Sitecore & Angular 2

Sitecore & Angular 2

Introduction 

The company I work for has a long history in implementing content management system based solutions, including implementations based on Sitecore. In the last few years this was usually an implementation based on Sitecore 7.x or 8 in combination with AngularJS (version 1.x) with which we build public and private sites and sometimes in Single Page Application setup.
With Angular 2 coming out already some time a go we realised we need to find a solution to create Sitecore based solutions in combination with Angular 2(+) to not only make sure we don't end up with new solutions based on old technologies that might be deprecated but also to stick to a technology stack that is close to our employee's skillset and know-how.

Requirements

Sitecore's rendering engine which generates dynamic HTML on the server including data (binding) and component based FrontEnd technologies like Angular 2 are not a match made in heaven out of the box, however as this blog will show, with some minor tweaks they can actually work together pretty neatly.

When we started our first implementation we had (of course) some requirements in mind like:

  • Support Experience Editor
  • Support Personalization, A/B testing etc.
  • Try to make use of the Sitecore engine where possible and rely on Angular (2) for all front end stuff


Implementation

The implementation we eventually went for is an extension of the Sitecore Rendering pipeline.
We extended two specific ones:

  • Sitecore.Mvc.Pipelines.Response.GetRenderer.GetViewRenderer
The view rendering renderer which extends the Sitecore View rendering to render an Angular 2 component for the view rendering.
  • Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer
The controller rendering renderer which extends the Sitecore (MVC) controller rendering.

With these two extensions we support the two renderings that we most commonly use, although one could argue that all you really need is the Sitecore controller rendering.




Both rendering extensions (of which code snippets can be found below) check whether the controls being rendered inherit from a specific base template to determine whether it concerns an Angular 2 control or a regular Sitecore control. These base templates also contain a field for the ComponentName which will hook the rendering to an Angular 2 component. This will result in a HTML component tag that Angular can hook up to:
       
<personal-data url="/angulartemplate?comp=personal-data&ds={9D680FF1-DDED-4476-A587-B6171D75A5C9}" item="a6d04b2c-5532-4539-bb79-01e1119de926" />
       
 




The extension lets the default Sitecore rendering engine execute the view or controller rendering normally (so that personalization etc. are also applied normally) but catches the outgoing HTML and saves that in cache for later retrieval. Instead it renders the component-name as specified for the control and applies a unique url where Angular can later retrieve the HTML from and an item id with the rendering's unique id to make sure different instances of a controller have different HTML to be retrieved, this is important so that if you have multiple instances of the same control on your page you can still differ its contents.

       
using System.Text;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using System.IO;
using System.Web.Mvc;
using Sitecore.Mvc.Presentation;

namespace Foundation.Pipelines
{
    public class AngularExtensionRendering : global::Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer
    {
        public override void Process(RenderRenderingArgs args)
        {
            if (args.Rendered)
            {
                return;
            }
            Renderer renderer = args.Rendering.Renderer;
            if (renderer == null)
            {
                return;
            }
            args.Rendered = this.Render(renderer, args.Writer, args);
        }
   
        protected override bool Render(Renderer renderer, TextWriter writer, RenderRenderingArgs args)
        {
            var item = global::Sitecore.Context.Database.GetItem(args.Rendering.RenderingItem.ID);
            if (item != null && (item.TemplateName == Constants.AngularControllerRendering.TemplateName || item.TemplateName == Constants.AngularViewRendering.TemplateName) && !global::Sitecore.Context.PageMode.IsExperienceEditor)
            {
                using (StringWriter newWriter = new StringWriter())
                {
                    base.Render(renderer, newWriter, args);
                    string partialView = newWriter.ToString();
                    var newPartial = GetAngularPartial(args, partialView, item).ToHtmlString();
                    StringBuilder sb = newWriter.GetStringBuilder();
                    sb.Remove(0, sb.Length);
                    newWriter.Write(newPartial);
                    args.Cacheable = false;
                    args.Writer = newWriter;
                    writer.Write(newPartial);
                    return true;
                }
            }
            return base.Render(renderer, writer, args);
        }

        private MvcHtmlString GetAngularPartial(RenderRenderingArgs args, string partialViewHtml, global::Sitecore.Data.Items.Item item)
        {
            var componentName = item.Fields[Constants.AngularRendering.ComponentName].Value;
            args.PageContext.RequestContext.HttpContext.Session[componentName + args.Rendering.DataSource + args.CacheKey] = partialViewHtml;

            return new MvcHtmlString(string.Format("<{0} url=\"/angulartemplate?comp={1}&ds={2}\" item=\"{3}\" />", componentName, componentName, args.Rendering.DataSource + args.CacheKey, args.Rendering.UniqueId));
        }
    }
}

       
 


       

using Sitecore.Mvc.Presentation;
using Sitecore.Mvc.Extensions;
using Sitecore.Mvc.Pipelines.Response.GetRenderer;

namespace Foundation.Pipelines
{
    public class GetAngularViewRenderingExtension : global::Sitecore.Mvc.Pipelines.Response.GetRenderer.GetViewRenderer
    {
        protected override Renderer GetRenderer(Rendering rendering, GetRendererArgs args)
        {
            string viewPath = this.GetViewPath(rendering, args);
            if (viewPath.IsWhiteSpaceOrNull())
            {
                return null;
            }
            return new ViewRenderer
            {
                ViewPath = viewPath,
                Rendering = rendering
            };
        }

        protected override string GetViewPath(Rendering rendering, GetRendererArgs args)
        {
            if (args.RenderingTemplate.Name == Constants.AngularViewRendering.TemplateName && args.Rendering.RenderingItem != null && args.Rendering.RenderingItem.InnerItem != null)
            {
                return args.Rendering.RenderingItem.InnerItem["path"];
            }
            else
            {
                return this.GetViewPathFromRenderingType(rendering, args) ?? this.GetViewPathFromRenderingItem(rendering);
            }
        }

        public override void Process(GetRendererArgs args)
        {
            if (args.Result != null)
            {
                return;
            }
            if (args.RenderingTemplate.FullName == Constants.AngularViewRendering.TemplateName)
            {
                args.Rendering.RenderingType = "View";
                args.Result = this.GetRenderer(args.Rendering, args);
            }
            else
            {
                args.Result = this.GetRenderer(args.Rendering, args);
            }
        }
    }
}
       
 

The next step is to have Angular hook up to the Component and retrieve the HTML and data (if applicable). The tricky bit here was that out of the box Angular 2 only allows for static HTML to be bound to a component: in the form of a static url to retrieve the HTML from. For this we implemented an Angular class decorator that reads the url from the component's url attribute.


       
export function DynamicUrl() {
    let __ref__ = window['Reflect'];

    function parseMeta(metaInformation) {
        for (let _i = 0, metaInformation_1 = metaInformation; _i < metaInformation_1.length; _i++) {
            let metadata = metaInformation_1[_i];

            if (!PRODMODE) {
                console.log(metadata);
            }

            let urlAttr = document.getElementsByTagName(metadata.selector)[0];
            if (urlAttr !== undefined) {
                if (urlAttr.getAttribute('url') !== '') {
                    metadata.templateUrl = urlAttr.getAttribute('url');
                } else {
                    console.warn(`${metadata.selector} url was not defined`);
                    metadata.url = `
                        <div class="alert alert-danger small">
Template url not found
                        </div>
`;
                }
            } else {
                console.warn(`${metadata.selector} failed to find a matching component selector`);
            }
        }
    }
    return function (target) {
        let metaInformation = __ref__.getOwnMetadata('annotations', target);
        if (metaInformation) {
            parseMeta(metaInformation);
        }
    };
}

       
 

Now that we had the Angular firing up the component is will try to request the dynamic HTML from the url provided in its attribute. To catch that call we implemented an HTTP handler that reads the rendered HTML from (redis) cache again that was stored earlier in the ExecuteRenderer pipeline override.


       
using System.Web;
using System.Web.SessionState;
using System.Web.Mvc;

namespace Foundation.Handlers
{
    /// 
    /// This handler is used by Angular components to read static html. This html is pre-stored by Sitecore rendering pipeline
    /// in session for the specific item instance, component and datasource.
    /// 
    /// 
    /// 
    public class AngularHttpHandler : IHttpHandler, IRequiresSessionState
    {
        bool IHttpHandler.IsReusable
        {
            get
            {
                return true;
            }
        }

        /// 
        /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the  interface.
        /// 
        /// An  object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.
        void IHttpHandler.ProcessRequest(HttpContext context)
        {
            var component = context.Request.Params["comp"];
            var datasource = context.Request.Params["ds"];
            var partialView = new MvcHtmlString(HttpContext.Current.Session[component + datasource] as string);
            context.Response.Write(partialView);
        }
    }
}
       
 

With the HTML returned the component can now render the HTML and fire another API call to retrieve and bind data if applicable to the component.

Experience editor

There was yet another step we needed to consider, the experience editor. Angular 2 loading components just didn't work inside the experience editor. While it may be feasible to get it to work, it seemed like a whole lot of work, so we decided for a different approach.
In case a page is running in experience editor mode the rendering pipeline continue to just execute the Sitecore rendering and return that HTML and they do not return the Angular component tags, so in this case you are back to plain old default Sitecore behaviour. While you might miss some of the experience like the interaction that your Angular implementation brings the pages will actually look pretty much like what you want. For very specific case you might have to write a few lines of additional code to have a better experience for the editor or marketeer, but the benefits of having a frontend framework like Angular 2 in your devkit easily outweighs this drawback.

Conclusion

With all the above steps we have a neat solution to integrate Angular 2 component based implementation side-by-side with Sitecore. From a Sitecore development perspective there is not a whole lot extra that a developer needs to do, only to create renderings based on the new templates and setting a component name. Frontend development needs to make sure to hook up to the correct component name and you are good to go.
As all serverside prerendered HTML is stored in cache, the Angular calls for HTML are really fast and the performance impact is minimal.

Reacties

Een reactie posten

Populaire posts van deze blog

I Robot - Sitecore JSS visitor identification

Sitecore campaigns and UTM tracking unified

Sitecore JSS - Sitecore first