Walkthrough: Creating a custom submit action that updates contact details

Current version: 9.0

You can use the submit button for navigation (previous and next buttons), to submit the form, and/or to trigger submit actions. To all submit buttons, whether used for navigation or for submitting the form, you can add submit actions. For example, the submit action Save data added to the next button makes sure the data is saved to the Forms database when a contact clicks Next to move on to the next page of the form.

By default, you can add the Trigger Goal, Trigger Campaign Activity, Trigger Outcome, Redirect to Page, and Save Data submit items. This walkthrough describes how to create a custom submit action that enables you to select the form fields used to update the contact details.

Map form fields to contact details dialog box.
Note

This walkthrough describes one example of building a custom submit action. Depending on your experience and preferences, you might prefer to do things differently.

Create a submit action class

To submit a form (page), a contact must click the Submit button. You can add different types of actions to perform when a user clicks Submit. For example, the Save Data submit action ensures that data is saved to your database, and the Trigger Campaign Activity submit action selects a preset campaign activity.

In this walkthrough, you create the Update Contact submit action.

The submit action stores the parameters of the JSON object that is passed to the action. The JSON object is parsed into an instance of the type specified in the TParametersData class, in this case, the UpdateContactData class. Therefore, in this example, you create a derived class UpdateContact that inherits from SubmitActionBase<TParametersData> with the UpdateContactData parameter.

To create a submit action class:

  1. Create the UpdateContactData class:

    RequestResponse
    using System;
    
    namespace Sitecore.ExperienceForms.Samples.SubmitActions
    {
        /// <summary>
        /// Data structure of the parameters for executing the update contact submit action.
        /// </summary>
        public class UpdateContactData
        {
            /// <summary>
            /// Gets or sets the email field identifier.
            /// </summary>
            public Guid EmailFieldId { get; set; }
    
            /// <summary>
            /// Gets or sets the first name field identifier.
            /// </summary>
            public Guid FirstNameFieldId { get; set; }
    
            /// <summary>
            /// Gets or sets the last name field identifier.
            /// </summary>
            public Guid LastNameFieldId { get; set; }
        }
    }
  2. Create the UpdateContact class:

    RequestResponse
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Sitecore.Analytics;
    using Sitecore.Diagnostics;
    using Sitecore.ExperienceForms.Models;
    using Sitecore.ExperienceForms.Processing;
    using Sitecore.ExperienceForms.Processing.Actions;
    using Sitecore.XConnect;
    using Sitecore.XConnect.Client;
    using Sitecore.XConnect.Client.Configuration;
    using Sitecore.XConnect.Collection.Model;
    namespace Sitecore.ExperienceForms.Samples.SubmitActions
    {
        /// <summary>
        /// Submit action for updating <see cref="PersonalInformation"/> and <see cref="EmailAddressList"/> facets of a <see cref="XConnect.Contact"/>.
        /// </summary>
        /// <seealso cref="Sitecore.ExperienceForms.Processing.Actions.SubmitActionBase{UpdateContactData}" />
        public class UpdateContact : SubmitActionBase<UpdateContactData>
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="UpdateContact"/> class.
            /// </summary>
            /// <param name="submitActionData">The submit action data.</param>
            public UpdateContact(ISubmitActionData submitActionData) : base(submitActionData)
            {
            }
            /// <summary>
            /// Gets the current tracker.
            /// </summary>
            protected virtual ITracker CurrentTracker => Tracker.Current;
            /// <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(UpdateContactData data, FormSubmitContext formSubmitContext)
            {
                Assert.ArgumentNotNull(data, nameof(data));
                Assert.ArgumentNotNull(formSubmitContext, nameof(formSubmitContext));
                var firstNameField = GetFieldById(data.FirstNameFieldId, formSubmitContext.Fields);
                var lastNameField = GetFieldById(data.LastNameFieldId, formSubmitContext.Fields);
                var emailField = GetFieldById(data.EmailFieldId, formSubmitContext.Fields);
                if (firstNameField == null && lastNameField == null && emailField == null)
                {
                    return false;
                }
                using (var client = CreateClient())
                {
                    try
                    {
                        var source = "Subcribe.Form";
                        var id = CurrentTracker.Contact.ContactId.ToString("N");
                        var identificationManager = ServiceLocator.ServiceProvider.GetRequiredService<IContactIdentificationManager>();
                        IdentificationResult result = identificationManager.IdentifyAs(new KnownContactIdentifier (source, id));
                        if (!result.Success)
                        {
                            throw new Exception ($"{result.ErrorCode}: {result.ErrorMessage}");
                        }
                        CurrentTracker.Session.IdentifyAs(source, id);
                        var trackerIdentifier = new IdentifiedContactReference(source, id);
                        var expandOptions = new ContactExpandOptions(
                            CollectionModel.FacetKeys.PersonalInformation,
                            CollectionModel.FacetKeys.EmailAddressList);
                        Contact contact = client.Get(trackerIdentifier, expandOptions);
                        SetPersonalInformation(GetValue(firstNameField), GetValue(lastNameField), contact, client);
                        SetEmail(GetValue(emailField), contact, client);
                        client.Submit();
                        return true;
                    }
                    catch (Exception ex)
                    {
                        Logger.LogError(ex.Message, ex);
                        return false;
                    }
                }
            }
            /// <summary>
            /// Creates the client.
            /// </summary>
            /// <returns>The <see cref="IXdbContext"/> instance.</returns>
            protected virtual IXdbContext CreateClient()
            {
                return SitecoreXConnectClientConfiguration.GetClient();
            }
            /// <summary>
            /// Gets the field by <paramref name="id" />.
            /// </summary>
            /// <param name="id">The identifier.</param>
            /// <param name="fields">The fields.</param>
            /// <returns>The field with the specified <paramref name="id" />.</returns>
            private static IViewModel GetFieldById(Guid id, IList<IViewModel> fields)
            {
                return fields.FirstOrDefault(f => Guid.Parse(f.ItemId) == id);
            }
            /// <summary>
            /// Gets the <paramref name="field" /> value.
            /// </summary>
            /// <param name="field">The field.</param>
            /// <returns>The field value.</returns>
            private static string GetValue(object field)
            {
                return field?.GetType().GetProperty("Value")?.GetValue(field, null)?.ToString() ?? string.Empty;
            }
            /// <summary>
            /// Sets the <see cref="PersonalInformation"/> facet of the specified <paramref name="contact" />.
            /// </summary>
            /// <param name="firstName">The first name.</param>
            /// <param name="lastName">The last name.</param>
            /// <param name="contact">The contact.</param>
            /// <param name="client">The client.</param>
            private static void SetPersonalInformation(string firstName, string lastName, Contact contact, IXdbContext client)
            {
                if (string.IsNullOrEmpty(firstName) && string.IsNullOrEmpty(lastName))
                {
                    return;
                }
                PersonalInformation personalInfoFacet = contact.Personal() ?? new PersonalInformation();
                if (personalInfoFacet.FirstName == firstName && personalInfoFacet.LastName == lastName)
                {
                    return;
                }
                personalInfoFacet.FirstName = firstName;
                personalInfoFacet.LastName = lastName;
                client.SetPersonal(contact, personalInfoFacet);
            }
            /// <summary>
            /// Sets the <see cref="EmailAddressList"/> facet of the specified <paramref name="contact" />.
            /// </summary>
            /// <param name="email">The email address.</param>
            /// <param name="contact">The contact.</param>
            /// <param name="client">The client.</param>
            private static void SetEmail(string email, Contact contact, IXdbContext client)
            {
                if (string.IsNullOrEmpty(email))
                {
                    return;
                }
                EmailAddressList emailFacet = contact.Emails();
                if (emailFacet == null)
                {
                    emailFacet = new EmailAddressList(new EmailAddress(email, false), "Preferred");
                }
                else
                {
                    if (emailFacet.PreferredEmail?.SmtpAddress == email)
                    {
                        return;
                    }
                    emailFacet.PreferredEmail = new EmailAddress(email, false);
                }
                client.SetEmails(contact, emailFacet);
            }
        }
    }
  3. Build and copy the DLL to your Sitecore instance’s <root>/bin directory.

Create the SPEAK editor control

The next step is to create the UI that enables mapping the form fields to the contact details fields. For Sitecore Forms, submit action editors are located in the core database:

/sitecore/client/Applications/FormsBuilder/Components/Layouts/Actions

Note

In this example, you must have the Sitecore Rocks Visual Studio plugin. The plugin is compatible with Visual Studio 2019 or earlier.

To create the control:

  1. In the core database, navigate to /sitecore/client/Applications/FormsBuilder/Components/Layouts/Actions

  2. Right-click Actions, click Add, and then click New Item.

  3. Select the /sitecore/client/Speak/Templates/Pages/Speak-BasePage template, and in the Enter the name of the new item field, enter UpdateContact and click OK.

  4.  Set BrowserTitle and __Display name to: Update contact.

  5. Right-click the UpdateContact item you just created and click Tasks, and click Design Layout.

  6. In the Layout dialog box, navigate to /sitecore/client/Speak/Layouts/Layouts and select the Speak-FlexLayout layout and click OK.

  7. In the upper-left corner, click Add Rendering and in the Select Renderings dialog box, click All and search for PageCode (/sitecore/client/Speak/Layouts/Renderings/Common/PageCode):

    Forms Select renderings
  8. Select PageCode and click OK.

  9. In the PageCode properties, set the PageCodeScriptFileName property to the JavaScript path that contains the page code script: /sitecore/shell/client/Applications/FormsBuilder/Layouts/Actions/UpdateContact.js

    PageCodeScriptFileName property
  10. Set the SpeakCoreVersion property to Speak 2-x.

  11. Search for and select the Text View rendering (/sitecore/client/Business Component Library/version 2/Layouts/Renderings/Common/Text) and click Add to add the HeaderTitle, HeaderSubtitle, and ValueNotInListText item in the Id fields of each renderings property.

  12. For the three items, in the Properties section, set the following ID properties:

    • IsVisibleFalse

    • PlaceholderKeyPage.Body

    Note

    These items are used as texts that set the action editor dialog title, subtitle, and the not found value. If you fill in the text property here, the texts will be visible in all languages but will not be localizable.

  13. Add the following renderings:

    • MainBorder of type Border (/sitecore/client/Business Component Library/version 2/Layouts/Renderings/Containers/Border).

    • MapContactForm of type Form (/sitecore/client/Business Component Library/version 2/Layouts/Renderings/Forms/Form). Set the FieldsLayout property to 1-1-1-1 and set the PlaceholderKey property to MainBorder.Content.

    Your rendering list now looks like this:

    Renderings and Placeholders

Add a folder that contains parameters for the editor

Next, you must add a folder that contains the parameters for the editor. To add the PageSettings folder:

  1. In the core database, navigate to /sitecore/client/Applications/FormsBuilder/Components/Layouts/Actions, and right-click the UpdateContact item you created earlier, click Add, and click New item.

  2. Search for and select the PageSettings template (/sitecore/client/Speak/Templates/Pages/PageSettings), enter the name PageSettings and click OK.

  3. Right-click the PageSettings item that you just created and click Add, New Item.

  4. Select the /sitecore/client/Business Component Library/version 2/Layouts/Renderings/Common/Text/Text Parameters template and click Add three times and name the items exactly the same as in the IDs in the layout you created previously:

    • HeaderTitle – double-click and in the Text field enter: Map form fields to contact details.

    • HeaderSubtitle – double-click and in the Text field enter: Map the fields in the form to the contact details that you want to update.

    • ValueNotInListText – double-click and in the Text field enter: value not in selection list.

  5. Navigate to /sitecore/client/Applications/FormsBuilder/Components/Layouts/Actions/UpdateContact and right-click the PageSettings item that you just created.

  6. Click New Folder and name it MapContactForm.

  7. Click the MapContactForm folder and add three FormDropList Parameters templates (/sitecore/client/Business Component Library/version 2/Layouts/Renderings/Forms/Form/Templates/FormDropList Parameters) with the following field values:

    FormDropList Parameter

    ValueFieldName

    DisplayFieldName

    FormLabel

    BindingConfiguration

    FirstName

    itemId

    name

    First name

    firstNameFieldId/SelectedValue

    LastName

    itemId

    name

    Last name

    lastNameFieldId/SelectedValue

    Email

    itemId

    name

    Email

    emailFieldId/SelectedValue

  8. Right-click the UpdateContact item, then click Tasks > Design Layout to navigate to the UpdateContact layout. Set the Form rendering ConfigurationItem property to the ID of the MapContactForm folder that contains the FormDropList parameters.

  9. Navigate to /sitecore/client/Applications/FormsBuilder/Components/Layouts/Actions/UpdateContact and right-click the PageSettings item that you created earlier.

  10. Add a new item named Stylesheet of type Page-Stylesheet-File (/sitecore/client/Speak/Templates/Pages/Page-Stylesheet-File):

    Stylesheet
  11. Click the new stylesheet item and set the Stylesheet value to: /sitecore/shell/client/Applications/FormsBuilder/Layouts/Actions/Actions.css

Create the client script for the editor

Now you must create the client script for the editor. In a previous step, when you created the UpdateContact item, you set the path to the script as follows:

RequestResponse
/sitecore/shell/client/Applications/FormsBuilder/Layouts/Actions/UpdateContact.js

To create the script:

  1. Use the base Submit actions editor script. The Submit actions editor script always has the following base:

    RequestResponse
    (function (speak) {
        var parentApp = window.parent.Sitecore.Speak.app.findApplication('EditActionSubAppRenderer');
     
        speak.pageCode(["underscore"],
            function (_) {
                return {
                    initialized: function () {
                        this.on({
                            "loaded": this.loadDone
                        },
                            this);
     
                        if (parentApp) {
                            parentApp.loadDone(this, this.HeaderTitle.Text, this.HeaderSubtitle.Text);
                            parentApp.setSelectability(this, true);
                        }
                    },
     
                    loadDone: function (parameters) {
                        this.Parameters = parameters || {};
                    },
     
                    getData: function () {
                        return this.Parameters;
                    }
                };
            });
    })(Sitecore.Speak);
  2. Use the EditActionSubAppRenderer component. The editors are loaded in a frame in a Speak dialog by the EditActionSubAppRenderer component. They must pass the dialog header title and subtitle to the parent, and set when the submit button is enabled.

    In this example, to create a submit action that updates contact details, you use a script that finds the canvas component FormDesignBoard, gets the data from the fields on the form canvas and transforms them to a simple array: empty item, followed by items with itemId and name properties. This is why, in the FormDropList Parameters items, you filled in the ValueFieldName and DisplayFieldName fields with itemId and name. The script works as follows:

    • initialized – collects the data from the fields into an array. Then it finds all form drop lists and sets their IsSelectionRequired property to false

    • loadDone – iterates the form controls, and sets their dynamic data to the fields array. If the current submit action Parameters property value is not in the fields list (for example, if the field is deleted, or the form copied), it includes an id - value not in the selection list item in the array. Then it binds the SPEAK form to the Parameters object.

    • getData – when the submit button is clicked, the getData function is called. It iterates the form data to collect the new Parameters object. Empty selections (field mappings) are omitted.

    Your final script should look like this:

    RequestResponse
    (function (speak) {
        var parentApp = window.parent.Sitecore.Speak.app.findApplication('EditActionSubAppRenderer'),
            designBoardApp = window.parent.Sitecore.Speak.app.findComponent('FormDesignBoard');
    
        var getFields = function () {
            var fields = designBoardApp.getFieldsData();
    
            return _.reduce(fields,
                function (memo, item) {
                    if (item && item.model && item.model.hasOwnProperty("value")) {
                        memo.push({
                            itemId: item.itemId,
                            name: item.model.name
                        });
                    }
                    return memo;
                },
                [
                    {
                        itemId: '',
                        name: ''
                    }
                ],
                this);
        };
    
        speak.pageCode(["underscore"],
            function (_) {
                return {
                    initialized: function () {
                        this.on({
                            "loaded": this.loadDone
                        },
                            this);
    
                        this.Fields = getFields();
    
                        this.MapContactForm.children.forEach(function (control) {
                            if (control.deps && control.deps.indexOf("bclSelection") !== -1) {
                                control.IsSelectionRequired = false;
                            }
                        });
    
                        if (parentApp) {
                            parentApp.loadDone(this, this.HeaderTitle.Text, this.HeaderSubtitle.Text);
                            parentApp.setSelectability(this, true);
                        }
                    },
    
                    setDynamicData: function (propKey) {
                        var componentName = this.MapContactForm.bindingConfigObject[propKey].split(".")[0];
                        var component = this.MapContactForm[componentName];
    
                        var items = this.Fields.slice(0);
    
                        if (this.Parameters[propKey] &&
                            !_.findWhere(items, { itemId: this.Parameters[propKey] })) {
                            var currentField = {
                                itemId: this.Parameters[propKey],
                                name: this.Parameters[propKey] +
                                    " - " +
                                    (this.ValueNotInListText.Text || "value not in the selection list")
                            };
    
                            items.splice(1, 0, currentField);
    
                            component.DynamicData = items;
                            $(component.el).find('option').eq(1).css("font-style", "italic");
                        } else {
                            component.DynamicData = items;
                        }
                    },
    
                    loadDone: function (parameters) {
                        this.Parameters = parameters || {};
                        _.keys(this.MapContactForm.bindingConfigObject).forEach(this.setDynamicData.bind(this));
                        this.MapContactForm.BindingTarget = this.Parameters;
                    },
    
                    getData: function () {
                        var formData = this.MapContactForm.getFormData(),
                            keys = _.keys(formData);
    
                        keys.forEach(function (propKey) {
                            if (formData[propKey] == null || formData[propKey].length === 0) {
                                if (this.Parameters.hasOwnProperty(propKey)) {
                                    delete this.Parameters[propKey];
                                }
                            } else {
                                this.Parameters[propKey] = formData[propKey];
                            }
                        }.bind(this));
    
                        return this.Parameters;
                    }
                };
            });
    })(Sitecore.Speak);

Create a submit action item

To create the submit action item:

  1. In the Master database, navigate to /sitecore/system/Settings/Forms/Submit Actions

  2. Right-click Submit Actions, click Insert, and click Insert from template.

  3. Select the /System/Forms/Submit Action template, in the Item Name field, enter the name Update Contact Details and click Insert.

  4. Navigate to the item you just created and in the Settings section, in the Model Type field, set the value to the class type name. For example, Sitecore.ExperienceForms.Samples.SubmitActions.UpdateContact.

  5. In the Error Message field, enter an error message, for example, Update contact failed!

  6. In the Editor field, select the editor that you just created, for example, Update contact.

  7. In the Appearance section, select the icon that you want to display in the Form elements pane.

In the Form elements pane, when you click Add a submit action, you can now select the Update Contact Details action.

Update contact action in Forms elements pane

Do you have some feedback for us?

If you have suggestions for improving this article,