Removing extra DIVs from EPiServer Content Areas

The default ContentAreaRenderer implementation renders div elements around the content area and every content area item. It is possible to control the rendering of wrapping elements, but not to disable them completely.

Rendering the wrapping elements makes sense in edit mode, since we want to have On-Page-Editing support. However, having them in view mode is not always desirable.

If we display a content area property like this:

@Html.PropertyFor(x => x.CurrentPage.MyContentArea)

We may expect the following output:

<div>
    <div><!-- Block --></div>
    <div><!-- Block --></div>
    <div><!-- Block --></div>
</div>

ContentAreaRenderer will display one div for the content area, and one div for each block (content area item).

We can disable the main div by passing hascontainer = false.

@Html.PropertyFor(x => x.CurrentPage.MyContentArea, new
{
    hascontainer = false
})
<div><!-- Block --></div>
<div><!-- Block --></div>
<div><!-- Block --></div>

And, we can even replace the divs around content area items by passing childrencustomtagname:

@Html.PropertyFor(x => x.CurrentPage.MyContentArea, new
{
    hascontainer = false,
    childrencustomtagname = "li"
})
<li><!-- Block --></li>
<li><!-- Block --></li>
<li><!-- Block --></li>

 However, we cannot disable those divs by setting childrencustomtagname to an empty string because childrencustomtagname fallbacks to div:

@Html.PropertyFor(x => x.CurrentPage.MyContentArea, new
{
    hascontainer = false,
    childrencustomtagname = ""
})
<div><!-- Block --></div>
<div><!-- Block --></div>
<div><!-- Block --></div>

What’s the solution?

One of the solutions would be to create a new content area renderer, which inherits ContentAreaRenderer, and modifies the default behavior:

  • It should be possible to disable div elements for content area items in view mode, not just to replace them with another element.
  • Containers should be disabled by default in view mode (i.e. it should not be necessary to pass hascontainer = false).

Here is my implementation:

public class MyContentAreaRenderer : ContentAreaRenderer
{
    private readonly IContentAreaLoader _contentAreaLoader;
    private readonly IContentRenderer _contentRenderer;
    private readonly IContentAreaItemAttributeAssembler _attributeAssembler;

    public MyContentAreaRenderer(
        IContentAreaLoader contentAreaLoader,
        IContentRenderer contentRenderer,
        IContentAreaItemAttributeAssembler attributeAssembler)
    {
        _contentAreaLoader = contentAreaLoader;
        _contentRenderer = contentRenderer;
        _attributeAssembler = attributeAssembler;
    }

    protected override void RenderContentAreaItem(
        HtmlHelper htmlHelper,
        ContentAreaItem contentAreaItem,
        string templateTag,
        string htmlTag,
        string cssClass)
    {
        var renderSettings = new Dictionary<string, object>
        {
            ["childrencustomtagname"] = htmlTag,
            ["childrencssclass"] = cssClass,
            ["tag"] = templateTag
        };

        renderSettings = contentAreaItem.RenderSettings.Concat(
            from r in renderSettings
            where !contentAreaItem.RenderSettings.ContainsKey(r.Key)
            select r).ToDictionary(r => r.Key, r => r.Value);

        htmlHelper.ViewBag.RenderSettings = renderSettings;
        var content = _contentAreaLoader.Get(contentAreaItem);
        if (content == null)
        {
            return;
        }

        bool isInEditMode = IsInEditMode(htmlHelper);

        using (new ContentAreaContext(htmlHelper.ViewContext.RequestContext, content.ContentLink))
        {
            var templateModel = ResolveTemplate(htmlHelper, content, templateTag);
            if (templateModel != null || isInEditMode)
            {
                bool renderWrappingElement = ShouldRenderWrappingElementForContentAreaItem(htmlHelper);

                // The code for this method has been c/p from EPiServer.Web.Mvc.Html.ContentAreaRenderer.
                // The only difference is this if/else block.
                if (isInEditMode || renderWrappingElement)
                {
                    var tagBuilder = new TagBuilder(htmlTag);
                    AddNonEmptyCssClass(tagBuilder, cssClass);
                    tagBuilder.MergeAttributes(_attributeAssembler.GetAttributes(
                        contentAreaItem,
                        isInEditMode,
                        templateModel != null));
                    BeforeRenderContentAreaItemStartTag(tagBuilder, contentAreaItem);
                    htmlHelper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag));
                    htmlHelper.RenderContentData(content, true, templateModel, _contentRenderer);
                    htmlHelper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.EndTag));
                }
                else
                {
                    // This is the extra code: don't render wrapping elements in view mode
                    htmlHelper.RenderContentData(content, true, templateModel, _contentRenderer);
                }
            }
        }
    }

    private bool ShouldRenderWrappingElementForContentAreaItem(HtmlHelper htmlHelper)
    {
        // set 'haschildcontainers' to false by default
        bool? item = (bool?)htmlHelper.ViewContext.ViewData["haschildcontainers"];
        return item.HasValue && item.Value;
    }

    protected override bool ShouldRenderWrappingElement(HtmlHelper htmlHelper)
    {
        // set 'hascontainer' to false by default
        bool? item = (bool?)htmlHelper.ViewContext.ViewData["hascontainer"];
        return item.HasValue && item.Value;
    }
}

Most of the code is copied from EPiServer.Web.Mvc.Html.ContentAreaRenderer.

The only differences are:

  • If/else block in RenderContentAreaItem method.
  • hascontainer is set to false by default in ShouldRenderWrappingElement method.
  • In ShouldRenderWrappingElementForContentAreaItem method, I’ve defined a new setting called haschildcontainers that toggles wrapping elements (divs) for content area items.

Next, we need to tell the StructureMap to replace the ContentAreaRenderer with our implementation.

This can be achieved with an initialization module:

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class DependencyResolverInitialization : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.StructureMap().Configure(ConfigureContainer);
        DependencyResolver.SetResolver(new StructureMapDependencyResolver(context.StructureMap()));
    }

    private static void ConfigureContainer(ConfigurationExpression container)
    {
        container.For<ContentAreaRenderer>().Use<MyContentAreaRenderer>();
    }

    public void Initialize(InitializationEngine context)
    {
    }

    public void Uninitialize(InitializationEngine context)
    {
    }
}

Please note that I’m using EPiServer version 10.10.1, and Structuremap version 3.1.9.463.

The code might be slightly different in other versions of Episerver.

And, that’s it!

If we display a content area property like this:

@Html.PropertyFor(x => x.CurrentPage.MyContentArea)

We should get a nice and clean HTML in view mode with On-Page-Editing support in edit mode:

<!-- Block -->
<!-- Block -->
<!-- Block -->

With all settings:

@Html.PropertyFor(x => x.CurrentPage.MyContentArea, new
{
    hascontainer = true,
    customtag = "ul",
    cssclass = "parent-class",

    haschildcontainers = true,
    childrencustomtagname = "li",
    childrencssclass = "child-class"
})
<ul class="parent-class">
    <li class="child-class">
        <!-- Block -->
    </li>
    <li class="child-class">
        <!-- Block -->
    </li>
    <li class="child-class">
        <!-- Block -->
    </li>
</ul>
comments powered by Disqus