Mastering Placeholders and Storybook in XM Cloud
We usually associate Placeholders inside our XM Cloud Headless projects as daunting and we tend to try to find work arounds from it. There are a couple of reasons and one of them is you might not really know how to get them working on storybook. I have an idea of what you might be facing right now when trying to create storybook components for these.
Imagine how powerful Placeholders are if you are able to confidently use them as one of your solutions on your projects. They allow you to manage reusable components better and create different layouts at ease.
Setting Up Your Storybook Preview File
We’ll start off with configuring your Storybook preview file. This will depend on what version of JSS you are using, a quick way of knowing that is by checking your temp folder. Older versions will have componentFactory.ts
while newer versions will have componentBuilder.ts
. What we’re aiming here is configuring the global decorators
so that our stories will act like they are inside the Sitecore ecosystem. Your decorators will look something like this.
export const decorators = [
(Story) => (
<SitecoreContext context={mockSitecoreContext} componentFactory={mockComponentFactory}>
<Story />
</SitecoreContext>
),
];
You will need to create a mockComponentFactory
that mimics how the Sitecore Component Factory works. Here is the code for that so you don’t have to think about it.
function baseComponentFactory(componentName, exportName, isEditing) {
const components = new Map();
// components.set('Component', Component);
const DEFAULT_EXPORT_NAME = 'Default';
const component = components.get(componentName);
// check that component should be dynamically imported
if (component?.element) {
// return next.js dynamic import
return component.element(isEditing);
}
if (exportName && exportName !== DEFAULT_EXPORT_NAME) {
return component[exportName];
}
return component?.Default || component?.default || component;
}
const mockComponentFactory = function (componentName, exportName) {
return baseComponentFactory(componentName, exportName, false);
};
It can look like this as well if you’re using a later version. This one utilizes the temp file created which is easier to manage.
<SitecoreContext
componentFactory={componentBuilder.getComponentFactory({ isEditing: mockLayoutData.sitecore.context.pageEditing })}
layoutData={mockLayoutData}
>
<Story />
</SitecoreContext>
Building Our Story
Our aim here is pretty straightforward, I will not try to do some complex configurations on our story. What I am here is just to showcase how you should be structuring the props passed to your component. The simplest solution of course is by printing out the data received by the component and copy pasting it directly but where’s the fun in that? We want to understand more of how things work and use that knowledge to simply create our components without the dependency whether the Sitecore setup for the component is done.
First, we have to create some essential parts of the stories file. You will need to create your meta
and Story
for your component.
const meta: Meta<typeof Component> = {
title: 'Components/Component',
component: Component,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof Component>;
Then we can go directly with what you need to do for your component to work in the story.
export const Default: Story = {
args: {
rendering: {
componentName: 'Component',
dataSource: 'DATASOURCE_VALUE',
fields: {},
placeholders: {},
params: {},
},
},
};
The code above shows some of the parts of the component props that we’ll need to complete in order for this to work. We have our fields
which hold the template fields of our component which should work normally. We normally skip or not pay in mind the params
but we’ll need to properly set this up in order for placeholders
to work properly as well. I’ve done my own discovery and some optimization by creating helpful functions that you can also use on your projects. I’ve create a mockParamsArgs
which can be reused on any function. It will look something like this.
export const paramsArgs = (id: string, variant = 'Default') => {
return {
GridParameters: 'col-12',
CacheClearingBehavior: 'Clear on publish',
DynamicPlaceholderId: id,
FieldNames: variant,
styles: 'col-12 ',
};
};
This is based on what is normally returned on components. The important bit here is the variant
which is useful when you want to switch between variants
on Storybook and id
which is used in DynamicPlaceholderId
. This is important since you will have to match the id
here with the id
inside the placeholders
. If you can remember how the name inside the Placeholder
component is formed it will looks something like this.
const phKey = `componentcontent-${params.DynamicPlaceholderId}`;
And when you look at the Layout Details of a page and see the Placeholder
field you will see something similar like componentcontent-10
. The 10
there is the DynamicPlaceholderId
of the Parent Component. So if we add the correct information to our args for our Story.
export const Default: Story = {
args: {
rendering: {
componentName: 'Component',
dataSource: 'DATASOURCE_VALUE',
fields: {},
params: paramsArgs('10'),
placeholders: {
'componentcontent-10': [
...
],
},
},
},
};
We get something like the code above. You might be wondering what items does the array componentcontent-10
hold. I can create data for that for you if you are curious.
export const Default: Story = {
args: {
rendering: {
componentName: 'Component',
dataSource: 'DATASOURCE_VALUE',
fields: {},
params: paramsArgs('10'),
placeholders: {
'componentcontent-10': [
{
componentName: 'ComponentB',
dataSource: 'DATASOURCE_VALUE',
fields: {},
placeholders: {
'componentBcontent-11': [
...
],
},
params: paramsArgs('11'),
},
],
},
},
},
};
It looks exactly like the main component’s props, nothing too complex! You can do the same thing with the 2nd level placeholders
object and you can mock nested placeholders easily.
Where Do We Go From Here?
Placeholders play an important role when creating reusable components in Sitecore. By reusing them we create a more manageable ecosystem and reduce the number of components we need to keep track of. As we start to develop the frontend code of these components, normally we usually end up developing in Storybook first. Now that we know how to setup these intricate details to get placeholders to show on our stories, we can streamline the process of development even further without the need of backend completely finishing the templates ahead.