If you are having issues with composite components with placeholders in Storybook or Jest, this blog is for you.
Background
In Sitecoreย Next.js, componentFactory.js is a temp file generated at build time that maps Sitecore renderings to react to components. When you place a rendering into a placeholder, this factory is responsible for correctly finding the right react component to display. However, you may find that if you are trying to display a composite component (with its own custom placeholder) in Storybook, you will be faced with an error:
This blog is aimed at solving this issue by creating a mock component factory.
The Issue
Storybook is simply unable to find the componentFactory.js because it is missing the Sitecore-specific wrapper that provides context and componentFactory.
My Setup
I have a parent component named ExpandableCardsContainer that contains a placeholder 'expandable-card' and a child component called ExpandableCard meant to be added to the 'expandable-card' placeholder.
const ExpandableCardsContainer = ({ rendering }: ExpandableCardsContainerProps): JSX.Element => {
return (
<div className="expandable-cards-container">
<Placeholder
name={`expandable-card`}
rendering={rendering}
/>
</div>
);
};
const ExpandableCard = ({ fields }: ExpandableCardProps): JSX.Element => (
<div className="expandable-card">
Content
</div>
);
My Storybook file:
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">import</span></span></span> ExpandableCardsContainer <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">from</span></span></span> <span class="hljs-string"><span class="hljs-string"><span class="hljs-string">'components/Feature/Page Content/ExpandableCardsContainer'</span></span></span>;
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">import</span></span></span> { loremIpsumGenerator } <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">from</span></span></span> <span class="hljs-string"><span class="hljs-string"><span class="hljs-string">'lib/lorem-ipsum-generator'</span></span></span>;
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">import</span></span></span> { withDatasourceCheckComponentArgs } <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">from</span></span></span> <span class="hljs-string"><span class="hljs-string"><span class="hljs-string">'src/stories/helper'</span></span></span>;
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">export</span></span></span> <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">default</span></span></span> {
title: <span class="hljs-string"><span class="hljs-string"><span class="hljs-string">'Feature/Page Content/ExpandableCardsContainer'</span></span></span>,
component: ExpandableCardsContainer,
argTypes: {},
} <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">as</span></span></span> ComponentMeta<<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">typeof</span></span></span> ExpandableCardsContainer>;
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span> expandableCardsFactory = (count: <span class="hljs-built_in"><span class="hljs-built_in"><span class="hljs-built_in">number</span></span></span>): <span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">any</span></span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function"> =></span></span></span></span> {
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">return</span></span></span> <span class="hljs-built_in"><span class="hljs-built_in"><span class="hljs-built_in">Array</span></span></span>.from(<span class="hljs-built_in"><span class="hljs-built_in"><span class="hljs-built_in">Array</span></span></span>(count).keys()).map(<span class="hljs-function"><span class="hljs-function"><span class="hljs-function"><span class="hljs-function">(</span></span></span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">index: </span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-built_in"><span class="hljs-function"><span class="hljs-params"><span class="hljs-built_in"><span class="hljs-function"><span class="hljs-params"><span class="hljs-built_in"><span class="hljs-function"><span class="hljs-params"><span class="hljs-built_in"><span class="hljs-function"><span class="hljs-params"><span class="hljs-built_in"><span class="hljs-function"><span class="hljs-params"><span class="hljs-built_in"><span class="hljs-function"><span class="hljs-params"><span class="hljs-built_in">number</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function">) =></span></span></span></span> {
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span> imgSrc = <span class="hljs-string"><span class="hljs-string"><span class="hljs-string"><span class="hljs-string">`stories/cards/cat</span></span></span><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst">${(index % </span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number">4</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst">) + </span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-number">1</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst">}</span></span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-string"><span class="hljs-string"><span class="hljs-string">.jpg`</span></span></span></span>;
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">return</span></span></span> {
componentName: <span class="hljs-string"><span class="hljs-string"><span class="hljs-string">'ExpandableCard'</span></span></span>,
dataSource: <span class="hljs-string"><span class="hljs-string"><span class="hljs-string">'Expandable Card Datasource'</span></span></span>,
fields: {
... content
},
};
});
};
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span> Template: ComponentStory<<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">typeof</span></span></span> ExpandableCardsContainer> = <span class="hljs-function"><span class="hljs-function"><span class="hljs-function"><span class="hljs-function">(</span></span></span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">args</span></span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function">) =></span></span></span></span> (
<ExpandableCardsContainer {...args} />
);
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">export</span></span></span> <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span> TenCards = Template.bind({});
TenCards.args = {
...withDatasourceCheckComponentArgs,
rendering: {
componentName: <span class="hljs-string"><span class="hljs-string"><span class="hljs-string">'ExpandableCardsContainer'</span></span></span>,
placeholders: {
<span class="hljs-string"><span class="hljs-string"><span class="hljs-string">'expandable-card'</span></span></span>: expandableCardsFactory(<span class="hljs-number"><span class="hljs-number"><span class="hljs-number">10</span></span></span>),
},
},
};
The Solution
Add or merge the code below to your Storybook's preview.js:
const mockSitecoreContext = {
context: {
pageEditing: false,
},
setContext: () => { },
};
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">export</span></span></span> <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span> mockComponentFactory = <span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword"><span class="hljs-function"><span class="hljs-keyword">function</span></span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function"> (</span></span></span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">componentName</span></span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function">) </span></span></span></span>{
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span> components = <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">new</span></span></span> <span class="hljs-built_in"><span class="hljs-built_in"><span class="hljs-built_in">Map</span></span></span>();
components.set(<span class="hljs-string"><span class="hljs-string"><span class="hljs-string">'YOUR RENDERING NAME'</span></span></span>, <YOUR COMPONENT>)
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span> component = components.get(componentName);
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">if</span></span></span> (component?.element) {
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">return</span></span></span> component.element();
}
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">return</span></span></span> component?.default || component;
};
<span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">export</span></span></span> <span class="hljs-keyword"><span class="hljs-keyword"><span class="hljs-keyword">const</span></span></span> decorators = [
<span class="hljs-function"><span class="hljs-function"><span class="hljs-function"><span class="hljs-function">(</span></span></span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">Story</span></span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-function"><span class="hljs-function"><span class="hljs-function">) =></span></span></span></span> (
<span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><</span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name">SitecoreContext</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"> </span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr">context</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">=</span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string">{mockSitecoreContext}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"> </span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr">componentFactory</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">=</span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string">{mockComponentFactory}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="xml"><span class="xml"><span class="xml">
</span></span></span><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><</span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name">Story</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"> /></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="xml"><span class="xml"><span class="xml">
</span></span></span><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"></</span></span></span></span></span></span></span></span></span></span></span></span><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name">SitecoreContext</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
),
];
Note that we straight-up copied the structure of the auto-generated componentFactory.js, but we manually set the map from the rendering name to their respective components. In my case, I had to add 'components.set('ExpandableCard', ExpandableCard)'. Do not forget to import the component FYI.
Additionally, if you have not, I included the mockSitecoreContext in order to bypass 'missing sitecore Context' error.
Boom.