JavaScript

JavaScript and Sitecore Renderings by Derek Hunziker

With the exception of modernizr, ployfills, and HTML5 shivs, it's generally a good idea to put your JavaScript at the end of the page. This is a widely adopted practice that helps prevent script execution from blocking the rendering of the DOM. It also has the added SEO benefit of pushing your content closer to the top of the page.

There are times, however, when it makes sense to include JavaScript on an individual component within your website. You may be trying to surface data from the component and make it available on the client-side, or perhaps you have a script that is very large and will only be used on a handful of pages on the site. Including the script in a global script bundle may not make sense in these cases.

Code Injection Fields

One approach is to include a "Code Injection" field on your site's base page template. The contents of this field could be rendered at the bottom of the page if a value is present. From my experience, this is a quick and easy way to get scripts onto a page, however, it is often abused by content authors, and it is also difficult to manage. It's far too easy to forget to include the script wherever your component appears.

JavaScript Within Renderings

The better approach, in my opinion, is to include the script in the view of the component that it relates to. This creates a tighter coupling between the scripts and the component, similar to how a Shadow DOM is coupled to a polymer component. The main challenge with this approach, however, is that components are often rendered far from the bottom of the page, and it's script may depend on other vendor scripts or frameworks that are not available yet.

Dependency Injection

One solution to this challenge is to register all scripts with a dependency manager, such as RequireJS. With this approach, scripts registered at the component level would have a dependency on the script(s) at the bottom of the page. The dependency manager would figure out which scripts to load and in what order. AngularJS is good example of a framework with built-in dependency injection that would allow for modules to be registered anywhere within the DOM. Sadly, though, using JavaScript DI on a project is not always an option, and I often see it fall lower on the priority list.

Script Block HTML Helper

This solution allows for scripts to be defined on the component's view, but rendered at the bottom of the page. It works by persisting the scripts alongside the rendering in Sitecore's HTML Cache. What's nice about this solution is that it works with and without caching enabled on the rendering, and it does not require an extra level of abstraction needed by JavaScript dependency managers..

To get this working, you will need to define a disposable script block class that will do the job of collecting and persisting the scripts (or any other markup for that matter).

public class ScriptBlock : IDisposable
{
    private readonly WebViewPage _webViewPage;
    private readonly ID _renderingUniqueId;

    public static string GetCacheKey(ID renderingUniqueId)
    {
        return String.Concat("SCRIPT_BLOCKS_", renderingUniqueId);
    }

    public ScriptBlock(WebViewPage webPageBase, ID renderingUniqueId)
    {
        _webViewPage = webPageBase;
        _webViewPage.OutputStack.Push(new StringWriter());
        _renderingUniqueId = renderingUniqueId;
    }

    public void Dispose()
    {
        string markup = _webViewPage.OutputStack.Pop().ToString();
        string cacheKey = GetCacheKey(_renderingUniqueId);
        string cacheMarkup = _webViewPage.Context.Items[cacheKey] != null ?
    	    _webViewPage.Context.Items[cacheKey].ToString() :
            string.Empty;

        cacheMarkup = !string.IsNullOrEmpty(cacheMarkup) ?
            string.Concat(cacheMarkup, Environment.NewLine, markup) :
            markup;

        _webViewPage.Context.Items[cacheKey] = cacheMarkup;
        Sitecore.Context.Site.Caches.HtmlCache.SetHtml(cacheKey, cacheMarkup);
    }
}

Next, we'll use a pair of HTML helper extension methods, one to expose our ScriptBlock, and the other to render out the cached script blocks at the bottom of the page.

public static IDisposable BeginScriptBlock(this HtmlHelper helper)
{
    return new ScriptBlock((WebViewPage)helper.ViewDataContainer, 
        new ID(helper.Sitecore().CurrentRendering.UniqueId));
}

public static MvcHtmlString RenderScriptBlocks(this HtmlHelper helper)
{
    RenderingReference[] renderings = 
        helper.Sitecore().CurrentItem.Visualization.GetRenderings(Sitecore.Context.Device, true);

    List<string> scriptBlocks = renderings
        .Select(rendering => ScriptBlock.GetCacheKey(new ID(rendering.UniqueId)))
        .Select(cacheKey => Sitecore.Context.Site.Caches.HtmlCache.GetHtml(cacheKey))
        .Where(markup => !String.IsNullOrEmpty(markup))
        .ToList();

    return MvcHtmlString.Create(String.Join(Environment.NewLine, scriptBlocks));
}

Now we can include scripts anywhere within the rendering's view like so:

@using (Html.BeginScriptBlock())
{
    <script>
        document.write('@DateTime.Now.ToString("s")');
    </script>
}

Finally, include a call to @Html.RenderScriptBlocks() at the bottom of your base layout. Any cached scripts that belong to a rendering on the page will be rendered here.