最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Storybook: How to define argTypes for properties that are objects? - Stack Overflow

programmeradmin0浏览0评论

I have an input web component that has a very simple API - it has both a get state() and a set state(model). This web component also handles the label which is being used for the input, thus a simple model looks like this:

{
  "value": "Foobar",
  "required": true,
  "disabled": false,
  "label": {
    "text": "Label for input",
    "toolTip": "Tooltip for the input's label",
    "position": "east"
  }
}

Now to describe the argTypes for the stories, I tried this:

export default {
    title: `Components/Input/text-input`,
    argTypes: {
        value: { control: 'text' },
        disabled: { control: 'boolean' },
        required: { control: 'boolean' },
        label: {
            text: { control: 'text' },
            toolTip: { control: 'text' },
            position: { control: 'select', options: ['north', 'south', 'east', 'west'] },
        },
    },
};

it renders in Storybook as follows:

As you can see, I'm not getting proper controls for the label aspect, like e.g. a dropdown for label.position. In fact, I'm getting the very same result even if I don't define argTypes.label at all.

Sure, I could compromise on my state structure and make all label properties flat state properties like labelText, labelPosition, and labelToolTip. But as I understand, Storybook is not meant to influence design decisions this way.

This seems like a very basic requirement and I'm surprised I couldn't find anything on it in the docs.

Question: So how do I achieve that without changing my model structure?

Note: I'm using Storybook HTML v6.3.8.

Edit:

What I've tried so far to work around the current limitations:

Im using a TemplateFactory function to replace the odd Template.bind({}) just to create a new instance. Our components each support setting the component state via an el.state setter.

import { ArgsParser } from '../helper/ArgsParser.js';

export function TemplateFactory(tagName) {
    return (args) => {
        const el = document.createElement(tagName);
        el.state = ArgsParser.expand(args);
        return el;
    };
}
// ArgsParser
export class ArgsParser {
    static flat = (args) => {
        const parsedArgs = {};
        for (const [key, value] of Object.entries(args)) {
            if (['string', 'boolean', 'number'].includes(typeof value)) {
                parsedArgs[key] = value;
            } else {
                for (const innerKey in value) parsedArgs[`${key}.${innerKey}`] = value[innerKey];
            }
        }
        return parsedArgs;
    };

    static expand(args) {
        const parsedArgs = {};
        for (const [key, value] of Object.entries(args)) {
            const parsedKeys = key.split('.');
            if (parsedKeys.length === 1) {
                parsedArgs[key] = value;
            } else {
                const [parentKey, prop] = parsedKeys;
                parsedArgs[parentKey] = parsedArgs[parentKey] ?? {};
                parsedArgs[parentKey][prop] = value[prop];
            }
        }
        return parsedArgs;
    }
}
// custom-text.stories.js
import { TemplateFactory } from '../helper/TemplateFactory.js';
const TAG_NAME = 'custom-text';

export default {
    title: `Components/Input/${TAG_NAME}`,
    argTypes: {
        value: { control: 'text' },
        disabled: { control: 'boolean' },
        required: { control: 'boolean' },
        ['label.text']: { control: 'text' },
        ['label.toolTip']: { control: 'text' },
        ['label.position']: {
            control: { type: 'select', options: ['north', 'south', 'east', 'west'] },
        },
    },
};

export const EmptyEnabled = TemplateFactory(TAG_NAME);
EmptyEnabled.args = ArgsParser.flat({
    value: '',
    disabled: false,
    label: {
        text: 'Empty and Labeled',
        toolTip: 'A beautiful tooltip',
        position: 'north',
    },
});
/* assigns
{
  "value": "",
  "disabled": false,
  "label.text": "Empty and Labeled",
  "label.toolTip": "A beautiful tooltip",
  "label.position": "north"
}
*/

This results in:

If I modify the controls for the 3 label properties now, it won't affect the component. Also, the label in the initial state is gone.

If instead I assign the expanded model:

export const EmptyEnabled = TemplateFactory(TAG_NAME);
EmptyEnabled.args = {
    value: '',
    disabled: false,
    label: {
        text: 'Empty and Labeled',
        position: 'north',
    },
};

then I get this:

but when I try to use the radio buttons for label.position it doesn't affect the component, but (only after picking twice) it results in the JSON suddenly displaying undefined for position:

The same happens if I edit label.text and/or label.toolTip:

I have an input web component that has a very simple API - it has both a get state() and a set state(model). This web component also handles the label which is being used for the input, thus a simple model looks like this:

{
  "value": "Foobar",
  "required": true,
  "disabled": false,
  "label": {
    "text": "Label for input",
    "toolTip": "Tooltip for the input's label",
    "position": "east"
  }
}

Now to describe the argTypes for the stories, I tried this:

export default {
    title: `Components/Input/text-input`,
    argTypes: {
        value: { control: 'text' },
        disabled: { control: 'boolean' },
        required: { control: 'boolean' },
        label: {
            text: { control: 'text' },
            toolTip: { control: 'text' },
            position: { control: 'select', options: ['north', 'south', 'east', 'west'] },
        },
    },
};

it renders in Storybook as follows:

As you can see, I'm not getting proper controls for the label aspect, like e.g. a dropdown for label.position. In fact, I'm getting the very same result even if I don't define argTypes.label at all.

Sure, I could compromise on my state structure and make all label properties flat state properties like labelText, labelPosition, and labelToolTip. But as I understand, Storybook is not meant to influence design decisions this way.

This seems like a very basic requirement and I'm surprised I couldn't find anything on it in the docs.

Question: So how do I achieve that without changing my model structure?

Note: I'm using Storybook HTML v6.3.8.

Edit:

What I've tried so far to work around the current limitations:

Im using a TemplateFactory function to replace the odd Template.bind({}) just to create a new instance. Our components each support setting the component state via an el.state setter.

import { ArgsParser } from '../helper/ArgsParser.js';

export function TemplateFactory(tagName) {
    return (args) => {
        const el = document.createElement(tagName);
        el.state = ArgsParser.expand(args);
        return el;
    };
}
// ArgsParser
export class ArgsParser {
    static flat = (args) => {
        const parsedArgs = {};
        for (const [key, value] of Object.entries(args)) {
            if (['string', 'boolean', 'number'].includes(typeof value)) {
                parsedArgs[key] = value;
            } else {
                for (const innerKey in value) parsedArgs[`${key}.${innerKey}`] = value[innerKey];
            }
        }
        return parsedArgs;
    };

    static expand(args) {
        const parsedArgs = {};
        for (const [key, value] of Object.entries(args)) {
            const parsedKeys = key.split('.');
            if (parsedKeys.length === 1) {
                parsedArgs[key] = value;
            } else {
                const [parentKey, prop] = parsedKeys;
                parsedArgs[parentKey] = parsedArgs[parentKey] ?? {};
                parsedArgs[parentKey][prop] = value[prop];
            }
        }
        return parsedArgs;
    }
}
// custom-text.stories.js
import { TemplateFactory } from '../helper/TemplateFactory.js';
const TAG_NAME = 'custom-text';

export default {
    title: `Components/Input/${TAG_NAME}`,
    argTypes: {
        value: { control: 'text' },
        disabled: { control: 'boolean' },
        required: { control: 'boolean' },
        ['label.text']: { control: 'text' },
        ['label.toolTip']: { control: 'text' },
        ['label.position']: {
            control: { type: 'select', options: ['north', 'south', 'east', 'west'] },
        },
    },
};

export const EmptyEnabled = TemplateFactory(TAG_NAME);
EmptyEnabled.args = ArgsParser.flat({
    value: '',
    disabled: false,
    label: {
        text: 'Empty and Labeled',
        toolTip: 'A beautiful tooltip',
        position: 'north',
    },
});
/* assigns
{
  "value": "",
  "disabled": false,
  "label.text": "Empty and Labeled",
  "label.toolTip": "A beautiful tooltip",
  "label.position": "north"
}
*/

This results in:

If I modify the controls for the 3 label properties now, it won't affect the component. Also, the label in the initial state is gone.

If instead I assign the expanded model:

export const EmptyEnabled = TemplateFactory(TAG_NAME);
EmptyEnabled.args = {
    value: '',
    disabled: false,
    label: {
        text: 'Empty and Labeled',
        position: 'north',
    },
};

then I get this:

but when I try to use the radio buttons for label.position it doesn't affect the component, but (only after picking twice) it results in the JSON suddenly displaying undefined for position:

The same happens if I edit label.text and/or label.toolTip:

Share Improve this question edited Sep 21, 2021 at 12:56 connexo asked Sep 17, 2021 at 8:23 connexoconnexo 56.8k15 gold badges108 silver badges145 bronze badges 5
  • 1 Hi @connexo. I run into the same problem. Did you manage somehow to find the solution for this? I've been trying out similar approach without any success... – Lazar Nikolic Commented Jan 27, 2022 at 11:22
  • 3 @LazarNikolic Unfortunately, no. We have decided to live with what Storybook offers. – connexo Commented Jan 27, 2022 at 11:46
  • @connexo Check out the answer I posted. Let me know if that works for you. – elliottregan Commented May 13, 2022 at 2:21
  • @elliottregan where did you post it? – Design by Adrian Commented Mar 2, 2023 at 10:28
  • Someone should write flatten prop types Storybook addon for that – d9k Commented Nov 10, 2023 at 15:08
Add a comment  | 

2 Answers 2

Reset to default 4

This was my approach in an Angular project:

type InputPropOverrides = {
  'arg1.label': string,
};

export default {
  title: 'Components/MyComponent',
  component: MyComponent,
  decorators: [
    moduleMetadata({
      imports: [MyModule],
    }),
  ],
  argTypes: {
    arg1: { ... },
    'arg1.label': {
      control: {
        type: 'text',
      },
    },
  },
} as Meta;

const Template: Story<MyComponent & InputPropOverrides> = (args: MyComponent & InputPropOverrides) => {
  const updatedArgs = args;
  updatedArgs.menuItem.label = args['arg1.label'];
  return { props: updatedArgs };
};

This will render an extra control labeled arg1.label with a text field. When I input data into that field, the Story gets re-rendered with the label field gets replaced by that text.

I can manually customize any argument's property just by adding an extra argType, and passing that arg's value back to the real arg.

Apparently argTypes for nested properties are not supported. But you can map custom arg to your component's property within render() function.

Here is an example for react and typescript:

type MyComponentAndCustomArgs = React.ComponentProps<typeof MyComponent> & {
  myCustomArg: string;
};

const meta = {
  title: "Example/MyComponent",
  component: MyComponent,
  tags: ["autodocs"],
  argTypes: {
    myCustomArg: { control: "text" },
  },
  render: ({ myCustomArg, ...args }) => {
    const props: MyComponentProps = { ...args };
    props.axis.color = myCustomArg;

    return <MyComponent {...props}></MyComponent>;
  },
} satisfies Meta<MyComponentAndCustomArgs>;

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

export const Primary: Story = {
  args: {
    axis: {
      color: "black",
    },
    myCustomArg: "red",
  },
};
发布评论

评论列表(0)

  1. 暂无评论