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
: