In one of the projects I worked on, a client asked to show some sort of progress indicator when submitting a Form. The forms were built using Sitecore Forms with Sitecore Forms Extensions on a 9.1.1 version of the Sitecore platform.
My first thought was to add a special class on the button from the Forms Dashboard, and using JavaScript and CSS make the loading happen on click event of the button. It sounded simple, but I start getting all strange behaviors, plus I know that Sitecore Forms uses jQuery Unobtrusive Ajax, so I start digging into how this library handles the JavaScript functions of actually submitting the form.
I came across this great article Using jQuery Unobtrusive AJAX in ASP.NET Core Razor Pages, It has a table that details the custom attributes that control the behavior of jQuery Unobtrusive AJAX
Attribute | Description |
data-ajax | Must be set to true to activate unobtrusive Ajax on the target element. |
data-ajax-confirm | Gets or sets the message to display in a confirmation window before a request is submitted. |
data-ajax-method | Gets or sets the HTTP request method (“Get” or “Post”). |
data-ajax-mode | Gets or sets the mode that specifies how to insert the response into the target DOM element. Valid values are before , after and replace . Default is replace |
data-ajax-loading-duration | Gets or sets a value, in milliseconds, that controls the duration of the animation when showing or hiding the loading element. |
data-ajax-loading | Gets or sets the id attribute of an HTML element that is displayed while the Ajax function is loading. |
data-ajax-begin | Gets or sets the name of the JavaScript function to call immediately before the page is updated. |
data-ajax-complete | Gets or sets the JavaScript function to call when response data has been instantiated but before the page is updated. |
data-ajax-failure | Gets or sets the JavaScript function to call if the page update fails. |
data-ajax-success | Gets or sets the JavaScript function to call after the page is successfully updated. |
data-ajax-update | Gets or sets the ID of the DOM element to update by using the response from the server. |
data-ajax-url | Gets or sets the URL to make the request to. |
I figured I could use data-ajax-loading
or data-ajax-begin
to solve my problem, I ended up using data-ajax-begin, as I didn’t want to add an HTML element on each of the forms I have, instead, I’ll call a JavaScript function that add a class on the Submit button, which has CSS rules defined that will show the animation.
The Implementation
When I inspect the form element in the browser, to see those data attributes, I found only a few are implemented, data-ajax-begin is not one of them.
With the help of dotPeak, I found out that Sitecore has a pipeline to initialize those attributes.
public class InitializeAjaxOptions : MvcPipelineProcessor<RenderFormEventArgs>
{
private readonly IFormRenderingContext _formRenderingContext;
public InitializeAjaxOptions(IFormRenderingContext formRenderingContext)
{
Assert.ArgumentNotNull((object) formRenderingContext, nameof (formRenderingContext));
this._formRenderingContext = formRenderingContext;
}
public override void Process(RenderFormEventArgs args)
{
Assert.ArgumentNotNull((object) args, nameof (args));
if (!args.ViewModel.IsAjax)
return;
if (args.HtmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled)
{
IDictionary<string, object> unobtrusiveHtmlAttributes = new AjaxOptions()
{
HttpMethod = "Post",
InsertionMode = InsertionMode.ReplaceWith,
UpdateTargetId = args.FormHtmlId,
OnSuccess = FormattableString.Invariant(FormattableStringFactory.Create("$.validator.unobtrusive.parse('#{0}');$.fxbFormTracker.parse('#{1}');", (object) args.FormHtmlId, (object) args.FormHtmlId))
}.ToUnobtrusiveHtmlAttributes();
foreach (string key in (IEnumerable<string>) unobtrusiveHtmlAttributes.Keys)
args.Attributes[key] = unobtrusiveHtmlAttributes[key];
}
if (!args.IsPost)
{
args.QueryString.Add("fxb.FormItemId", (object) args.ViewModel.ItemId.ToGuid());
args.QueryString.Add("fxb.HtmlPrefix", (object) this._formRenderingContext.Prefix.Trim('.'));
}
args.RouteName = "FormBuilder";
}
}
So I created a new pipeline to replace it, which would add the data attributes I need.
public class InitializeAjaxOptions : MvcPipelineProcessor<RenderFormEventArgs>
{
private readonly IFormRenderingContext _formRenderingContext;
public InitializeAjaxOptions(IFormRenderingContext formRenderingContext)
{
Assert.ArgumentNotNull((object)formRenderingContext, nameof(formRenderingContext));
this._formRenderingContext = formRenderingContext;
}
public override void Process(RenderFormEventArgs args)
{
Assert.ArgumentNotNull((object)args, nameof(args));
if (!args.ViewModel.IsAjax)
return;
if (args.HtmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled)
{
IDictionary<string, object> unobtrusiveHtmlAttributes = new AjaxOptions()
{
HttpMethod = "Post",
InsertionMode = InsertionMode.ReplaceWith,
UpdateTargetId = args.FormHtmlId,
OnSuccess = FormattableString.Invariant(FormattableStringFactory.Create("$.validator.unobtrusive.parse('#{0}');$.fxbFormTracker.parse('#{1}');", (object)args.FormHtmlId, (object)args.FormHtmlId)),
OnBegin = "OnBegin",
}.ToUnobtrusiveHtmlAttributes();
foreach (string key in unobtrusiveHtmlAttributes.Keys)
args.Attributes[key] = unobtrusiveHtmlAttributes[key];
}
if (!args.IsPost)
{
args.QueryString.Add("fxb.FormItemId", (object)args.ViewModel.ItemId.ToGuid());
args.QueryString.Add("fxb.HtmlPrefix", (object)this._formRenderingContext.Prefix.Trim('.'));
}
args.RouteName = "FormBuilder";
}
}
This line (OnBegin = “OnBegin”) will add the data-ajax-begin attribute to all forms, with the value “OnBegin” which is the JavaScript function I’ll need to write.
The JavaScript function is very simple I’ll just select all buttons with a specific class, and add another class.
OnBegin = function (xhr) {
$(".loader-button").addClass("loader");
}
With help of a Front End developer, I added the CSS rules that will create the animation when the user submits the form.
Of course don’t forget to add the patch configuration file for the new pipeline.
<forms.renderForm>
<processor type="Sitecore.ExperienceForms.Mvc.Pipelines.RenderForm.InitializeAjaxOptions, Sitecore.ExperienceForms.Mvc">
<patch:attribute name="type">ProjectX.Feature.Forms.Pipelines.RenderForm.InitializeAjaxOptions,
ProjectX.Feature.Forms</patch:attribute>
</processor>
</forms.renderForm>
That’s it, and it worked as expected.