Sitecore
Sitecore 9 – Creating a custom send email submit action for xForms
April 14, 2018
0

After setting up my Sitecore 9 instance I spent some time wondering around the new instance and was drawn to the new Forms tool which is now a core feature of the platform. After toying around with it a bit I found a lot I really liked:

Although there were also some things I liked less:

  • No out of the box send email action which allows for inserting data from Form Fields
  • No out of the box captcha
  • Css classes are free format text instead of choice drop downs (not friendly for content editors at all)

Since I was already experimenting I chose to tackle the issue “No out of the box send email action which allows for inserting data from Form Fields” since a lot of my clients use WFFM for a contact us form.

What do you need to get started with creating a custom submit action?

To create a custom submit action you do not actually need a lot:

Let’s get started!

Now that we got the prerequisites out of the way we can get started with creating the custom action. For a large part we can use the very good guide provided by Sitecore at “https://doc.sitecore.net/sitecore_experience_platform/digital_marketing/sitecore_forms/setting_up_and_configuring/walkthrough_creating_a_custom_submit_action

To start off we will follow the whole guide but instead of building a logger, we will create a Send Email action which entails using the following code for the action:

using System.Linq;
using Sitecore.Diagnostics;
using Sitecore.ExperienceForms.Models;
using Sitecore.ExperienceForms.Processing;
using Sitecore.ExperienceForms.Processing.Actions;
using static System.FormattableString;
namespace Sitecore.ExperienceForms.Samples.SubmitActions
{
    ///

<summary>
    /// Executes a submit action for sending an email
    /// </summary>


    /// <seealso cref="Sitecore.ExperienceForms.Processing.Actions.SubmitActionBase{TParametersData}" />
    public class SendEmailSubmit : SubmitActionBase<string>
    {
        ///

<summary>
        /// Initializes a new instance of the <see cref="LogSubmit"/> class.
        /// </summary>


        /// <param name="submitActionData">The submit action data.</param>
        public SendEmailSubmit(ISubmitActionData submitActionData) : base(submitActionData)
        {
        }
        ///

<summary>
        /// Tries to convert the specified <paramref name="value" /> to an instance of the specified target type.
        /// </summary>


        /// <param name="value">The value.</param>
        /// <param name="target">The target object.</param>
        /// <returns>
        /// true if <paramref name="value" /> was converted successfully; otherwise, false.
        /// </returns>
        protected override bool TryParse(string value, out string target)
        {
            target = string.Empty;
            return true;
        }
        ///

<summary>
        /// Executes the action with the specified <paramref name="data" />.
        /// </summary>


        /// <param name="data">The data.</param>
        /// <param name="formSubmitContext">The form submit context.</param>
        /// <returns>
        ///   <c>true</c> if the action is executed correctly; otherwise <c>false</c>
        /// </returns>
        protected override bool Execute(string data, FormSubmitContext formSubmitContext)
        {
            return true;
        }
    }
}

This will be our base code which we will extend later on, it is just the skeleton for now, doing nothing.

Another point where we diver is in creating the action in Sitecore: we will not use the “Log Submit” name but will use “Send Email” and we will set it to the correct model (our SendEmailSubmit). The icon in appearance can be anything you like.

So now that we got the basics out of the way, let’s go to the fun part: adding a Settings window.

Setting up the settings dialog

Like mentioned before the dialogs for the ExperienceForms are to be created using SPEAK 2. The section will cover how we can do that and what you need to be able to build your own form.

Keep in mind that the dialog that we will be created is by no means finished but serves as a good starting point to get to a final version.

What we need to setup a dialog are the following things:

  • Layout definition in core database
  • Javascript to manage the dialog and handle all interactions
  • Connect the layout to the action
  • Changing our skeleton code to handle the parameters from the dialog.

We will start off with creating the layout definition in the core database:

  1. Since I was unable to find the correct branch template I started off by duplicating the Trigger Goal windows definition found at “/sitecore/client/Applications/FormsBuilder/Components/Layouts/Actions/TriggerGoal”
  2. Create a duplicate and name it “Send Email”
  3. On the duplicate, set the display name to “Send Email” and set the Browser title to “Send Email”
  4. On the new Layout (the Send Email) open the presentation Details and the delete the “Button” and “ItemTreeView” renderings so that the result looks like :
    image.png
    We are doing this to remove a couple of the elements of the original Trigger Goal window.
  5. Now we are going to add the renderings which represent the elements needed for the Send Email window. We start off with opening Sitecore Rocks and navigation to our Send email action and open the Design Layout window:
    image.png
  6. Now add the renderings for 3 textboxes, 4 texts and a Text area:
    image.png
    image.png
    image.png
  7. Name them “ToTextBox”, “FromTextBox”, “SubjectTextBox”, “ToLabel”, “FromLabel”, “SubjectLabel”, “BodyTextAreaLabel” and “BodyTextArea”
  8. Set the placeholder of all the new controls to “MainBorder.Content” :
    image.png
  9. The resulting layout should look like the following:
    image.png
  10. Now open the page Settings node in the content tree :
    image.png
  11. Now delete the items “CreateGoalButton” and “ItemTreeView”
  12. Now insert a item of the template “TextArea Parameters” and name it “BodyTextArea”:
    image.png
  13. Now insert 3 items of the template “TextBox Parameters” and name them “ToTextBox”, “FromTextBox” and “SubjectTextBox”.
  14. Lastly insert 4 items of the template “Text” and name them “ToLabel”, “FromLabel”, “SubjectLabel” and “BodyTextAreaLabel”
    This needed to be done to create settings items for our renderings, it is important that all the created items have the exact same name as the controls. The resulting item list should be like the following:
    image.png

We are now done creating everything in Sitecore and can now move on to the programming side of things.

Creating the dialog javascript code

Since the dialog exists out of the two things : the layout definition and a piece of javascript which handles all the interaction with the dialog, including loading saved values and saving the values.

This javascript file we will be using is the following:

(function (speak) {
  var parentApp = window.parent.Sitecore.Speak.app.findApplication('EditActionSubAppRenderer');
  speak.pageCode([], function () {
    return {
      initialized: function () {
        this.on({
          "loaded": this.loadDone
        }, this);

        this.ToTextBox.on("change:Value", this.changedTextValue, this);
        this.BodyTextArea.on("change:Value", this.changedTextValue, this);
        this.SubjectTextBox.on("change:Value", this.changedTextValue, this);
        this.FromTextBox.on("change:Value", this.changedTextValue, this);

        if (parentApp) {
          parentApp.loadDone(this, this.HeaderTitle.Text, this.HeaderSubtitle.Text);
        }
      },

      changedTextValue: function () {
        var isSelectable = this.ToTextBox.Value && this.BodyTextArea.Value && this.SubjectTextBox.Value && this.FromTextBox.Value;
        parentApp.setSelectability(this, isSelectable, "");
      },

      loadDone: function (parameters) {
        this.Parameters = parameters || {};
        this.ToTextBox.Value = this.Parameters.To;
        this.BodyTextArea.Value = this.Parameters.Body;
        this.FromTextBox.Value = this.Parameters.From;
        this.SubjectTextBox.Value = this.Parameters.Subject;
      },

      getData: function () {
        this.Parameters.To= this.ToTextBox.Value ;
        this.Parameters.Body = this.BodyTextArea.Value;
        this.Parameters.From = this.FromTextBox.Value;
        this.Parameters.Subject = this.SubjectTextBox.Value;
        return this.Parameters;
      }
    };
  });
})(Sitecore.Speak);

 

When saving this script make sure you place it somewhere so that when you publish this file to sitecore it will end up in the “sitecore\shell\client\Applications\FormsBuilder\Layouts\Actions” folder.

 

This script takes the following actions:
Initialization

initialized: function () {
        this.on({
          "loaded": this.loadDone
        }, this);

        this.ToTextBox.on("change:Value", this.changedTextValue, this);
        this.BodyTextArea.on("change:Value", this.changedTextValue, this);
        this.SubjectTextBox.on("change:Value", this.changedTextValue, this);
        this.FromTextBox.on("change:Value", this.changedTextValue, this);

        if (parentApp) {
          parentApp.loadDone(this, this.HeaderTitle.Text, this.HeaderSubtitle.Text);
        }
      },

In the initialization it binds the change:Vlaue event to the ToTextBox and the BodyTextArea. This is an event that triggers after a input element loses focus and the value has been changed.

It will execute the function changedTextValue when triggered.

 

It ends with notifying the parent the dialog has finished loading.
 

changedTextValue function



      changedTextValue: function () {
        var isSelectable = this.ToTextBox.Value && this.BodyTextArea.Value && this.SubjectTextBox.Value && this.FromTextBox.Value;
        parentApp.setSelectability(this, isSelectable, "");

In the changedTextValue function we calculate if the ToTextBox and BodyTextArea have content in them. We then pass the result to the setSelectability function. If the value is true then this function will enable the dialogs buttons.

 

loadDone function

      loadDone: function (parameters) {
        this.Parameters = parameters || {};
        this.ToTextBox.Value = this.Parameters.To;
        this.BodyTextArea.Value = this.Parameters.Body;
        this.FromTextBox.Value = this.Parameters.From;
        this.SubjectTextBox.Value = this.Parameters.Subject;
      },

The load done function takes care of filling our form elements with previously setup values.

 

getData function

getData: function () {
        this.Parameters.To= this.ToTextBox.Value ;
        this.Parameters.Body = this.BodyTextArea.Value;
        this.Parameters.From = this.FromTextBox.Value;
        this.Parameters.Subject = this.SubjectTextBox.Value;
        return this.Parameters;
      }

The getData function is called by Sitecore when the dialog closes. The function will return the values of our form elements in json format.

 

After creating the javascript file we will need to connect it to the layout:

  1. Open Sitecore rocks and navigate to the Send Email item in the core database:
    image.png
  2. Open the design layout dialog:
    image.png
  3. Open the properties of the PageCode rendering and change the value of “PageCodeScriptFileName” to “/sitecore/shell/client/Applications/FormsBuilder/Layouts/Actions/SendEmail.js”:
    image.png
  4. Save your changes

 

Connecting the dialog to our action backend code

Now that everything on the dialog is done we can move to finishing our save action backend code. Our final file will look like the following:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mail;
using System.Reflection;
using Derk.Forms.Feature.Email.SaveActions.Models;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.ExperienceForms.Models;
using Sitecore.ExperienceForms.Processing;
using Sitecore.ExperienceForms.Processing.Actions;
using System.Text.RegularExpressions;

namespace Derk.Forms.Feature.Email.SaveActions
{
    public class SendEmail: SubmitActionBase<SendEmailData>
    {
        protected Regex TextTokenInputs = new Regex("\\$\\(([^$()]+)\\)", RegexOptions.Compiled);
        private FormSubmitContext _formSubmitContext;

        public SendEmail(ISubmitActionData submitActionData) : base(submitActionData)
        {
        }

        protected override bool Execute(SendEmailData data, FormSubmitContext formSubmitContext)
        {
            Assert.ArgumentNotNull(formSubmitContext, nameof(formSubmitContext));
            this._formSubmitContext = formSubmitContext;
            if (!formSubmitContext.HasErrors 
                && !string.IsNullOrWhiteSpace(data.From) 
                && !string.IsNullOrWhiteSpace(data.To) 
                && !string.IsNullOrWhiteSpace(data.Subject) 
                && !string.IsNullOrWhiteSpace(data.Body) )
            {
                try
                {
                    //check  fields for tokens
                    data.To = ReplaceTokens(data.To);
                    data.From = ReplaceTokens(data.From);
                    data.Subject = ReplaceTokens(data.From);
                    data.Body = ReplaceTokens(data.Body);

                    MainUtil.SendMail(new MailMessage(data.From, data.To, data.Subject, data.Body));
                }
                catch (Exception ex)
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
            return true;
        }

        private string ReplaceTokens(string value)
        {

            foreach (Match match in this.TextTokenInputs.Matches(value))
            {
                string textVariableKey = match.Groups[1].Value;
                if (string.IsNullOrWhiteSpace(textVariableKey))
                {
                    continue;
                }

                var formField= this._formSubmitContext.Fields.FirstOrDefault(f => f.Name == textVariableKey);                

                if (formField != null)
                {
                    PropertyInfo property = formField.GetType().GetProperty("Value");

                    string formFieldValue = "";

                    if (property != null)
                    {
                        object formFieldObject = property.GetValue(formField);
                        formFieldValue = ParseFieldValue(formFieldObject);
                    }
                    

                    value = value.Replace(value, formFieldValue);
                }
            }

            return value;
        }

        private string ParseFieldValue(object postedValue)
        {
            Assert.ArgumentNotNull(postedValue, "postedValue");
            List<string> strs = new List<string>();
            IList lists = postedValue as IList;
            if (lists == null)
            {
                strs.Add(postedValue.ToString());
            }
            else
            {
                foreach (object obj in lists)
                {
                    strs.Add(obj.ToString());
                }
            }
            return string.Join(",", strs);
        }


    }
}

 

This code basically does the following things:

  • Check if the settings of the action are set
  • Check if the form does not have any errors
  • Replace any tokens in the $() format like $(Email) where email is the form field name.
  • Send the email using the Sitecore MainUtil

 

Please note that the code is not production ready as it does not have the proper error/exception handling. I would strongly advice adding that yourself.

 

Just to explain the code we will dive in some areas of interest.

 

The data model

For the send email action we need to use a custom data model to receive our settings from the action. The model is a simple class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Derk.Forms.Feature.Email.SaveActions.Models
{
    public class SendEmailData
    {
        public string To { get; set; }

        public string Subject { get; set; }

        public string From { get; set; }

        public string Body { get; set; }
    }
}

 

To use this class in our submit action we have a couple of sections in our code :

public class SendEmail: SubmitActionBase<SendEmailData>

Here we basically tell our Base class that we will be using the SendEmailData class as data model

protected override bool Execute(SendEmailData data, FormSubmitContext formSubmitContext)

Here we make sure that our Execute method actually uses our new model.

Replacing the tokens

To make our emails useful we need to replace tokens in our action settings. These tokens will be replaced by values from the form fields so they need to be dynamic. In my code I chose to go with the $() format like $(Email) where email is the form field name.

The reason for this is that it is a simple format and SxA is already using it so it keeps it similar for the content editor.

To make all this work we have a couple of sections in the code:

protected Regex TextTokenInputs = new Regex("\\$\\(([^$()]+)\\)", RegexOptions.Compiled);

This is the definition of the regex we will use to identify the $() format.

  data.To = ReplaceTokens(data.To);
  data.From = ReplaceTokens(data.From);
  data.Subject = ReplaceTokens(data.From);
  data.Body = ReplaceTokens(data.Body);

Here we replace the tokens in any of the 4 settings.

private string ReplaceTokens(string value)
        {

            foreach (Match match in this.TextTokenInputs.Matches(value))
            {
                string textVariableKey = match.Groups[1].Value;
                if (string.IsNullOrWhiteSpace(textVariableKey))
                {
                    continue;
                }

                var formField= this._formSubmitContext.Fields.FirstOrDefault(f => f.Name == textVariableKey);                

                if (formField != null)
                {
                    PropertyInfo property = formField.GetType().GetProperty("Value");

                    string formFieldValue = "";

                    if (property != null)
                    {
                        object formFieldObject = property.GetValue(formField);
                        formFieldValue = ParseFieldValue(formFieldObject);
                    }
                    

                    value = value.Replace(value, formFieldValue);
                }
            }

            return value;
        }

The ReplaceTokens function iterates through any found token and tries to replace this token with a value from the Form Field it is relating to. We do this by comparing the value between the $() to the names of the Fields. If it finds a field with the correct name we will parse its value and replace the token with that value. If it is not found we actually just remove the token.

private string ParseFieldValue(object postedValue)
        {
            Assert.ArgumentNotNull(postedValue, "postedValue");
            List<string> strs = new List<string>();
            IList lists = postedValue as IList;
            if (lists == null)
            {
                strs.Add(postedValue.ToString());
            }
            else
            {
                foreach (object obj in lists)
                {
                    strs.Add(obj.ToString());
                }
            }
            return string.Join(",", strs);
        }

The ParseFieldValue method was actually taken from the out of the box SaveData action since i could find no way of identifying the form fields types to enable retrieving the value. So i just took a look at what Sitecore did to make it work.

 

Wrapping it up

This blog post has become longer then I anticipated but I wanted to make sure the process of setting up an action would be clear.

 

With all the steps listed in this blog post you should be able to create your own custom actions with a. I feel that although the creating of such an action has be poorly documented it is still a huge improvement over webforms for marketeers and I had a lot of fun in figuring this one out.

 

Please note that the code in this article is not production ready and I would heavily advise introducing proper error and exception handling.

 

One of my next posts will be about creating your own marketing automation action since I had some challenges in figuring that one out as well.

Leave a Reply