London Dev Meetup Rescheduled! Due to unavoidable reasons, the event has been moved to 21st May. Speakers remain the same—any changes will be communicated. Seats are limited—register here to secure your spot!

Antti Alasvuo
Jun 18, 2022
  3118
(1 votes)

Custom placeholders in Optimizely Forms submission emails

In this post I'll show you how you can easily create your own custom placeholders and use those in Optimizely Forms.

The idea for this blog post came from the requirement of a customer project where they wanted to have the Forms submit timestamp in an email (email sent when a user submits a form).

By default only the fields editor has added to the Forms form are available in the placeholders (summary field is a special placeholder) but when a form is submitted by the user there are also the system columns like:

  • hosted page, from what page the form was submitted
  • timestamp, when the form was submitted
  • language, language of the forms FormContainerBlock
  • user, username of th euser if the user was logged in

So in this customer case we already have the forms submitted timestamp and would be ideal to just be able to use that instead of starting to create a completely custom email sending functionality, just to be able to add a timestamp to the email. Optimizely Plaholder API to the rescue ;-) ...yes yes it is still in beta, but it has been that for a long time (Improve PlaceHolder API (BETA)).

Custom placeholder provider

Implementing a custom placeholder provider is as easy as creating a class that implements the interface EPiServer.Forms.Core.Internal.IPlaceHolderProvider from EPiServer.Forms.Core.dll (EPiServer.Forms.Core NuGet package).

The interface defines two properties and one method:

  • int Order { get; set; }
    • Priority of the PlaceHolderProvider to process AvailablePlaceHolders
  • IEnumerable<PlaceHolder> ExtraPlaceHolders { get; }
    • Custom PlaceHolders will be merged into AvailablePlaceholders
  • IEnumerable<PlaceHolder> ProcessPlaceHolders(IEnumerable<PlaceHolder> availablePlaceHolders, FormIdentity formIden, HttpRequestBase requestBase = null, Submission submissionData = null, bool performHtmlEncode = true);
    • Called when "replacing" the placeholders with the actual data

Define your custom placeholders

In the ExtraPlaceHolders property we define the custom placeholders this provider supports:

// Note the placeholder key value is what is displayed in placeholder dropdown

/// <summary>
/// Form submitted placeholder.
/// </summary>
private const string FormSubmittedTimestamp = "Form submitted";

/// <summary>
/// Form submitted by user placeholder.
/// </summary>
private const string FormSubmittedBy = "Form submitted by";

/// <summary>
/// Form submitted from page placeholder.
/// </summary>
private const string FormSubmittedFromPage = "Form submit page";

public IEnumerable<PlaceHolder> ExtraPlaceHolders => new PlaceHolder[] {
    new PlaceHolder(FormSubmittedTimestamp, null),
    new PlaceHolder(FormSubmittedBy, null),
    new PlaceHolder(FormSubmittedFromPage, null)
};

The PlaceHolder object takes two arguments:

  • First the "Key" which is the placeholder string
  • Second is the value

So here three placeholder strings are created and "null" is used for the value for all of them.

Process custom placeholders

The replacement of the placholders are done in the ProcessPlaceHolders method. Extension methods used in the code are available in GitHub Gist.

public IEnumerable<PlaceHolder> ProcessPlaceHolders(IEnumerable<PlaceHolder> availablePlaceHolders, FormIdentity formIden, HttpRequestBase requestBase = null, Submission submissionData = null, bool performHtmlEncode = true)
{
    if (availablePlaceHolders == null)
    {
        // the DefaultPlaceHolderProvider throw null exception too if the availablePlaceHolders is null
        // the interface documentation doesn't say anything about exceptions (which ones should be thrown)
        // so do it the same way
        throw new ArgumentNullException(nameof(availablePlaceHolders));
    }

    // get the submission data
    var data = submissionData?.Data;

    // if we don't have data, then do nothing
    if (data == null)
    {
        return availablePlaceHolders;
    }

    //
    // NOTE! Extension methods used in this code can be seen in gist:
    // https://gist.github.com/alasvant/b114f32c0f991efbbe0a628a6fe6ddee
    //

    foreach (var ph in availablePlaceHolders)
    {
        if (FormSubmittedTimestamp.Equals(ph.Key, StringComparison.Ordinal))
        {
            // get the submitted timestamp
            if (data.TryGetSubmitTime(out DateTime submitted))
            {
                data.TryGetLanguage(out string languageCode);

                try
                {
                    // the timestamp is DateTimeKind.Utc, format the timestamp using
                    // the forms culture or the default culture
                    ph.Value = $"{submitted.ToString(GetCultureInfo(languageCode))} UTC";
                }
                catch {}
            }
        }
        else if(FormSubmittedBy.Equals(ph.Key, StringComparison.Ordinal))
        {
            // get the submitted by user
            if (data.TryGetSubmitUser(out string username))
            {
                ph.Value = string.IsNullOrWhiteSpace(username) ? AnonymousUsername : username;
            }
            else
            {
                ph.Value = AnonymousUsername;
            }
        }
        else if (FormSubmittedFromPage.Equals(ph.Key, StringComparison.Ordinal))
        {
            // get the form hosted page
            if (data.TryGetHostedPage(out ContentReference hostedPage))
            {
                LoaderOptions loadingOptions;

                // try to get the language code
                if (data.TryGetLanguage(out string languageCode))
                {
                    loadingOptions = new LoaderOptions { LanguageLoaderOption.FallbackWithMaster(GetCultureInfo(languageCode)) };
                }
                else
                {
                    loadingOptions = new LoaderOptions { LanguageLoaderOption.MasterLanguage() };
                }

                // try to load the content in the forms language and fallback to masterlanguage
                if (_contentLoader.Service.TryGet(hostedPage, loadingOptions, out PageData page))
                {
                    // and then resolve the url using the language the content was actually loaded in
                    var pageUrl = _urlResolver.Service.GetUrl(page);
                    ph.Value = $"{page.Name}, {pageUrl}";
                }
            }
        }
    }

    return availablePlaceHolders;
}

Custom placeholders in action

Now the editor can use the custom placeholders in the email template.

And then in the email we can see the values like this:

  • Form submitted, is formatted using the culture of the submitted form, in these samples in Finnish, Swedish and English
  • Form submitted by, anonymous or the logged in user
  • Form submitted from page, contains page name in the correct culture and the url of the page

Full source code in GitHub gists

Jun 18, 2022

Comments

Please login to comment.
Latest blogs
Optimizely Product Recommendation Troubleshooting

In today’s fast-paced digital landscape, personalization is everything . Customers expect relevant, tailored experiences whenever they interact wit...

Sanjay Kumar | Apr 28, 2025

Natural Language Q&A in Optimizely CMS Using Azure OpenAI and AI Search

In Part 2, we integrated Azure AI Search with Azure Personalizer to build a smarter, user-focused experience in Optimizely CMS. We used ServiceAPI ...

Naveed Ul-Haq | Apr 25, 2025 |

Identifying Spike Requests and Issues in Application Insights

Sometimes within the DXP we see specific Azure App Instances having request spikes causing performance degredation and we need to investigate. I fi...

Scott Reed | Apr 25, 2025

Optimizely Frontend Hosting Beta – Early Thoughts and Key Questions

Optimizely has opened the waitlist for its new Frontend Hosting capability. I’m part of the beta programme, but my invite isn’t due until May, whil...

Minesh Shah (Netcel) | Apr 23, 2025