Show progress indicator while submitting Sitecore Form

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

AttributeDescription
data-ajaxMust be set to true to activate unobtrusive Ajax on the target element.
data-ajax-confirmGets or sets the message to display in a confirmation window before a request is submitted.
data-ajax-methodGets or sets the HTTP request method (“Get” or “Post”).
data-ajax-modeGets 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-durationGets or sets a value, in milliseconds, that controls the duration of the animation when showing or hiding the loading element.
data-ajax-loadingGets or sets the id attribute of an HTML element that is displayed while the Ajax function is loading.
data-ajax-beginGets or sets the name of the JavaScript function to call immediately before the page is updated.
data-ajax-completeGets or sets the JavaScript function to call when response data has been instantiated but before the page is updated.
data-ajax-failureGets or sets the JavaScript function to call if the page update fails.
data-ajax-successGets or sets the JavaScript function to call after the page is successfully updated.
data-ajax-updateGets or sets the ID of the DOM element to update by using the response from the server.
data-ajax-urlGets 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.