HomeDev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunitySubmit a ticketLog In
GitHubNuGetDev CommunitySubmit a ticket

Render multi-column form container block

Shows how to create your own form container block template and customize the rendering process to arrange elements into rows and columns using Display options and Bootstrap. To get the best experience, the Bootstrap version should be version 3 or higher.

📘

Note

Optimizely Forms is only supported by MVC-based websites and HTML5-compliant browsers.

The form container block renders elements line-by-line by default. Using a grid system like Bootstrap, you can customize the rendering process to display the form in a multi-column layout.

  1. Add predefined breakpoints for the bootstrap grid.

    public static class FormDisplayOptionTags {
      public static readonly string[] FullWidth = new string[] { "span12", "col-xs-12", "col-sm-12", "col-md-12", "col-lg-12" };
      public static readonly string[] ThirdQuaterWidth = new string[] { "span9", "col-xs-9", "col-sm-9", "col-md-9", "col-lg-9" };
      public static readonly string[] TwoThirdsWidth = new string[] { "span8", "col-xs-8", "col-sm-8", "col-md-8", "col-lg-8" };
      public static readonly string[] HalfWidth = new string[] { "span6", "col-xs-6", "col-sm-6", "col-md-6", "col-lg-6" };
      public static readonly string[] OneThirdWidth = new string[] { "span4", "col-xs-4", "col-sm-4", "col-md-4", "col-lg-4" };
      public static readonly string[] OneQuaterWidth = new string[] { "span3", "col-xs-3", "col-sm-3", "col-md-3", "col-lg-3" };
    }
    
  2. Optionally, add more display options corresponding to these breakpoints:

    var options = ServiceLocator.Current.GetInstance();
    options.Add("full", "/displayoptions/full", Global.ContentAreaTags.FullWidth, "", "epi-icon__layout--full")
           .Add("wide", "/displayoptions/wide", Global.ContentAreaTags.TwoThirdsWidth, "", "epi-icon__layout--two-thirds")
           .Add("narrow", "/displayoptions/narrow", Global.ContentAreaTags.OneThirdWidth, "", "epi-icon__layout--one-third")
           ...;
    
  3. Create the FormContentAreaRenderer class to get content area items style and calculate the item width necessary for the layout.

    [ServiceConfiguration(ServiceType = typeof (FormContentAreaRender), Lifecycle = ServiceInstanceScope.Singleton)]
    public class FormContentAreaRender: ContentAreaRenderer {
      private IContent _currentContent;
    
      /// Get css of a content area item
      public string GetItemCssClass(HtmlHelper html, ContentAreaItem areaItem) {
        var tag = GetContentAreaItemTemplateTag(html, areaItem);
        var baseClasses = base.GetContentAreaItemCssClass(html, areaItem);
        return $ "block {GetTypeSpecificCssClasses(areaItem)} {tag} {baseClasses}";
      }
    
      /// Get layout width of a content area item
      public int GetColumnWidth(HtmlHelper html, ContentAreaItem item) {
        var tag = GetContentAreaItemTemplateTag(html, item);
        return GetColumnWidth(tag);
      }
    
      private string GetTypeSpecificCssClasses(ContentAreaItem contentAreaItem) {
        var content = GetCurrentContent(contentAreaItem);
        var cssClass = content?.GetOriginalType().Name.ToLowerInvariant() ?? string.Empty;
        var customClassContent = content as ICustomCssInContentArea;
        if (customClassContent != null && !string.IsNullOrWhiteSpace(customClassContent.ContentAreaCssClass)) {
          cssClass += $ " {customClassContent.ContentAreaCssClass}";
        }
        return cssClass;
      }
    
      private IContent GetCurrentContent(ContentAreaItem contentAreaItem) {
        if (_currentContent == null || !_currentContent.ContentLink.CompareToIgnoreWorkID(contentAreaItem.ContentLink)) {
          _currentContent = contentAreaItem.GetContent();
        }
        return _currentContent;
      }
    
      /// Get the width of a css tag (bootstrap column width)
      private int GetColumnWidth(string tag) {
        if (Global.FormDisplayOtionTags.FullWidth.Contains(tag))
    
        {
          return 12;
        }
        if (Global.FormDisplayOtionTags.ThirdQuaterWidth.Contains(tag)) {
          return 9;
        }
        if (Global.FormDisplayOtionTags.TwoThirdsWidth.Contains(tag)) {
          return 8;
        }
        if (Global.FormDisplayOtionTags.HalfWidth.Contains(tag)) {
          return 6;
        }
        if (Global.FormDisplayOtionTags.OneThirdWidth.Contains(tag)) {
          return 4;
        }
        if (Global.FormDisplayOtionTags.OneQuaterWidth.Contains(tag)) {
          return 3;
        }
        return 12;
      }
    }
    
  4. Define RenderFormElements extension method for HtmlHelper.

    static Injected _formContenAreaRender;
    
    ///
    /// Renders form elements
    ///
    ///Instance of HtmlHelper class
    ///Form element collection
    public static void RenderFormElements(this HtmlHelper html, int currentStepIndex, IEnumerable elements) {
      FormContainerBlock model = (FormContainerBlock) html.ViewData.Model;
      if (model == null) {
        return;
      }
      /// TODO: calculate element width and group elements into rows
      /// We use rows to keep the layout unbroken since elements have different heights
      int rowWidthState = 0;
      var elementsInfos = elements.Select(element => {
        var areaItem = model.ElementsArea.Items.FirstOrDefault(i => i.ContentLink == element.SourceContent.ContentLink);
        var columnWidth = _formContenAreaRender.Service.GetColumnWidth(html, areaItem);
        rowWidthState += columnWidth;
        return new {
          ContentAreaItem = areaItem,
            RowNumber = rowWidthState % 12 == 0 ? rowWidthState / 12 - 1 : rowWidthState / 12
        };
      });
    
      var rows = elementsInfos.GroupBy(a => a.RowNumber, a => a.ContentAreaItem);
      foreach(var row in rows) {
        // start of new row
        html.ViewContext.Writer.Write("<div class=\"row row-" + row.Key + "\">");
        foreach(var item in row) {
          IContent content = item.GetContent();
          if (content == null || content.IsDeleted) {
            continue;
          }
          var cssClasses = _formContenAreaRender.Service.GetItemCssClass(html, item);
          // start of conten area item
          html.ViewContext.Writer.Write($"<div class=\"{cssClasses}\">");
    
          if (content is ISubmissionAwareElement) {
            var submissionAwareElement = (content as IReadOnly).CreateWritableClone() as IContent;
            (submissionAwareElement as ISubmissionAwareElement).FormSubmissionId = html.ViewBag.FormSubmissionId;
            html.RenderContentData(submissionAwareElement, false);
          } else {
            html.RenderContentData(content, false);
          }
          // end of content area item
          html.ViewContext.Writer.Write("</div>");
        }
        // end of row
        html.ViewContext.Writer.Write("</div>");
      }
    }
    
  5. Edit the view of FormContainerBlock (FormContainerBlock.ascx) to use the new render method.

    Replace:

    <%@ Import Namespace="EPiServer.Forms.EditView.Internal" %>
    

    with:

    <%@ Import Namespace="EpiserverSite2.Helpers" %>
    
  6. ICustomCssInContentArea interface for adding a custom CSS for form element.

    public interface ICustomCssInContentArea {
      string ContentAreaCssClass {
        get;
      }
    }
    

The following shows a form rendered in view mode. 

📘

Note

A form can appear differently between edit view and view mode because in view mode, form elements are grouped into rows and columns to keep the layout unbroken. If you want a consistent look between edit view and view mode, create a custom content area renderer and apply it for the whole site.

🚧

Limitation

Currently, for simplicity, breakpoints are fixed for bootstrap grid, so 1/3 is 1/3 across screens. You can specify that 1/3 should be on the desktop and 12/12 should be on smaller screens. However, modifying the form container block template (FormContainerBlock.ascx) can be a little tricky.

See also: blog post, Create custom Forms container