SVDD: Using Forms in Sitecore 9 and xDB Together

vsugconf feature image

We just wrapped up the first annual Sitecore Virtual Developer Day. I am really happy to have had the opportunity to participate! My topic was on using the new Sitecore Forms available now in Sitecore 9 and identifying contacts through it as well as triggering goals which can then translate to performing personalization on a component once someone fills out a form.

During this session, I walked the viewers through a made up use case that a Product Owner might tell a developer or write as a story in the backlog.

slide5.png

When you boil down what is really being asked of, it comes down to three overarching asks.

slide7

Behind the Scenes

During portions of the video and my demo, I mention that I will post larger snippets of code on my blog for later viewing.

Creating a Custom Submit Action

When creating a custom submit action there are three things that have to be created:

  1. The Submit Action Item

slide12.png

2. The Submit Action Code Behind

slide13

3. The Editor for additional information. This step is optional if you don’t need to map fields from the form to flow through to your submit action.

slide16

slide17

Identifying Contact From Form Submit Action Code Behind

The following code is used to create the submit action that can be called up from Sitecore Forms and used to identify a contact.

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 SitecoreHacker.Feature.ExperienceForms.SubmitActions
{
    public class TriggerIdentifyContact : SubmitActionBase
    {
        ///
<summary>
        ///     Initializes a new instance of the  class.
        /// </summary>

        /// The submit action data.
        public TriggerIdentifyContact(ISubmitActionData submitActionData) : base(submitActionData)
        {
        }

        ///
<summary>
        ///     Gets the current tracker.
        /// </summary>

        protected virtual ITracker CurrentTracker =&gt; Tracker.Current;

        public override void ExecuteAction(FormSubmitContext formSubmitContext, string parameters)
        {
            Assert.ArgumentNotNull(formSubmitContext, "formSubmitContext");
            UpdateContactData target;
            if (TryParse(parameters, out target))
                try
                {
                    if (Execute(target, formSubmitContext))
                        return;
                }
                catch (ArgumentNullException)
                {
                }
            formSubmitContext.Errors.Add(new FormActionError
            {
                ErrorMessage = SubmitActionData.ErrorMessage
            });
            formSubmitContext.Abort();
        }

        ///
<summary>
        ///     Executes the action with the specified .
        /// </summary>

        /// The data.
        /// The form submit context.
        /// true if the action is executed correctly; otherwise false
        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 &amp;&amp; lastNameField == null &amp;&amp; emailField == null)
                return false;

            using (var client = CreateClient())
            {
                try
                {
                    var emailIdent = new IdentifiedContactReference("IdentifiedEmail", GetValue(emailField));

                    //Identify with Email
                    CurrentTracker.Session.IdentifyAs(emailIdent.Source, emailIdent.Identifier);

                    var expandOptions = new ContactExpandOptions(
                        CollectionModel.FacetKeys.PersonalInformation,
                        CollectionModel.FacetKeys.EmailAddressList);
                    var contact = client.Get(emailIdent, expandOptions);

                    //Set Fields
                    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>

        /// The  instance.
        protected virtual IXdbContext CreateClient()
        {
            return SitecoreXConnectClientConfiguration.GetClient();
        }

        ///
<summary>
        ///     Gets the field by .
        /// </summary>

        /// The identifier.
        /// The fields.
        /// The field with the specified .
        private static IViewModel GetFieldById(Guid id, IList fields)
        {
            return fields.FirstOrDefault(f =&gt; Guid.Parse(f.ItemId) == id);
        }

        ///
<summary>
        ///     Gets the  value.
        /// </summary>

        /// The field.
        /// The field value.
        private static string GetValue(object field)
        {
            return field?.GetType().GetProperty("Value")?.GetValue(field, null)?.ToString() ?? string.Empty;
        }

        ///
<summary>
        ///     Sets the  facet of the specified .
        /// </summary>

        /// The first name.
        /// The last name.
        /// The contact.
        /// The client.
        private static void SetPersonalInformation(string firstName, string lastName, Contact contact,
            IXdbContext client)
        {
            if (string.IsNullOrEmpty(firstName) &amp;&amp; string.IsNullOrEmpty(lastName))
                return;
            var personalInfoFacet = contact.Personal() ?? new PersonalInformation();
            if (personalInfoFacet.FirstName == firstName &amp;&amp; personalInfoFacet.LastName == lastName)
                return;
            personalInfoFacet.FirstName = firstName;
            personalInfoFacet.LastName = lastName;
            client.SetPersonal(contact, personalInfoFacet);
        }

        ///
<summary>
        ///     Sets the  facet of the specified .
        /// </summary>

        /// The email address.
        /// The contact.
        /// The client.
        private static void SetEmail(string email, Contact contact, IXdbContext client)
        {
            if (string.IsNullOrEmpty(email))
                return;
            var 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);
        }
    }

    public class UpdateContactData
    {
        public Guid FirstNameFieldId { get; set; }
        public Guid LastNameFieldId { get; set; }
        public Guid EmailFieldId { get; set; }
    }
}

 Update Contact SPEAK Editor Javascript

During the live demo, I gloss over the fact that there’s a Javascript file that needs to be created to support some of the SPEAK functionality. This file will live in /sitecore/shell/client/Applications/FormsBuilder/Layouts/Actions/UpdateContact.js

(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 &amp;&amp; item.model &amp;&amp; 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 &amp;&amp; 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] &amp;&amp;
                        !_.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);<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>

The whole point of this JavaScript is to provide the backing that will take the mapping of fields from the Form, to the mapping of Contact fields in the Editor. The output of this form lies in the Submit Action on the form, in the Parameters field as a JSON object.

params

Be aware: There is a problem with this JavaScript. It is not my strong suit. If anyone can figure out for me what the problem is (trust me, if you follow all my instructions, you’ll see what I mean) please let me know. (It doesn’t seem to map the first element. I think there’s an index issue somewhere). That said once you understand the values being assigned to the parameters, this can also be manually created.

Summary

I hope that you enjoyed watching this session as much as I enjoyed making it. Please feel free to reach out to me if you have any questions. You can find me at any of the community outlets.

slide33

2 thoughts on “SVDD: Using Forms in Sitecore 9 and xDB Together

  1. Hi Pete, which Sitecore 9 release are you using, Initial or Update-1? Also, which version of the Sitecore Forms are you using, 9.0.171219 or 9.0.171002?

    Like

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s