If you’re not using Storybook, you’re kinda missing out on one of the best free, open-source tools for front-end developers. I’m not going to head into great depths about how great Storybook is. You’ll just have to trust me. It’s great for dev, testing, basic accessibility validation and more. You can even use Chromatic to automate visual “diffing” across versions in your CI/CD. It’s a bunch. Go check it out.

There’s been a bunch of blogs out there and even some videos about how to get going with Storybook and Sitecore. I won’t drain that whole deal, since there’s way better folks out there to get you started.

I will say, though, I’ve made some changes in the linked repo that are what I consider “Quality of Life” things:

Ok, so as a quick refresh, here’s what a simple component looks like:

import { Text, TextField, withDatasourceCheck } from '@sitecore-jss/sitecore-jss-nextjs';
import { ComponentProps } from 'lib/component-props';

//Forms created with https://bulma.io/, which is pretty neat!
// https://github.com/jgthms/bulma

type HeroProps = ComponentProps & {
  fields: {
    Title: TextField;
    SubTitle: TextField;
  };
};

export const Default = withDatasourceCheck()<HeroProps>((props): JSX.Element => {
  const { Title, SubTitle } = props.fields;

  return (
    <section className="hero">
      <div className="hero-body">
        <Text tag="p" class="title" field={Title} />
        <Text tag="p" class="subtitle" field={SubTitle} />
      </div>
    </section>
  );
});

Simple. Title. Subtitle. Win. Now for the Stories:

import type { Meta, StoryObj } from '@storybook/react';
import { Default as DefaultHero } from './Hero';

const meta: Meta<typeof DefaultHero> = {
  title: 'Components/Hero',
  component: DefaultHero,
};

export default meta;
type Story = StoryObj<typeof DefaultHero>;

export const Default: Story = {
  args: {
    rendering: {
      uid: 'Empty',
      componentName: 'Hero',
      dataSource: 'Empty',
    },
    params: {},
    fields: {
      Title: { value: 'Storybook  Title' },
      SubTitle: { value: 'Storybook  SubTitle' },
    },
  },
};

export const DefaultWithoutSubTitle: Story = {
  args: {
    rendering: {
      uid: 'Empty',
      componentName: 'Hero',
      dataSource: 'Empty',
    },
    params: {},
    fields: {
      Title: { value: 'Storybook  Title' },
      SubTitle: { value: '' },
    },
  },
};

Nothing in here that’s rocket science. We import the component, map some props and storybook does the rest. Now, let’s take a more complex component. How about a form with a drop down that requires a fetch of data from Sitecore:

/* eslint-disable @typescript-eslint/no-unused-vars */
import {
  ComponentRendering,
  GetStaticComponentProps,
  LayoutServiceData,
  Text,
  TextField,
  useComponentProps,
  withDatasourceCheck,
} from '@sitecore-jss/sitecore-jss-nextjs';
import { ComponentProps } from 'lib/component-props';

//Forms created with https://bulma.io/, which is pretty neat!
// https://github.com/jgthms/bulma

type FormProps = ComponentProps & {
  fields: {
    FormTitle: TextField;
  };
};



export const BlogForm = withDatasourceCheck()<FormProps>((props): JSX.Element => {
  const formSettings = useComponentProps<FormSettings>(props.rendering.uid);

  return (
    <>
      <Text class="title is-2" field={props.fields.FormTitle} tag="h2" />
      <div className="field">
        <label className="label">First Name</label>
        <div className="control">
          <input className="input" type="text" placeholder="Text input" />
        </div>
      </div>
      <div className="field">
        <label className="label">Last Name</label>
        <div className="control">
          <input className="input" type="text" placeholder="Text input" />
        </div>
      </div>

      <div className="field">
        <label className="label">Subject</label>
        <div className="control">
          <div className="select">
            <select>
              <option>Select</option>
              {formSettings?.SubjectOptions?.map((opt) => (
                <option key={opt.ID} value={opt.ID}>
                  {opt.Value}
                </option>
              ))}
            </select>
          </div>
        </div>
      </div>

      <div className="field is-grouped">
        <div className="control">
          <button className="button is-link">Submit</button>
        </div>
        <div className="control">
          <button className="button is-link is-light">Cancel</button>
        </div>
      </div>
    </>
  );
});


export const getStaticProps: GetStaticComponentProps = async (
  _rendering: ComponentRendering,
  _layoutData: LayoutServiceData
) => {
  //TODO: This is where you'd return the props for the form
  const emptySettings: FormSettings = {
    SubjectOptions: [
      { ID: '1', Value: 'From Sitecore 1' },
      { ID: '2', Value: 'From Sitecore 2' },
    ],
  };

  return emptySettings;
};

export default BlogForm;

Notice our getStaticProps. It’s doing nothing smart right now. This is where you’d wire up your own GraphQL to fire off a request and get the values for the select. But that’s happening because Sitecore invokes it as part of their framework. Invoking that method is not a component choice, really. Here’s an associated story:

import type { Meta, StoryObj } from '@storybook/react';
import { BlogForm } from './BlogForm';

const meta: Meta<typeof BlogForm> = {
  title: 'Components/Form',
  component: BlogForm,
};

export default meta;
type Story = StoryObj<typeof BlogForm>;

export const BasicForm: Story = {
  args: {
    rendering: {
      uid: 'Empty',
      componentName: 'BlogForm',
      dataSource: 'Empty',
    },
    fields: { FormTitle: { value: 'Storybook Form Title' } },
  },
};

Here’s what SB looks like with this code:

This will work great in Sitecore, because something will invoke getStaticProps for us. But…in SB, not so much. So what’s the “right” way to do this? A couple things:

  • We need to have Storybook not call the default component here. Why? Because it doesn’t have all the params necessary.
  • We need to update our story to inject the right params for the drop down.
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
  ComponentRendering,
  GetStaticComponentProps,
  LayoutServiceData,
  Text,
  TextField,
  useComponentProps,
  withDatasourceCheck,
} from '@sitecore-jss/sitecore-jss-nextjs';
import { ComponentProps } from 'lib/component-props';

//Forms created with https://bulma.io/, which is pretty neat!
// https://github.com/jgthms/bulma

type FormProps = ComponentProps & {
  fields: {
    FormTitle: TextField;
  };
};
export const _BlogForm = ({
  settings,
  editProps,
}: {
  settings: FormSettings;
  editProps: FormProps;
}): JSX.Element => {
  console.log(editProps.fields);
  return (
    <>
      <Text class="title is-2" field={editProps.fields.FormTitle} tag="h2" />
      <div className="field">
        <label className="label">First Name</label>
        <div className="control">
          <input className="input" type="text" placeholder="Text input" />
        </div>
      </div>
      <div className="field">
        <label className="label">Last Name</label>
        <div className="control">
          <input className="input" type="text" placeholder="Text input" />
        </div>
      </div>

      <div className="field">
        <label className="label">Subject</label>
        <div className="control">
          <div className="select">
            <select>
              <option>Select</option>
              {settings.SubjectOptions?.map((opt) => (
                <option key={opt.ID} value={opt.ID}>
                  {opt.Value}
                </option>
              ))}
            </select>
          </div>
        </div>
      </div>

      <div className="field is-grouped">
        <div className="control">
          <button className="button is-link">Submit</button>
        </div>
        <div className="control">
          <button className="button is-link is-light">Cancel</button>
        </div>
      </div>
    </>
  );
};

const BlogForm = withDatasourceCheck()<FormProps>((props): JSX.Element => {
  const formSettings = useComponentProps<FormSettings>(props.rendering.uid);

  //If we didn't find formSettings, we probably should be hiding things...
  if (!formSettings) {
    {
      return <></>;
    }
  }

  return <_BlogForm settings={formSettings} editProps={props} />;
});

export const getStaticProps: GetStaticComponentProps = async (
  _rendering: ComponentRendering,
  _layoutData: LayoutServiceData
) => {
  //TODO: This is where you'd return the props for the form
  const emptySettings: FormSettings = {
    SubjectOptions: [
      { ID: '1', Value: 'From Sitecore 1' },
      { ID: '2', Value: 'From Sitecore 2' },
    ],
  };

  return emptySettings;
};

export default BlogForm;

Some specific callouts:

  • We’ve abstracted the component from the actual Sitecore-hooked component (Line 73, which invokes the actual component which does the rendering
  • The _BlogForm, which renders the HTML, now gets hits props from two different locations.
    • “settings” comes from either getStaticProps or from Storybook
    • “editProps” are what is fed from the DataSource in Sitecore. We still want to be able to edit with Pages, so this is a crucial differentiation.

What’s the story look like for this?

import type { Meta, StoryObj } from '@storybook/react';
import { _BlogForm } from './BlogForm';

const meta: Meta<typeof _BlogForm> = {
  title: 'Components/Form',
  component: _BlogForm,
};

export default meta;
type Story = StoryObj<typeof _BlogForm>;

export const BasicForm: Story = {
  args: {
    editProps: {
      rendering: {
        uid: '',
        componentName: '',
        dataSource: '',
      },
      params: {},
      fields: {
        FormTitle: { value: 'Storybook form Title' },
      },
    },
    settings: {
      SubjectOptions: [
        { ID: '1', Value: 'Contact Us' },
        { ID: '2', Value: 'Praises' },
        { ID: '3', Value: 'Speak to the Manager' },
      ],
    },
  },
};

Notice the corresponding editProps and editSettings? When you view this component in SB, you get:

Looks like a match for the story data!

Now when you view in in Sitecore, you can see the Sitecore data:

That’s from Sitecore itself (in our case, it’s hard-cdoed in the getStaticProps, but you know how that would work), as you’d imagine.

All in all, this was a good exercise in just the power of Storybook. The ability to segment Front End and Sitecore Dev is pretty important to an efficient dev-flow.