Sitecore Forms NextJS – Create a dynamic dropdown with parameters

Friends, world-citizens and sitecorians! Christmas is almost upon us, best holiday of the year ๐ŸŽ…๐Ÿ˜Š

If you don’t know what to do during your Christmas holidays, why not check out all the lovely videos from SUGCON North America 2023.

Today’s post will be about Sitecore Forms and how to implement a custom dropdown in a Sitecore Headless setup (NextJS).

The scenario is this:
We want to have a custom dropdown control in Sitecore Forms. The dropdown will call an external API (Dynamics) and populate a dropdown. We also need to set parameters for the API call, meaning we need to have extra input fields for the editors when adding the dropdown.

Let’s divide the work into a Sitecore part and a NextJs part ๐Ÿ™‚

First up is the Sitecore part. We will create a dropdown list in Sitecore, for this we need to some work in both Core and Master.

And of course, someone has totally nailed it! Let’s give a massive shout-out to Joรฃo A. Neto and his awesome blog post – Create a dropdown list with external data in Sitecore Forms. This is exactly what we need!

Sitecore also has some really good documentation on how to create a custom control in Sitecore Forms – Walkthrough: Creating a custom form element.

Let’s combine the two!

Before we continue, I would like to say a few words about Sitecore Rocks. For me, it has been a good friend during the “SPEAK era” but now I feel it has outplayed its role. It is also quite evident if you look at when the last update was done…

*https://marketplace.visualstudio.com/items?itemName=JakobChristensen.SitecoreRocks

Based on that, I have decided not to use Sitecore Rocks at all.

While Sitecore Rocks was a popular extension for Sitecore development, there are alternative options available that might suit your needs better.

One such alternative is the Sitecore for Visual Studio, which is a Visual Studio extension specifically designed for Sitecore Content Serialization.

Another option to consider is the Sitecore Command Line Interface (CLI), which allows you to interact with Sitecore from the command line.

Okay sitecorians let’s continue ๐Ÿ™‚

Here is how the control will look like when we are finished.

First up is to create a new field type and we will follow Joรฃo’s great instructions.
In the Content Editor, navigate to /sitecore/system/Settings/Forms/Field Types/Lists and create a new field type (template: /sitecore/templates/System/Forms/Field Type).
*We will call it DynamicsDropdownList.

Since we need to set parameters for the API call, we will have to add “custom” inputs so that the editors can “query” and get a result (populated dropdown). Before we create the custom inputs, we will make “value fields” (they will contain the values from what the editors have “selected”).
*They are all plain Single-Line Text fields

*/sitecore/templates/System/Forms/Fields/DynamicsDropdownList

To customize our inputs, we will create our own Property Editor.
*This will allow us to have control over the appearance and behavior of our custom inputs.

Let’s head to Core and PropertyGridForm
and navigate to /sitecore/client/Applications/FormsBuilder/Components/Layouts/PropertyGridForm.
Locate Property Editor Settings (Settings) and make a copy of DropDownList and call it DynamicsDropdownList. We will remove stuff we don’t need, like ListItems and Conditions.
It’s time to create the custom inputs!
When selecting params for the API call, I want to have dropdowns for the editors. So how do we do that? Well, Sitecore has already solved it for us with Button and NavigationStep ๐Ÿ™‚

Great! Now we just have to copy NavigationStep. The top 3 fields will be dropdown fields, the rest will be read-only fields.

Let’s explore the functionality of a dropdown field called CountrySelection, which allows users to select a country from a list. In this scenario, the database will be set as the master, containing all the available countries.

We need to know in which folder the countries are located by providing an item ID.

*Here is the folder with countries in master (The country item contains two fields, Text and Value).

In the Form Section, we will set some basic stuff like a label and make the input required. Here we also have the BindingConfiguration. This will map the selected value from the “countries dropdown” to the dynamicsCountry property. (If you recall, we created a bunch of field templates for the DynamicsDropdownList in master)

Let’s examine a read-only field called ApiBaseURLValue. These fields serve as informational references for the editors. However, they are also utilized in code when we execute the API call. The field follows the format of the template FormTextBox Parameters.

In the Form Section, we will set the BindingConfiguration. This will map the value from the read-only input to the dynamicsApiBaseURL property.

We don’t want the editors to change the values in the “read-only” fields. In the Behavior Section we will set IsReadOnly to true:

We have one last thing to do with DynamicsDropdownList in Core. We need to add our custom inputs to the Details item.

We’re all set in Core! Now let’s go back to Master again, folks! Did you notice? We didn’t even touch Sitecore Rocks at all! ๐Ÿ˜‰

In master, we will continue with the field type DynamicsDropdownList (/sitecore/system/Settings/Forms/Field Types/Lists/Dynamics Dropdown list).
In the “Property Editor field,” we will select our newly created Property Editor – DynamicsDropdownList:

For the Field Template, we set the created template – /sitecore/templates/System/Forms/Fields/DynamicsDropdownList.

It can be confusing with all the DynamicsDropdownLists ๐Ÿ™‚
See it like this, we have created a new field type DynamicsDropdownList and it is using the template DynamicsDropdownList.

We have one more thing to do and that is to set the Model Type:

Let’s dive into the code ๐Ÿ™‚

Here is Sandbox.Foundation.Forms.Models.DynamicsDropdownListViewModel:

using Sandbox.Foundation.Dynamics.Services;
using Sandbox.Foundation.Forms.Managers;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ExperienceForms.FieldSettings;
using Sitecore.ExperienceForms.Mvc.Models;
using Sitecore.ExperienceForms.Mvc.Models.Fields;
using System;
using System.Web.Mvc;
using static Sandbox.Foundation.Dynamics.Templates;

namespace Sandbox.Foundation.Forms.Models
{
    [Serializable]
    public class DynamicsDropdownListViewModel : DropDownListViewModel
    {
        private IDynamicsLookUpSettingsService _dynamicsLookUpSettingsService;

        protected override IFieldSettingsManager<ListFieldItemCollection> DataSourceSettingsManager => new DynamicsSettingsManager();

        
        public DynamicsDropdownListViewModel()
        {
            _dynamicsLookUpSettingsService = DependencyResolver.Current.GetService<IDynamicsLookUpSettingsService>();
            _dynamicsLookUpSettingsService.UpdateDynamicsLookUpSettingsFromBaseSettingsItem();
        }

        public string  DynamicsEntity
        {
            get; 
            set;
        }

        public string DynamicsCountry
        {
            get;
            set;
        }

        public string DynamicsImporter
        {
            get;
            set;
        }

        public string DynamicsAuthorizationCode => _dynamicsLookUpSettingsService.DynamicsLookUpSettings.AuthorizationCodeValue;
        

        public string DynamicsApiBaseURL => _dynamicsLookUpSettingsService.DynamicsLookUpSettings.BaseUrl;
        

        public string DynamicsApiMethod => _dynamicsLookUpSettingsService.DynamicsLookUpSettings.ApiMethod;
        

        public string DynamicsApiTimeOut => _dynamicsLookUpSettingsService.DynamicsLookUpSettings.Timeout.TotalSeconds.ToString();



        protected override void InitItemProperties(Item item)
        {
            Assert.ArgumentNotNull((object)item, nameof(item));
            base.InitItemProperties(item);
            DynamicsEntity = item.Fields[DynamicsLookUpFormSettingsFields.DynamicsEntity]?.Value;
            DynamicsCountry = item.Fields[DynamicsLookUpFormSettingsFields.DynamicsCountry]?.Value;
            DynamicsImporter = item.Fields[DynamicsLookUpFormSettingsFields.DynamicsImporter]?.Value;
        }

        override protected void UpdateItemFields(Item item)
        {
            Assert.ArgumentNotNull((object)item, nameof(item));
            base.UpdateItemFields(item);

            item.Fields[DynamicsLookUpFormSettingsFields.DynamicsEntity]?.SetValue(DynamicsEntity, false);
            item.Fields[DynamicsLookUpFormSettingsFields.DynamicsCountry]?.SetValue(DynamicsCountry, false);
            item.Fields[DynamicsLookUpFormSettingsFields.DynamicsImporter]?.SetValue(DynamicsImporter, false);
        }
    }
}

The interesting part is the properties and the protected override IFieldSettingsManager DataSourceSettingsManager => new DynamicsSettingsManager();

The properties are the “field properties” that are mapped in the BindingConfiguration (in Core). Notice how to set the read-only fields:
public string DynamicsApiBaseURL => _dynamicsLookUpSettingsService.DynamicsLookUpSettings.BaseUrl;

The IFieldSettingsManager DataSourceSettingsManager => new DynamicsSettingsManager() is an excellent piece of code. Inspired by the insightful post by Joรฃo A. Neto, we will incorporate his code into our implementation and enhance it by adding our own code. Let’s take a closer look at the DynamicsSettingsManager class:

using Sandbox.Foundation.Dynamics.Services;
using Sitecore.Common;
using Sitecore.Data.Items;
using Sitecore.ExperienceForms.FieldSettings;
using Sitecore.ExperienceForms.Mvc.Models;
using System;
using System.Web.Mvc;


namespace Sandbox.Foundation.Forms.Managers
{
    public class DynamicsSettingsManager : IFieldSettingsManager<ListFieldItemCollection>
    {
        private readonly IDynamicsLookUpService _dynamicsLookUpService;
        private readonly IDynamicsLookUpSettingsService _dynamicsLookUpSettingsService;

        public DynamicsSettingsManager()
        {
            _dynamicsLookUpService = DependencyResolver.Current.GetService<IDynamicsLookUpService>();
            _dynamicsLookUpSettingsService = DependencyResolver.Current.GetService<IDynamicsLookUpSettingsService>();
        }


        public ListFieldItemCollection GetSettings(Item fieldItem)
        {
            try
            {
                return GetDynamicsItems(fieldItem);
            }
            catch (Exception ex)
            {
                Sitecore.Diagnostics.Log.Error("DynamicsSettingsManager: Error getting Dynamics list values", ex, this);
                return new ListFieldItemCollection();
            }
        }

        public void SaveSettings(Item fieldItem, ListFieldItemCollection settings)
        {
            // This method is supposed to create items in the content tree from the specified datasource. We don't need it.
        }

        protected ListFieldItemCollection GetDynamicsItems(Item fieldItem)
        {

            var items = new ListFieldItemCollection();

            try
            {

                items.Add(new ListFieldItem { Value = "", Text = "" });

                _dynamicsLookUpSettingsService.UpdateDynamicsLookUpSettingsFromFormsSettingsItem(fieldItem);


                var response = _dynamicsLookUpService.GetLookUpValues<Dynamics.Models.DynamicsLookUpModelResponse>();

                response .ForEach(model =>
                {
                    items.Add(new ListFieldItem { Value = model.Id, Text = model.Name });
                });

            }
            catch (Exception ex)
            {

                Sitecore.Diagnostics.Log.Error("Error: GetDynamicsItems", ex, this);
            }

            return items;
        }


    }
}

* GetDynamicsItems will do the API call and return the list items

We are now ready to use our new component, Dynamics Dropdown list, in Sitecore Forms:

Success! And what’s even more impressive is that the editors now have the power to modify the parameter inputs. This means that the dropdown is genuinely dynamic and can adapt to their needs!

But hey, we are not ready yet! We have the NextJs part left.

In our frontend repo, we will navigate to the component JssNextForm.tsx.
If you want to know how to implement Sitecore Forms in your Next.js app, take a look at the great documentation from Sitecore – Implement a Sitecore form in a JSS Next.js app

Here we will add the new dropdown list control – DynamicsDropdownType. Notice the GUID; it’s the item ID of the field type DynamicsDropdownList we created in Sitecore.

const defaultFieldFactory = createDefaultFieldFactory();
const DynamicsDropdownType = '{C839725C-609C-4AC5-B3A4-6764B303F298}';

defaultFieldFactory.setComponent(
  DynamicsDropdownType as FieldTypes,
  (props: ListFieldProps<DropdownListViewModel>) => {
    const { field, onChange } = props;

    function handleOnChange(
      field: ValueFormField,
      newValue: string,
      callback: FieldChangeCallback
    ) {
      let valid = true;
      const errorMessages = [];

      // custom client validation logic here
      if (field?.model?.required && !newValue) {
        valid = false;
        errorMessages.push(`${field.model.title} is required`);
      }

      callback(field?.valueField?.name, [newValue], valid, errorMessages);
    }

    return (
      <>
        <Label {...props}></Label>

        <select
          className={field?.model?.cssClass}
          name={field?.valueField?.name}
          id={field?.valueField?.id}
          onChange={(e) => handleOnChange(props.field, e.target.value, onChange)}
        >
          {field?.model?.items?.map((item: ListFieldItem, index: number) => (
            <option key={index} value={item.value}>
              {item.text}
            </option>
          ))}
        </select>

        <FieldErrorComponent {...props}></FieldErrorComponent>
      </>
    );
  }
);

Notice the handleOnChange function. I was totally puzzled by this problem. I couldn’t understand why the selected value from the dropdown wasn’t getting posted. It completely slipped my mind that we need to set the onChange event on the control.

A huge shout-out to my fellow frontend man, Ludvig Andersson, for assisting and making the Next.js code readable. ๐Ÿ˜Ž

Here is the end result on a “live” site:

Well, look at that, I just finished crafting this long and oh-so-interesting post. Did I manage to keep you entertained or did I put you to sleep? Don’t worry, I won’t judge your snoring skills. Now, jokes aside, if you have any burning questions or hilarious comments, feel free to drop them below. Let’s keep the fun going!

Thatโ€™s all for now folks ๐Ÿ˜Š


Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.