Render Callbacks in Storybook
Published: 2020-12-30 22:52:25

---
title: Render Callbacks in Storybook
published: true
description: Some of us just need a callback with a reference to the DOM element.
tags: 
cover_image: https://i.ibb.co/QDW3gLB/D93n1kr-5d005df0-4d97-4997-ba5a-70cc2befbfd2.jpg
---

Storybook.js is flat out amazing. If you're familiar with [Rizzo](http://engineering.lonelyplanet.com/2014/05/18/a-maintainable-styleguide.html) then you know how long people have been trying to solve the living style guide problem. The issue was never "Could we?". It was, "Is all this curating cost effective?". With storybook that's no longer a question and it represents the future of UI development.

But like most new things, a huge number of our projects have a sea of red-tape to get through before they are modern enough. That is, a lot of us still decorate DOM elements rather than generate them and that makes it difficult to leverage the component goodness that is Storybook.

## The Problem
Storybook supports HTML component development, but currently does not provide a callback to the story with a reference to the newly rendered DOM element. Additionally, we need access to the Story's meta within that callback. I imagine the thinking is, use a framework if you need javascript and the addons api probably starts to break down if they start handing off control to the dev.

## The Goal
What we need is to be able to get a callback with a reference to our DOM element _and_ the Story's meta once it's been rendered. This is particularly needed by jQuery style developers who can't run scripts until elements exist.

## The Workaround
This isn't a hack because it leverages supported Storybook features but isn't a solve because the usage is non-standard and could become native some day. This is a collection of what I found in discord messages, github issues and source code so the credit goes to the community.

#### Decorators to the rescue.
Storybook's Decorators feature serves to allow custom alterations _just_ within storybook and our test suite would still get the pure component. Decorators are an array of functions that just so happen to run at a time where we can use the addon api to listen for the render event and yet can still communicate with our stories.

Armed with this, we can create our callback. I'm choosing to solve this globally but will work anywhere Decorators do.

The aim is to supply our decorator with a selector to find and a function to call with what it found.

Note: I'm skipping addon registration which would be needed for an official addon.
```js
// .storybook/callback-addon/decorators.js
import addons from "@storybook/addons";

export const withRenderCallback = (selector, callback) => {
  return (Story, context) => {
    // your needs from context may vary
    const { args, id, kind, name, ...rest } = context;
    addons.getChannel().once(STORY_RENDERED, (e) => {
      callback(document.querySelector(selector), { args, id, kind, name });
    });
    return Story();
  };
};
```

Now, from within our Story file we can decorate as needed
```js
// stories/Button.stories.js
import { withRenderCallback } from "./.storybook/callback-addon/decorators.js";
import { Button } from "/button.ts";

export default {
  title: "Example/Button",
  argTypes: {
    onclick: { action: "button clicked" }
  }
}

// create a js component that renders our HTML
const Component = args => {
  return `<a data-story-of="Button" class="btn btn-primary">This is a button</a>`;
}

// create a template for bindings
const Template = (args) => Component(args);

// bind and decorate a story
export const Primary = Template.bind({});
Primary.decorators = [
  withRenderCallback(`[data-story-of="Button"]`, (element, props) => {
      const { onclick } = props.args;
      element.onclick = onclick;
      const btn = new Button($(element));
    }),
];
```

### Getting Knobs to work
If you got this far, it's probably because you want to now control your component. Controls handle updating props but not calling methods on your components. Knobs can do this but manually.
```js
import { button } from "@storybook/addon-knobs";
...
export const Primary = Template.bind({});
Primary.decorators = [
  withRenderCallback(`[data-story-of="Button"]`, (element, props) => {
      const { onclick } = props.args;
      element.onclick = onclick;
      const btn = new Button($(element));
      button(
        "set 'loading'",
        () => {
          btn.setState("loading");
          return false; // prevent further processing
        },
        "group_1"
      );
      button(
        "set 'default'",
        () => {
          btn.setState("default");
          return false;
        },
        "group_1"
      );
    }),
];
```
And that's it!
This was a very specific demo so keep in mind Decorators can work at multiple levels. Common controls could be set at the Stories level for instance. This method may work in many situations but I would always recommend seeking out officially supported patterns first.