Create Custom Sitecore Forms field with validation

Creating a custom Sitecore Forms field is easy to follow, first I recommend to start with official Sitecore documentation Sitecore guide for adding custom fields . This post is a helpful guide for you if you want to create a Sitecore Custom field and you want also to do backend validation for that field.

Before creating a custom field please make sure you have Sitecore Rocks for that in your Visual Studio 2019 instance(from what I found, Sitecore Rocks is not compatible with the newest version of Visual Studio). You need Sitecore Rocks because not all the items that we need to create in this post are available via Content Editor.

1. Create field template in master DB

Add a new template somewhere under /sitecore/templates, depending on your solution architecture. I would be creating mine under /sitecore/templates/System/Forms/Fields. The template should be based on /sitecore/templates/System/Templates/Template.

Your template need to inherit from Input template {0908030B-4564-42EA-A6FA-C7A5A2D921A8} or the Save Settings template (Templates/System/Forms/Save Settings)

Add needed fields and create standard values for your template (otherwise your field wouldn’t be droppable in Forms designer):

Important note: If your element is an input inheriting Save Settings template, open __Standard Values and make sure that the Allow Save option is checked.

2. Create a view model class

View model class has to derive from FieldViewModel class. The class has to be marked as Serializable for the field values to stored properly in session state.

If you’re creating a custom input, that would allow user to actually submit some data, it’s a good idea to inherit the InputViewModel<TValueType> class (or some more specific class that already inherits InputViewModel class) – it has already everything prepared for validation, has the “Allow Save” field logic ready, etc.

using Sitecore.ExperienceForms.Mvc.Models.Fields;
using Sitecore.Data.Items;
using System;
using Sitecore;
 
namespace ScSandbox.Foundation.SitecoreFormsExtensions.Models.Fields
{
[Serializable]
public class DynamicPromoInfoViewModel : InputViewModel<DynamicPromodInfo>
{
    public DynamicPromoInfoViewModel() { }


    private DynamicPromodInfo SafeValue()
    {
        this.CheckValueIsNotNull();
        return Value;
    }


    #region Input Properties
    public virtual long? PromoCode
    {
        get
        {
            return this.SafeValue().PromoCode;
        }
        set
        {
            this.SafeValue().PromoCode = value;
        }
    }

    public string PromoGevonden
    {
        get
        {
            return this.SafeValue().PromoGevonden;
        }
        set
        {
            this.SafeValue().PromoGevonden = value;
        }
    }


    public override DynamicPromodInfo Value
    {
        get
        {
            return base.Value;
        }
        set
        {
            base.Value = value;
        }
    }


    #endregion // Input Properties


    #region Dropdown properties


    public string DisplayFieldName { get; set; } = string.Empty;
    public string ValueFieldName { get; set; } = string.Empty;


    [NonSerialized]
    private IFieldSettingsManager<ListFieldItemCollection> _dataSourceSettingsManager;


    public string DataSource { get; set; } = string.Empty;
    protected virtual IFieldSettingsManager<ListFieldItemCollection> DataSourceSettingsManager
    {
        get
        {
            IFieldSettingsManager<ListFieldItemCollection> fieldSettingsManager = this._dataSourceSettingsManager;
            if (fieldSettingsManager == null)
            {
                IFieldSettingsManager<ListFieldItemCollection> service = ServiceLocator.ServiceProvider.GetService<IFieldSettingsManager<ListFieldItemCollection>>();
                IFieldSettingsManager<ListFieldItemCollection> fieldSettingsManager1 = service;
                this._dataSourceSettingsManager = service;
                fieldSettingsManager = fieldSettingsManager1;
            }
            return fieldSettingsManager;
        }
    }
    public bool IsDynamic
    {
        get;
        set;
    }


    public List<ListFieldItem> Items { get; } = new List<ListFieldItem>();


    protected virtual void OnValueChanged(IEnumerable<string> value)
    {
        this.Items.ForEach((ListFieldItem i) => i.Selected = value.Contains<string>(i.Value));
    }


    protected virtual ListFieldItemCollection UpdateDataSourceSettings(Item item)
    {
        Assert.ArgumentNotNull(item, "item");
        ListFieldItemCollection listFieldItemCollection = new ListFieldItemCollection();
        listFieldItemCollection.AddRange(this.Items);
        this.DataSourceSettingsManager.SaveSettings(item, listFieldItemCollection);
        return listFieldItemCollection;
    }
    protected virtual void InitializeDataSourceSettings(Item item)
    {
        Assert.ArgumentNotNull(item, "item");
        ListFieldItemCollection settings = this.DataSourceSettingsManager.GetSettings(item);
        if (settings != null)
        {
            this.Items.Clear();
            this.Items.AddRange(settings);
        }
    }
    #endregion // Dropdown properties


    #region Label/Placeholders


    public string PromoCodeLabel { get; set; }
    public string CheckButtonLabel { get; set; }
    public string PromoCodePlaceholder { get; set; }
    public string PromoCodeRequired { get; set; }
    public string ErrorMessageNotFound { get; set; }
    public string CheckSuccesMessage { get; set; }


    public string NotificationClass { get; set; } = "error";
    #endregion // Label/Placeholders

    public virtual string Name
    {
        get
        {
            return this.SafeValue().Name;
        }
        set
        {
            this.SafeValue().Name = value;
        }
    }
   
    protected override void InitializeValidations(Item item)
    {
        Assert.ArgumentNotNull(item, "item");
        base.InitializeValidations(item);


        foreach (IValidationElement validation in Validations)
        {
            if (validation is DynamicPromoInfoValidation)
            {
                (validation as DynamicPromoInfoValidation).Model = this as DynamicPromoInfoViewModel;
            }
        }
    }


    protected override void InitItemProperties(Item item)
    {
        Assert.ArgumentNotNull(item, "item");
        base.InitItemProperties(item);


        this.PromoCodeLabel = StringUtil.GetString(item.Fields["PromoCodeLabel"]);
        this.CheckButtonLabel = StringUtil.GetString(item.Fields["CheckButtonLabel"]);
        this.PromoCodePlaceholder = StringUtil.GetString(item.Fields["PromoCodePlaceholder"]);
        this.PromoCodeRequired = StringUtil.GetString(item.Fields["PromoCodeRequired"]);
        this.ErrorMessageNotFound = StringUtil.GetString(item.Fields["ErrorMessageNotFound"]);
        this.CheckSuccesMessage = StringUtil.GetString(item.Fields["CheckSuccesMessage"]);


        this.DataSource = StringUtil.GetString(item.Fields["Datasource"]);
        this.IsDynamic = MainUtil.GetBool(item.Fields["Is Dynamic"], false);
        this.DisplayFieldName = StringUtil.GetString(item.Fields["Display Field Name"]);
        this.ValueFieldName = StringUtil.GetString(item.Fields["Value Field Name"]);
        this.Required = true;
        this.InitializeDataSourceSettings(item);
    }
    protected override void UpdateItemFields(Item item)
    {
        Assert.ArgumentNotNull(item, "item");
        base.UpdateItemFields(item);


        {
            Field field = item.Fields["PromoCodeLabel"];
            if (field != null)
            {
                field.SetValue(this.PromoCodeLabel, false);
            }
        }
        {
            Field field = item.Fields["PromoCodePlaceholder"];
            if (field != null)
            {
                field.SetValue(this.PromoCodePlaceholder, false);
            }
        }
        {
            Field field = item.Fields["PromoCodeRequired"];
            if (field != null)
            {
                field.SetValue(this.PromoCodeRequired, false);
            }
        }
        {
            Field field = item.Fields["ErrorMessageNotFound"];
            if (field != null)
            {
                field.SetValue(this.ErrorMessageNotFound, false);
            }
        }
        {
            Field field = item.Fields["CheckSuccesMessage"];
            if (field != null)
            {
                field.SetValue(this.CheckSuccesMessage, false);
            }
        }
        {
            Field field = item.Fields["CheckButtonLabel"];
            if (field != null)
            {
                field.SetValue(this.CheckButtonLabel, false);
            }
        }
    }


    public override string GetStringValue()
    {
        if (this.Value == null)
        {
            return string.Empty;
        }
        return this.Value.ToString();
    }


    private void CheckValueIsNotNull()
    {
        if (this.Value == null)
        {
            this.Value = new DynamicPromodInfo();
        }
    }
}


[Serializable]
public class DynamicPromodInfo
{
    public DynamicPromodInfo() { }


    public long? PromoCode { get; set; }


    public string Name { get; set; }
    public string Adres { get; set; }
    public string PromoGevonden { get; set; }
    public string NotificationClass { get; set; }


    public string DynamicResult { get; set; }


    public override string ToString()
    {
        if (this.PromoCode.HasValue)
        {
            return string.Format("{0}", this.PromoCode.Value.ToString());
        }
        else
        {
            return null;
        }
    }
}
}

3. Create validation class

public class CheckPromoCodesValidation : ValidationElement<CheckPromoCodes>
{
    public override IEnumerable<ModelClientValidationRule> ClientValidationRules
    {
        get
        {
            return new List<ModelClientValidationRule>();
        }
    }

    public CheckPromoCodesValidation(ValidationDataModel validationItem) : base(validationItem)
    {
    }

    public override void Initialize(object validationModel)
    {
        base.Initialize(validationModel);

    }

    internal CheckPromoCodesViewModel Model { get; set; }
    public override ValidationResult Validate(object value)
    {
        if (value is CheckPromoCodes == false)
        {
            return ValidationResult.Success;
        }
        if (string.IsNullOrEmpty(Model.Naam))
        {
            Model.NotificationClass = "error";
            string msg = Model.ErrorMessageNotFound;
            return new ValidationResult(msg);
        }
        else
        {
            return ValidationResult.Success;
        }
    }
}

4. Create the view for our custom field


@{
    var PromoRequired = Model.MakePromoCodeRequired ? "required = required" : "";
}
<div class="f-Promoinfoform">
    @{
        bool hasGlobalValueError = ViewData.ModelState.IsValidField(Html.NameFor(m => Model.Value).ToString()) == false;

        if (hasGlobalValueError)
        {
            <div id="backendValidationMessage" aria-hidden="false" class="notification notification-@Model.NotificationClass">
                <span class="icon notification-icon">
                    <i aria-hidden="true" class="ic-g-error" title="notification-error"></i>
                    <span class="sr-only">notification-error</span>
                </span>
                <div>
                    <div class="notification-body">
                        @Html.ValidationMessageFor(m => Model.Value, null, null, "p")
                    </div>
                </div>
            </div>
        }
    }


    @{
        string formFieldClass = "formfield";
        if (Model.Required)
        {
            formFieldClass += " required";
        }
        formFieldClass += ViewData.ModelState.IsValidField(Html.NameFor(m => Model.PromoPremiumCode).ToString()) ? " field-validation-valid" : " field-validation-error";

        string PromoFormFieldClass = "formfield";
        if (Model.MakePromoCodeRequired)
        {
            PromoFormFieldClass += " required";
        }
        PromoFormFieldClass += ViewData.ModelState.IsValidField(Html.NameFor(m => Model.PromoCode).ToString()) ? " field-validation-valid" : " field-validation-error";
    }

    @if (Html.Sitecore().IsExperienceFormsEditMode() && string.IsNullOrWhiteSpace(Model.Title))
    {
        <div style="min-height:20px;">
            <strong>PromoCode</strong> Click to edit the label.
        </div>
    }

    <div class="formfield-container">
        <div class="formfield-group">
            <div class="@formFieldClass">
                <label for="@Html.IdFor(m => Model.Value)">@Model.PromoPremiumCodeLabel</label>
                @{
                    if (!string.IsNullOrEmpty(Model.PromoCodeTooltip))
                    {
                        <span class="ic-g-info" title="@Model.PromoPremiumTooltip"></span>
                    }
                }
                <div class="input-group">
                    <div class="input">
                        <input id="@Html.IdFor(m => Model.PromoPremiumCode)"
                               name="@Html.NameFor(m => Model.PromoPremiumCode)"
                               class="CheckPromoPremiumCodeInput"
                               type="text"
                               value="@Model.PromoPremiumCode"
                               placeholder="@Model.PromoPremiumCodePlaceholder"
                               data-sc-tracking="@Model.IsTrackingEnabled"
                               data-sc-field-name="@Model.Name"
                               data-sc-field-key="@Model.ConditionSettings.FieldKey"
                               required="required"                              
                               @Html.GenerateUnobtrusiveValidationAttributes(m => m.PromoPremiumCode) />
                    </div>
                </div>
            </div>
        </div>
        <ul class="error">
            @Html.ValidationMessageFor(m => Model.PromoCode, null, null, "li")
        </ul>
        <div id="PromoCodeMessage" aria-hidden="false" class="hidden notification notification-error">
            <span class="icon notification-icon">
                <i aria-hidden="true" class="ic-g-error" title="notification-error"></i>
                <span class="sr-only">notification-error</span>
            </span>
            <div>
                <div id="errorText" class="hidden notification-body">
                    @Model.ErrorMessageNotFound
                </div>
                <div id="formatErrorText" class="hidden notification-body">
                    @Model.PromoFormatErrorMessage
                </div>
            </div>
        </div>
    </div>

    <div id="PromocodeData" class="hidden">
        <dl class="label-value-list--vertical  label-value-list">
            <dt class="text-small text-light">Name</dt>
            <dd id="checkPromoName" class=""></dd>
            <dt class="text-small text-light">Address</dt>
            <dd id="adressName" class=""></dd>
        </dl>
    </div>

    <div class="formfield-container">
        <div class="formfield-group">
            <div class="@PromoFormFieldClass">
                <label for="@Html.IdFor(m => Model.Value)">@Model.PromoCodeLabel</label>
                @{
                    if (!string.IsNullOrEmpty(Model.PromoCodeTooltip))
                    {
                        <span class="ic-g-info" title="@Model.PromoCodeTooltip"></span>
                    }
                }
                <div class="input-group">
                    <div class="input">
                        <input id="@Html.IdFor(m => Model.PromoCode)"
                               name="@Html.NameFor(m => Model.PromoCode)"
                               class="CheckPromoCodeInput"
                               type="text"
                               value="@Model.PromoCode"
                               placeholder="@Model.PromoCodePlaceholder"
                               data-sc-tracking="@Model.IsTrackingEnabled"
                               data-sc-field-name="@Model.Name"
                               data-sc-field-key="@Model.ConditionSettings.FieldKey"
                               @PromoRequired
                                />
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div id="PromoPersonData" class="hidden">
        <dl class="label-value-list--vertical  label-value-list">
            <dt class="text-small text-light">Name Person</dt>
            <dd id="PersonName" class=""></dd>
        </dl>
    </div>

    <input id="@Html.IdFor(m => Model.Name)"
           name="@Html.NameFor(m => Model.Name)"
           class="NameHidden"
           type="hidden"
           value="@Model.Name"
           placeholder="@Model.Name"
           data-sc-tracking="@Model.IsTrackingEnabled"
           data-sc-field-name="@Model.Name"
           data-sc-field-key="@Model.ConditionSettings.FieldKey" />


    <input id="@Html.IdFor(m => Model.PromoName)"
           name="@Html.NameFor(m => Model.PromoName)"
           class="PromoNameHidden"
           type="hidden"
           value="@Model.PromoName"
           placeholder="@Model.PromoName"
           data-sc-tracking="@Model.IsTrackingEnabled"
           data-sc-field-name="@Model.Name"
           data-sc-field-key="@Model.ConditionSettings.FieldKey" />
</div>

4. Create the Sitecore Forms custom field and the Section item in Core DB using Sitecore Rocks

Form field definitions are stored in Core db under /sitecore/client/Applications/FormsBuilder/Components/Layouts/PropertyGridForm/PageSettings/Settings. I’ve created a folder named Custom for our custom fields there.

To create a new item there based on Form Parameters template {72E58860-AC29-47E1-A1C5-8F9E492DB999} , you would need to use Sitecore Rocks, because template, that your new item should be based on, is not available via Content Editor to choose.

First step, create a new item based on /sitecore/client/Business Component Library/version 2/Layouts/Renderings/Forms/Form/Form Parameters template under our newly created Custom folder.

Second Step, create needed sections under your newly created item. Sections items, i.e. Details, Labels, Conditions, should be based on /sitecore/client/Applications/FormsBuilder/Common/Templates/FormSection template.

Third step is to create the necessary items for each section from our Sitecore Forms custom field. Under each section we will create items based on the /sitecore/client/Business Component Library/version 2/Layouts/Renderings/Forms/Form/Templates/FormTextBox Parameters or /sitecore/client/Business Component Library/version 2/Layouts/Renderings/Forms/Form/Templates/FormCheckBox Parameters

In the Form section for each item under our sections:

fill FormLabel field

check IsLabelOnTop checkbox

set BindingConfiguration – map field model property (in camelCase) to editor property

After we have all the necessary field items under each section we will need to open each section item and add the corresponding items in the ControlDefinitions field, otherwise it will not work.

5. Create field type item in master DB via content editor

Create a new item under /sitecore/system/settings/forms/fieldtypes/custom, based on /sitecore/templates/System/Forms/Field Type template 

In the Settings section set the View Path (the ViewPath is relative to Views/FormBuilder location), set Model Type to view model class you created earlier, set Property Editor to the custom Form Parameters item created earlier in the Core database.

Set Field Template from the Data section to the template you created in the master database.

Leave a Reply

Your email address will not be published. Required fields are marked *