How to Create Sitecore Components With Placeholders in Storybook

Discover how to create stories in Storybook on Sitecore components that rely on Placeholders

June 14, 2024

By John Flores

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.

Error message indicating a missing React implementation in a JSS component on an orange background.

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.



John Headshot

John Flores

Front-End Developer

John is a Senior Front-End Developer who is passionate about design and development. Outside of work, John has wide range of hobbies, from his plant collection, being a dog daddy, and a foodie.