Salesforce Commerce Cloud - React powered Custom Attribute Editors
Salesforce Commerce Cloud offers a lot of data types and corresponding UI controls for their custom attributes, but sometimes you need something different. Maybe a little extra validation or a particular input UI to really make the user experience great.
This is where the Custom Attribute Editor feature really shines. These custom editors gives you fine grain control over the UI input and behavior for a particular attribute, and even lets you save JSON as an attribute value.
It does this by allowing you to build a custom form with HTML/CSS/Javascript to interact with a particular attribute value via an iFrame embedded directly in the Page Designer interface. Normally these Custom Attribute Editors are plain HTML/CSS/JS forms however there are some scenarios when it may be beneficial to use a small React app to power these editor experiences.
We’re going to break this guide down into a few sections and tackle each task one at a time:
- Register a regular custom attribute editor
- Add our React app
- Expanding our app to support any number of editors
- Adding support for breakout editors
As this post is quite long you can find a table of contents listed to the side of the content.
Prerequisites
As usual this article expects you to have a basic understanding of web technologies and to have familiarly with the Salesforce Commerce Cloud platform at a high level an on-demand sandbox (or similar test environment) available with your IDE connected to it.
1.0 - Register a plain custom attribute editor
To get us started we are going to set up a regular (non-React) custom attribute editor as detailed in the documentation under Develop a Custom Attribute Editor. First thing we are going to do is set up a new cartridge to keep our custom attribute editors and any other to our business manager extensions we want to build.
1.1 - Create a new cartridge for our business manager extensions
This can be done using the Prophet debugger plugin for VSCode or by setting up the following scaffold file in the following directory structure:
app_custom_bm_extensions
└─> cartridge
└─> app_custom_bm_extensions.properties
Inside the app_custom_bm_extensions.properties
file we add the following:
## cartridge.properties for cartridge app_custom_bm
#Mon Apr 29 19:46:1 CEST 2023
demandware.cartridges.app_custom_bm_extensions.multipleLanguageStorefront=true
demandware.cartridges.app_custom_bm_extensions.id=app_custom_bm
One final step is to ensure this cartridge is added to your sites’ Business Manager cartridge path
(not the site one). You can do this by going through Administration > Sites > Manage Sites
and
under the heading “Business Manager Site” click the link that reads “Manage the Business Manager
site”.
Now add your cartridge ID, for our example that is app_custom_bm_extensions
to the cartridge path like you
would for the sites, separating with a :
.
1.2 - Adding a debug component
Now we have a new cartridge start setting up a Page Designer component to test our attribute editor with. We will add this new component to our existing pages to ensure the editor is working correctly before using it on a regular component. This is not a requirement a more personal preference to make development more reliable.
We’re going to need two new files one (JSON and the other JS) added to our cartridge which together make up our component
definition. I am going to call our new component Custom Attribute Editor
. First up lets create a
placeholder JS file and just add a comment for now:
Next up the component meta definition JSON file:
With those two files created we’ve registered a new component that is requesting the
com.acme.customEditor
custom editor. However, right now we’ve not registered it will appear as a
HTML textarea
element as a fallback when viewed in Page Designer:
In the next step we will register our custom attribute editor with the platform which will replace the fallback.
1.3 - Create our custom attribute editor definition files
Registering our custom editor requires two files, similar to the ones we created for our component,
a JS and a JSON file respectively, this time they need to be in a different location. Specifically
they need to be under experience/editors
then additionally under a folder structure similar to
that of the reverse domain string we used above for the editor type
so path would look like:
app_custom_bm_extensions/cartridge/experience/editors/com/acme/
First lets register our editor using a “Custom Attribute Editor Meta Definition File”. Here we add
some supplementary information but most importantly we are adding a reference to our
(yet to be created) customEditorClient.js
file.
With our meta definition file created lets move on to the Custom Attribute Editor script file. The
script file has the same name as the corresponding meta definition file but with a .js
extension.
In the script file, you can optionally implement the init function to initialize the custom
attribute editor with server-side logic or resources. For now, we’re just going to add a bit of
configuration to the editor:
1.4 - Custom editor client script
Our last step for Part 1 is to create the customEditorClient.js
file we added to the resources
of our custom attribute editor definition JSON file. This file is the one added to the iFrame our
custom attribute editor is presented within when viewed in Page Designer. This iFrame is also where
lifecycle events are emitted to Page Designer in order to communicate interactions and updates to
the values.
Note the change in file path here, this file needs to live in the static
directory, not the
experience
directories like our previous files.
Typically, this is where we add an HTML form (via JavaScript) to edit our attribute and report back. For now lets just use the basic scaffold to ensure everything is wired up:
1.5 - Talking to Page Designer
From the example above, we can see we’re subscribed to the sfcc:ready
event, and when that is
emitted the callback function is executed. This event is one of a series of “Custom Attribute Editor
Events”. This series of events are used to communicate with Page Designer and instruct it to carry
out certain actions on our behalf:
Event | Description |
---|---|
sfcc:interacted | Indicates that the user has interacted with the custom attribute editor. |
sfcc:valid | Indicates whether the value of the attribute is valid. Can include an error message. |
sfcc:value | The value of the attribute. Sent when the value changes inside the editor. |
You can find full information and examples for each of these in the SFCC documentation site for “Custom Attribute Editor Events”.
We can transmit these events to Page Designer by using the window.emit
function and will be using
them in a moment after we add our React app, however for now keep these in your back
pocket.
1.6 - Review
To quickly recap, we can now click on our Custom Attribute Editor
component we added to Page
Designer we should see our custom attribute editor and in the place of a textarea component we saw
earlier we should now see a <h1 />
saying Hello from customEditorClient.js
and in the browser
console we should see the"sfcc:ready"
message.
We should have the following files present in our app_custom_bm_extensions
cartridge in the
appropriate directories:
2.0 - Adding React to the mix
Now we have our plain custom attribute editor we can move on to replacing it with a React app! We will assume you’ve got a React app ready to go for this, if not check out the create-react-app tutorial.
2.1 - Required files
First step is to ensure we’re including the dist
or build output from our React app in our
cartridge code. For this example we’re going to upload the build using Prophet into a new directory
we’re creating in /static/default/
:
If you’re looking to keep the React app source code close to the cartridge code I would recommend using something like NX to manage your mono-repo.
I use NX and one of the things I’ve setup for my NX build is disabled the usual cache busting hashes present in the file names along with removing chunks. This leaves me with four JS files and one CSS for the whole of my React app:
vendor.js
polyfills.js
main.js
runtime.js
styles.css
Now we have our dist
files we can update the cusotmEditorClient.js
code (the file SFCC initially
loads into our iFrame that we added in step 1.4) to inject our React app. We can do this by
replacing the JavaScript with the following:
To recap, this file now sets up the React root element and then proceeds to dynamically insert the four script tags into the custom attribute editor iFrame body along with a stylesheet which are our required files for the React app. Our build files should be in the right places also:
Loading the Custom Attribute Editor now should look like the following if you’re using the quick-start React app or your own if you had a pre-built one:
Now we’ve got a React app embedded in Page Designer, now lets get them to talk to each other and save a value provided in the app to Page Designer.
2.2 - Basic SFCC events
Let’s add a button that saves the current date as a value to Page Designer and then
view that in the serialized page output. To my React app I am going to add a component that has a
simple button that when clicked, calls a handler function that calls the window.emit
we discussed
earlier in Step 1.5:
After clicking the “Test me” button you’ll notice the “Unpublish” button in the top right becomes greyed out signifying there is an unsaved change on the page.
Then after clicking save in the bottom right of the sidebar, I see the confirmation alert pop-in and the “Unpublish” button goes back to being enabled:
Viewing a JSON representation of the page via a controller
using PageMgr.serializePage()
we
can see the new value in the data:
Just like that we have a primitive custom attribute editor powered by a React app, which is awesome. But wait, we can do a few other things to make this more scalable and useful.
3.0 - One React App, many custom editors
As things stand right now we’ve registered a single custom editor called customEditor
, which we
can trigger by setting any component to use by setting the editor_definition
property like so:
However, as we’re injecting a React app here we can make this app multipurpose and use it for many inputs, saving us the hassle and challenge of setting up a separate editor declaration for every custom attribute editor experience we implement.
One way we can do this by using the configration
property we set to {}
earlier in our JSON:
This configuration
proper gets passed to our customEditorClient.js
as the config
parameter
passed to our in the sfcc:ready
callback from Step 1.4
above!
3.1 - Passing configuration into the app
Now we’ve got access to anything we’ve set on the component definition file we’ve got plenty of ways
we can pass this to the React app in the iFrame. One of the easiest ways and the way I like to do
this is to attach the configuration to a window
global.
We can do this in the customEditorClient.js
file by expanding what we added
in Step 1.4:
From here you can handle the React app however you see fit. The following part will showcase how I use React Router to handle multiple editors but if you want to skip ahead the next implementation agnostic section is Part 3.2 where we look at a “client” for the various Page Designer events we can support.
3.1 - Add React Router
Inside my App.jsx
file I set up a basic router to navigate to various routes based on the
config.editorType
property we set in the component definition JSON file. For example:
With the above component, we’re first checking we’ve got a editorType
property on the config and
redirecting to the page as required on initial load, we’re then deferring to our
<EditorComponent />
component to handle the rendering of the specific editor.
In the EditorComponent
we use a mapping pattern to dynamically map components to their
editorType
names:
Build and deploy to your sandbox, and we now have a way to dynamically change the React app that is rendered based on the value configured in the component definition JSON!
3.2 - SFCC Event Client
As we briefly mentioned above and in Part 1.5, there are a series
of Custom Attribute Editor Events we can use to communicate between Page Designer and our custom
attribute editor. Now we support any number of editors in a single application it is probably a good
idea to standardize the communication between the editor and Page Designer to avoid repeating the
various window.emit
calls we will be making.
To do this I like to build out an “SFCC Editor Client” of sorts. A simple JS library that facilitates communication between our React app and Page Designer. Into your React app copy the following JavaScript into a library/script directory:
You’ll see of the three different events we mentioned earlier that you can emit from an editor we’ve
combined them into a single handleChangeEvent
function that we can use in our components to
communicate all changes with. The rest of the file is more scaffolding and ensuring the
handleChangeEvent
does what we need it to.
We can emit a change event by calling handleChange
passing our current data or state, followed by
a boolean flag for the validity state that (in this example) defaults to true
, following by an
optional message to display in the UI. For example, we can refactor our previous simple example from
Step 2.2 to use the client like so:
4.0 - Adding support for breakout editors
A breakout editor is the SFCC term for an editor that appears in a Page Designer managed modal. The editor “breaks out” of the sidebar and takes over the entire screen. This is a great solution for complex user experiences where you need that additional screen real-estate to craft the data we’re storing in Page Designer.
Note: this is an optional addition to a custom attribute editor, you do not need to add support for breakout editors if it doesn’t work for your use case.
Before we dive in to how we can implement this I want to mention how the management of breakout editors work. First the editor in the sidebar (what I refer to as the inline editor) opens, from that editor, we emit a new event type requesting a breakout editor. Page Designer receives that event and handles the modal presentment. The communication between the breakout editor and page designer is routed through the sidebar editor.
Sequence Diagram
With that said lets get started adding support for breakout editors.
4.1 - Additional definition files to support breakout editors
Like we did for our custom editors back
in Step 1.3 we need to add some other
editor definition files. Let’s start with adding two new files to
our /cartridge/experience/editors/com/acme/
directory:
customEditorBreakout.js
customEditorBreakout.json
Let’s start by setting up the server-side JS file part of the definition with content that is identical to our previous attribute editor file:
And same again with the JSON file, again with identical content:
Note how we’re using the same customEditorClient.js
in both the customEditorBreakout.json
and
customEditor.json
files. This is deliberate as we want the same React app to be mounted in both
our “inline” and “breakout” editors. We need to make the following additions to our
original customEditor.js
file also:
With that the definition files are updated, last step is to update the Page Designer client we set up during Step 3.2 to ensure the breakout editor can work which we will look at next.
4.2 - Client Updates for Breakout editor
When adding breakout editor support we add support for three new event types we didn’t add earlier. These are:
Open
- Requests Page Designer to open the specified editor in a modalApply
- This closes the breakout editor and apply the changes made in the breakout editor to the inline editor.Cancel
- This closes the breakout editor and discard the changes made in the breakout editor.
Taking what we added to the file we set up in Step 3.2 we can expand our client to use these new breakout methods like so:
That was a lot of code but, we’ve added client functionality for our three new event types, and we’ve
added a new handleBreakoutRequest
method to abstract the behavior to make opening breakout editors
easier for any number of editors.
We use a localStorage
key to keep a reference to an open breakout editor, as in theory you could
have more than one Custom Attribute Editor active at any one time for a single component in Page
Designer, this ensures we can keep track of which editor should be presented in the breakout editor.
4.3 - Augmenting our React components to support breakout editors
With our client updated we can make some minor adjustments to our React components to trigger a breakout editor. As this changes the viewport of an application from a narrow sidebar to a larger modal I would recommend taking care when designing your components to maximise this additional screen space.
For my set-up I wrap my editors in a generic component (see below) with the definition being our
config object being passed from the component definition, predominantly being the editorId
, and as
a reminder the defintiion
we’re passing here is simply a reference to the window.acme
object we
set up back in Step 3.1.
Doing this allows me to have a standard set of I/O for my editors and easily trigger breakout editors and save events as required.
4.4 - Challenges using the same customEditorClient.js
for both editors
As we’re using the same customEditorClient.js
for both our inline and breakout editors there is an
unfortunate issue we need to work-around when it comes to the editorType
configuration. Typically,
we extract that property from the config
argument from the sfcc:ready
callback. For our inline
editor, this is defined and available, for breakout editors it is not provided!
We can use localStorage
as a workaround for this, below is a simplified example to explain how we
use localStorage
to get around this missing config
. When we trigger a breakout editor to open we
set the ID of the editor requesting the breakout to a localStorage
entry, which is then read
during the initial set up:
During the breakoutCloseCallback
we added as part of our client above, we then remove the
localStorage
key.
4.5 - A note about iFrame sandboxes
Your custom attribute editor will be added via an iFrame that has both the allow-scripts
and the
allow-same-origin
sandbox attributes. Without the allow-forms
using forms funnily enough doesn’t
work, even things like react-hook-form
’s example makes use of the <form onSubmit={() => {}} />
and without the allow-forms
sandbox rule, the iFrame stops any forms submitting.
The best way around this is to adjust the react-hook-form example to the following and note the handle submit on a normal, non-submit type button:
5.0 - Resources
- Component Attribute Types and UI Controls (Salesforce B2C Commerce Docs - accessed June 25, 2023).
- Develop a Custom Attribute Editor (Salesforce B2C Commerce Docs - accessed June 25, 2023).
- ”Creating a Breakout Custom Attribute Editor in Page Designer for Salesforce Commerce Cloud” - a post from Bee IT which helped me in my background reading for this post.