Hide menu Last updated: Jan 23 2017
Area: Episerver Add-ons Applies to versions: Forms 4.3.0 and higher

Rendering a multi-column form container block

Note: Episerver Forms is only supported by MVC-based websites and HTML5-compliant browsers.

The form container block render elements line-by-line by default. You can customize the rendering proccess to display the form in a multi-column layout using a grid system like Bootstrap.

The following steps show how to create your own form container block template and customize the rendering process to arrange elements into rows and columns using EPiServer Display options and Bootstrap. To get the best experience, the Bootstrap version should be >= v3.

  1. Add predefined breakpoints for bootstrap grid.
    public static class FormDisplayOtionTags
    {
        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 that necessary for 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 ViewMode. 

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 little tricky.

Comments

Nice to see that forms are also supporting Bootstrap.

However, in the given example there is practical downside with the Bootstrap breakpoints for different screensizes. Usually when editors define half size ore even 1/3 -> it does not look good on smaller devices as there is not so much space tho. Usually half sized objects occupy full width on smaller screens. It's really depends on project / templates.

Editors usually expect to have the same behavior also for EPiServer Forms elements (as they are just yet another block types). So for that reason I created small package to support Bootstrap grid system for Forms elements as well (more info here: http://blog.tech-fellow.net/2016/09/05/apply-displayoption-for-episerver-forms-elements/).

Also, using "parent" package (EPiBootstrapArea) for adding Bootstrap classes for ordinary content area items - you get row support out of the box (and not only for Forms elements, but for other blocks as well).

And of course bunch of other sweeties :)