Require JS and CSS for a Sitecore components

Context

According to HTML best practices and guidelines we need to put styles in head (so that they would be applied to html immediately after rendering) and scripts in the end of body (so that they will not block your browser from downloading other content). On the other hand for Sitecore you need to create component, which could require some styles on specific scripts, and put them in a placeholder in the middle of the page.

This results in two common solutions: small scripts and style section spread all over the page, added in each component, or huge styles and scripts files containing all required content and added according to guidelines.

There should be a better way, lets see how it looks.

Approach

In each component you register required script or style-sheet in a HttpRequest and update page generated output by replacing placeholders with resource insertion snippets.

Extension for HtmlHelper

First of all we need to create extension for HtmlHelper that will allow you to register scripts.

public static class RequireHtmlHelperExtension
{
    public static RequireHelper Require(this HtmlHelper htmlHelper)
    {
        return RequireHelper.GetInstance();
    }
}

public class RequireHelper
{
    public RequireHelper()
    {
        this.Styles = new ItemRegistrar(AssetFormatters.StyleFormat);
        this.Script = new ItemRegistrar(AssetFormatters.ScriptFormat);
        ...

        this.Registrars = new List<ItemRegistrar>
                            {
                                this.Styles,
                                this.Script,
                                ...
                            };
    }

    public List<ItemRegistrar> Registrars { get; private set; }

    public ItemRegistrar Script { get; private set; }

    public ItemRegistrar Styles { get; private set; }

    ...
    
    public static RequireHelper GetInstance()
    {
        const string InstanceKey = "some key here";

        var context = HttpContext.Current;
        if (context == null)
        {
            return null;
        }

        var assetsHelper = (RequireHelper)context.Items[InstanceKey];

        if (assetsHelper == null)
        {
            context.Items.Add(InstanceKey, assetsHelper = new RequireHelper());
        }

        return assetsHelper;
    }
}

In extension shown in first code snippet we get singleton instance of Helper object from HttpRequest.Context.Items or create new one. This object has registrars classes as properties, which are responsible for resources on specific types (line 13-14). In addition to that registrar elements are grouped into a collection for convenience (will be shown later).

Registrar class

ItemRegistrar class will be responsible for adding elements on specific types, check uniqueness, pre-rendering and rendering html snippets.

public class ItemRegistrar
{
    private readonly string format;

    private readonly string uid;

    private readonly IList<string> items;

    public ItemRegistrar(string format)
    {
        this.uid = string.Format("<i id='{0}'/>", Guid.NewGuid());
        this.format = format;
        this.items = new List<string>();
    }

    public ItemRegistrar Add(string url)
    {
        if (!this.items.Contains(url))
        {
            this.items.Add(url);
        }

        return this;
    }


    public string UniqueId
    {
        get
        {
            return uid;
        }
    }

    public IHtmlString PreRender()
    {
        return new HtmlString(uid);
    }

    public IHtmlString Render()
    {
        var sb = new StringBuilder();

        foreach (var item in this.items)
        {
            var fmt = string.Format(this.format, item);
            sb.AppendLine(fmt);
        }

        return new HtmlString(sb.ToString());
    }
}

You probably noticed that class above has two methods: PreRender and Render. We will need them due to the fact that MVC generates output consequentially processing elements as they appear. HTML in head will be added to output writer by the time we register resource somewhere in the body. So we use method PreRender to add placeholder with unique key, which would be replaced afterwards.

mvc filters

The last element in our puzzle is replacement of placeholders defined by PreRender method. Here MVC filters come into a play. We need to register global filter in global.asax file that will perform replacements.

public class RequireFilterAttribute : ActionFilterAttribute
{
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        base.OnResultExecuted(filterContext);

        var response = filterContext.HttpContext.Response;

        if (response.Filter == null) return;

        response.Filter = new InsertRequiredTagsFilter(response.Filter, response.ContentEncoding);
    }

    public class InsertRequiredTagsFilter : MemoryStream
    {
        private readonly Encoding contentEncoding;

        private readonly Stream response;

        public InsertRequiredTagsFilter(Stream response, Encoding contentEncoding)
        {
            this.response = response;
            this.contentEncoding = contentEncoding;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            var html = new StringBuilder(this.contentEncoding.GetString(buffer));
            html = this.IncertRequiredTags(html);
            buffer = this.contentEncoding.GetBytes(html.ToString());
            this.response.Write(buffer, offset, buffer.Length);
        }

        private StringBuilder IncertRequiredTags(StringBuilder html)
        {
            var require = RequireHelper.GetInstance().Registrars
                .Select(x => new { Key = x.UniqueId, Value = x.Render() })
                .ToList();

            foreach (var pair in require)
            {
                html = html.Replace(pair.Key, pair.Value.ToString());
            }

            return html;
        }
    }
}

As you see we read content of response.Filter stream and replace placeholders that we left earlier with tags<![CDATA[
and .

Usage

Now we just need to put required PreRender placeholders in layout once and use Html.Require() in views where we need to reference scripts.

// Layout.cshtml file
Html.Require().Styles.PreRender()

// Component.cshtml file
Html.Require().Script.Add(BundleTable.Bundles.ResolveBundleUrl("~/bundles/js"));

As usual: Share if you like post. Follow me on twitter @true_shoorik

Require JS and CSS for a Sitecore components

One thought on “Require JS and CSS for a Sitecore components

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s