Skip to content

Latest commit

 

History

History
165 lines (113 loc) · 7.97 KB

multiframework.md

File metadata and controls

165 lines (113 loc) · 7.97 KB

Storybook Docs framework dev guide

Storybook Docs provides basic support for all non-RN Storybook view layers out of the box. However, some frameworks have been docs-optimized, adding features like automatic props table generation and inline story rendering. This document is a dev guide for how to optimize a new framework in docs.

Framework-specific configuration

Your framework might need framework-specific configuration. This could include adding extra webpack loaders or global decorators/story parameters.

Addon-docs handles this kind of customization by file naming convention. Its common preset does this by looking for files ../<framework>/{preset,config}.[tj]sx?, where <framework> is the framework identifier, e.g. vue, angular, react, etc.

For example, consider Storybook Docs for Vue, which needs vue-docgen-loader in its webpack config, and also has custom extraction functions for props tables and component descriptions.

For webpack configuration, Docs for Vue defines preset.ts, which follows the preset file structure:

export function webpack(webpackConfig: any = {}, options: any = {}) {
  webpackConfig.module.rules.push({
    test: /\.vue$/,
    loader: 'vue-docgen-loader',
    enforce: 'post',
  });
  return webpackConfig;
}

This appends vue-docgen-loader to the existing configuration, which at this point will also include modifications made by the common preset.

For props tables and descriptions, both of which are described in more detail below, it defines a file config.tsx.

Arg tables

Each framework can auto-generate ArgTables by exporting one or more ArgType enhancers, which extracts a component's properties into a common data structure.

Here's how it's done in Vue's framework-specific preview.js:

import { enhanceArgTypes } from './enhanceArgTypes';

export const argTypesEnhancers = [enhanceArgTypes];

The enhanceArgTypesfunction takes a StoryContext (including the story id, parameters, args, argTypes, etc.), and returns an updated ArgTypes object:

export interface ArgType {
  name?: string;
  description?: string;
  defaultValue?: any;
  [key: string]: any;
}

export interface ArgTypes {
  [key: string]: ArgType;
}

For more information on what this generation looks like, see the controls generation docs.

For React and Vue, the extraction works as follows:

  • A webpack loader is added to the user's config via the preset
  • The loader annotates the component with a field, __docgenInfo, which contains some metadata
  • The view-layer specific enhanceArgTypes function translates that metadata into ArgTypes

For Angular, Web components, and Ember, the extraction works as follows:

  • Read JSON file in the user's .storybook/preview.json and story it into a global variable
  • The view-layer specific enhanceArgTypes function translates that metadata into ArgTypes

However, for your framework you may want to implement this in some other way.

Component descriptions

Component descriptions are enabled by the docs.extractComponentDescription parameter, which extract's a component description (usually from source code comments) into a markdown string.

It follows the pattern of Arg tables above, only it's even simpler because the function output is simply a string (or null if there no description).

Inline story rendering

Inline story rendering is another framework specific optimization, made possible by the docs.prepareForInline parameter.

Again let's look at Vue's framework-specific preview.js:

import toReact from '@egoist/vue-to-react';

addParameters({
  docs: {
    // `container`, `page`, etc. here
    prepareForInline: (storyFn, { args }) => {
      const Story = toReact(storyFn());
      return <Story {...args} />;
    },
  },
});

The input is the story function and the story context (id, parameters, args, etc.), and the output is a React element, because we render docs pages in react. In the case of Vue, all of the work is done by the @egoist/vue-to-react library. If there's no analogous library for your framework, you may need to figure it out yourself!

Dynamic source rendering

With the release of Storybook 6.0, we've improved how stories are rendered in the Source doc block. One of such improvements is the dynamic source type, which renders a snippet based on the output the story function.

This dynamic rendering is framework-specific, meaning it needs a custom implementation for each framework.

Let's take a look at the React framework implementation of dynamic snippets as a reference for implementing this feature in other frameworks:

import { addons, StoryContext } from '@storybook/addons';
import { SNIPPET_RENDERED } from '../../shared';

export const jsxDecorator = (storyFn: any, context: StoryContext) => {
  const story = storyFn();

  // We only need to render JSX if the source block is actually going to
  // consume it. Otherwise it's just slowing us down.
  if (skipJsxRender(context)) {
    return story;
  }

  const channel = addons.getChannel();

  const options = {}; // retrieve from story parameters
  const jsx = renderJsx(story, options);
  channel.emit(SNIPPET_RENDERED, (context || {}).id, jsx);

  return story;
};

A few key points from the above snippet:

  • The renderJsx function call is responsible for transforming the output of a story function into a string specific to the framework (in this case React).
  • The returned snippet string is emitted on Storybook's channel through channel.emit() and subsequently consumed up by the Source block for any given story, if it exists.
To learn more and see how it's implemented in context, check out the code .

Now we need a way to configure how it's displayed in the UI:

import { jsxDecorator } from './jsxDecorator';
export const decorators = [jsxDecorator];

This configures the jsxDecorator to be run on every story.

To learn more and see how it's implemented in context, check out the code .

More resources