Custom auto-generated form layouts
28th of July, 2017 0 comments

Custom auto-generated form layouts

If you are working on a complex website where forms are being programmatically generated, you might run into the issue of configuring form layouts.

Kentico provides a powerful "Form builder" feature, with drag-and-drop functionality for constructing forms. However, this feature does not help much with programmatically generated forms. Setting CSS classes on the settings of each field can help, but it does not allow you to pre-define the HTML structure of the form.

Ultimately, configuring a custom HTML structure of a form layout is only possible if the "automatically generated form layout" setting is changed. This requires you to instruct the client to manually edit form layouts, which is unintuitive (and usually impossible).

Custom layouts

Fortunately, the auto-generated form layout functionality can be overriden and customised. The steps are:

  1. Create a class that inherits AbstractDefaultLayout
  2. Create a class that inherits BizForm
  3. Clone the "On-line form" webpart, and change the control that is referenced. For each custom layout, a clone of the webpart needs to be created.

Customising the classes

The classes that generate the layouts inherit the AbstractDefaultLayout class, which contains methods such as CreateField, used for rendering field labels and controls.

Due to the complexity of the AbstractDefaultLayout class, we are better off inheriting the DivDefaultLayout class that already implements the required methods.

public class CustomLayout : DivDefaultLayout
{
	public CustomLayout(BasicForm basicForm, Panel categoryListPanel) : base(basicForm, categoryListPanel)
    {
    }
}

With our class set up, we can proceed with overriding the CreateField method. This method is responsible for constructing the required HTML elements for each form field.

The first thing we should do when overriding the CreateField method is to make sure we do not need to deal with the form "design mode". Design mode represents the state of the form while it is being edited in the "Form builder". As this is not something we need to customise, we can use the behaviour of the base method.

public class CustomLayout : DivDefaultLayout
{
	public CustomLayout(BasicForm basicForm, Panel categoryListPanel) : base(basicForm, categoryListPanel)
    {
    }
    
    protected override void CreateField(FieldCreationArgs args)
    {
    	if (BasicForm.IsDesignMode)
        {
        	base.CreateField(args);
            return;
        }
    }
}

We can now use the AddControlToPanel method to add the necessary elements to the form. This includes elements that define the HTML structure of our form, as well as control labels and actual form controls.

void AbstractLayout.AddControlToPanel(Control control, IDataDefinitionItem parentControl);

For example, to add a <div> element:

protected override void CreateField(FieldCreationArgs args)
{
  if (BasicForm.IsDesignMode)
  {
  	base.CreateField(args);
  	return;
  }
  
  var panel = new Panel();
  panel.CssClass = "my-div";
  
  AddControlToPanel(panel, args.FormFieldInfo);
}

Providing the args.FormFieldInfo property to the AddControlToPanel method ensures our <div> element is added to the HTML structure of the field that is being created. Remember - CreateField runs for each field in the form.

Another approach to add elements is to add LiteralControl objects, as it allows for a "flat" approach of constructing the form.

protected override void CreateField(FieldCreationArgs args)
{
  if (BasicForm.IsDesignMode)
  {
  	base.CreateField(args);
  	return;
  }
  
  AddControlToPanel(new LiteralControl("<div class='my-div'>"), args.FormFieldInfo);
  
  // More elements here..
  
  AddControlToPanel(new LiteralControl("</div>"), args.FormFieldInfo);
}

Form controls and labels

Combined with the AddControlToPanel method, we can use CreateFieldLabel and CreateEditingFormControl methods to create labels and form controls.

protected override void CreateField(FieldCreationArgs args)
{
  if (BasicForm.IsDesignMode)
  {
  	base.CreateField(args);
  	return;
  }
  
  AddControlToPanel(new LiteralControl("<div class='field'>"), args.FormFieldInfo);
  
  AddControlToPanel(CreateFieldLabel(args.FormFieldInfo), args.FormFieldInfo);
  AddControlToPanel(CreateEditingFormControl(args.FormFieldInfo), args.FormFieldInfo);
  
  AddControlToPanel(new LiteralControl("</div>"), args.FormFieldInfo);
}

Enabling CSS classes

As the CreateField method has a FieldCreationArgs parameter with a FormFieldInfo property, we can use the GetPropertyValue method to retrieve the values for the CSS classes defined under the "CSS styles" section of the form field properties.

For example, to get the field CSS class:

var fieldCssClass = args.FormFieldInfo.GetPropertyValue(FormFieldPropertyEnum.FieldCssClass);

We can then integrate this value when creating LiteralControl objects to construct our markup. One thing we need to consider is that the CSS class value can be empty. To accommodate this scenario, we can use the CMS.Helpers.CssHelper.GetCssClassAttribute(string cssClass) method, which returns a class attribute if the string parameter is not empty - otherwise, an empty string is returned.

protected override void CreateField(FieldCreationArgs args)
{
  var fieldCssClass = args.FormFieldInfo.GetPropertyValue(FormFieldPropertyEnum.FieldCssClass);
  
  // . . .
  
  var fieldCssClassAttribute = CssHelper.GetCssClassAttribute(fieldCssClass);
  
  AddControlToPanel(new LiteralControl(string.Format("<div{0}>", fieldCssClassAttribute)), args.FormFieldInfo);

Creating a custom BizForm class

The final step is to create a custom class that inherits the BizForm class. We can use it to load our previously created custom layout, by overriding the LoadDefaultLayout method.

public class CustomBizForm : BizForm
{
  protected override void LoadDefaultLayout()
  {
    // Create a new instance of the custom layout
    var layout = new CustomLayout(this, categoryListPanel);
    
    // Load the layout, and assign it as the layout of the form
    layout.LoadLayout();
    Layout = layout;
  }
}

We can reference this class in our custom BizForm webpart.

<%@ Control Language="C#" AutoEventWireup="true" Inherits="CMSWebParts_Custom_CustomBizForm" CodeFile="~/CMSWebParts/Custom/CustomBizForm.ascx.cs" %>
<%@ Register TagPrefix="KenticoTricks" Namespace="KenticoTricks.FormLayouts" Assembly="KenticoTricks.FormLayouts" %>

<KenticoTricks:CustomBizForm ID="viewBiz" runat="server" IsLiveSite="true" />

Example form

Let's look at implementing the markup of a Bootstrap horizontal form in a custom layout class.

This would be the markup required for our form:

<div class="form-horizontal">
  <div class="form-group">
    <label for="input" class="col-sm-2 control-label">(label text)</label>
    <div class="col-sm-10">
      (input control)
    </div>
  </div>
  
  <div class="form-group">
    <div class="col-sm-offset-2 col-sm-10">
        (submit button)
    </div>
  </div>
</div>

Our CreateField method would then look like this:

protected override void CreateField(FieldCreationArgs args)
{
  if (BasicForm.IsDesignMode)
  {
    base.CreateField(args);
    return;
  }
  
  if (BasicForm.FieldsToHide.Contains(args.FormFieldInfo.Name))
  {
    args.FormFieldInfo.Visible = false;
  }
  
  var labelCssClass = args.FormFieldInfo.GetPropertyValue(FormFieldPropertyEnum.CaptionCssClass);
  
  // Add form group
  var pnl = new Panel { CssClass = "form-group" };
  AddControlToPanel(pnl, args.FormFieldInfo);
  
  // Add label
  var fieldLabel = CreateFieldLabel(args.FormFieldInfo);
  fieldLabel.CssClass = "col-sm-2 control-label " + labelCssClass;
  pnl.Controls.Add(fieldLabel);
  
  // Open fields div
  pnl.Controls.Add(new LiteralControl("<div class='col-sm-10'>"));
  
  // Add form control
  pnl.Controls.Add(CreateEditingFormControl(args.FormFieldInfo));
  
  // Close field div
  pnl.Controls.Add(new LiteralControl("</div>"));
}

Configuring the submit button markup

The submit button is automatically added to the form. By overriding the Load method, we can define different markup:

protected override void Load()
{
  base.Load();
  
  // Get the submit button control
  var submitButton = (Control)GetSubmitButton();
  
  // Create a new panel to contain the submit button
  var submitButtonPanel = new Panel { CssClass = "form-group" };
  
  // Add wrapper div and submit button to panel
  submitButtonPanel.Controls.Add(new LiteralControl("<div class='col-sm-offset-2 col-sm-10'>"));
  submitButtonPanel.Controls.Add(submitButton);
  submitButtonPanel.Controls.Add(new LiteralControl("</div>"));
  
  // Add the panel to the form
  FormPanel.Controls.Add(submitButtonPanel);
}
Written by Kristian Bortnik


Tags

Comments