Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TableComponent option for addon-info #2400

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 98 additions & 1 deletion addons/info/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ setDefaults({
maxPropsIntoLine: 1, // Max props to display per line in source code
maxPropObjectKeys: 10, // Displays the first 10 characters of the prop name
maxPropArrayLength: 10, // Displays the first 10 items in the default prop array
maxPropStringLength: 100, // Displays the first 100 characters in the default prop string
maxPropStringLength: 100, // Displays the first 100 characters in the default prop string,
TableComponent: props => {}, // Override the component used to render the props table
}
```

Expand Down Expand Up @@ -160,6 +161,102 @@ setDefaults({
setAddon(infoAddon);
```

### Rendering a Custom Table

The `TableComponent` option allows you to define how the prop table should be rendered. Your component will be rendered with the following props.

```js
{
propDefinitions: Array<{
property: string, // The name of the prop
propType: Object | string, // The prop type. TODO: info about what this object is...
required: boolean, // True if the prop is required
description: string, // The description of the prop
defaultValue: any // The default value of the prop
}>
}
```

Example:

```js
// button.js
// @flow
import React from 'react'

const paddingStyles = {
small: '4px 8px',
medium: '8px 16px'
}

const Button = ({
size,
...rest
}: {
/** The size of the button */
size: 'small' | 'medium'
}) => {
const style = {
padding: paddingStyles[size] || ''
}
return <button style={style} {...rest} />
}
Button.defaultProps = {
size: 'medium'
}

export default Button
```
```js
// stories.js
import React from "react";

import { storiesOf } from "@storybook/react";
import { withInfo } from "@storybook/addon-info";
import Button from "./button";

const Red = props => <span style={{ color: "red" }} {...props} />;

const TableComponent = ({ propDefinitions }) => {
const props = propDefinitions.map(
({ property, propType, required, description, defaultValue }) => {
return (
<tr key={property}>
<td>
{property}
{required ? <Red>*</Red> : null}
</td>
<td>{propType.name}</td>
<td>{defaultValue}</td>
<td>{description}</td>
</tr>
);
}
);

return (
<table>
<thead>
<tr>
<th>name</th>
<th>type</th>
<th>default</th>
<th>description</th>
</tr>
</thead>
<tbody>{props}</tbody>
</table>
);
};

storiesOf("Button", module).add(
"with text",
withInfo({
TableComponent
})(() => <Button>Hello Button</Button>)
);
```

### React Docgen Integration

React Docgen is included as part of the @storybook/react package through the use of `babel-plugin-react-docgen` during babel compile time.
Expand Down
100 changes: 19 additions & 81 deletions addons/info/src/components/PropTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,81 +7,6 @@ import { Table, Td, Th } from '@storybook/components';
import PropVal from './PropVal';
import PrettyPropType from './types/PrettyPropType';

const PropTypesMap = new Map();

Object.keys(PropTypes).forEach(typeName => {
const type = PropTypes[typeName];

PropTypesMap.set(type, typeName);
PropTypesMap.set(type.isRequired, typeName);
});

const isNotEmpty = obj => obj && obj.props && Object.keys(obj.props).length > 0;

const hasDocgen = type => isNotEmpty(type.__docgenInfo);

const propsFromDocgen = type => {
const props = {};
const docgenInfoProps = type.__docgenInfo.props;

Object.keys(docgenInfoProps).forEach(property => {
const docgenInfoProp = docgenInfoProps[property];
const defaultValueDesc = docgenInfoProp.defaultValue || {};
const propType = docgenInfoProp.flowType || docgenInfoProp.type || 'other';

props[property] = {
property,
propType,
required: docgenInfoProp.required,
description: docgenInfoProp.description,
defaultValue: defaultValueDesc.value,
};
});

return props;
};

const propsFromPropTypes = type => {
const props = {};

if (type.propTypes) {
Object.keys(type.propTypes).forEach(property => {
const typeInfo = type.propTypes[property];
const required = typeInfo.isRequired === undefined;
const docgenInfo =
type.__docgenInfo && type.__docgenInfo.props && type.__docgenInfo.props[property];
const description = docgenInfo ? docgenInfo.description : null;
let propType = PropTypesMap.get(typeInfo) || 'other';

if (propType === 'other') {
if (docgenInfo && docgenInfo.type) {
propType = docgenInfo.type.name;
}
}

props[property] = { property, propType, required, description };
});
}

if (type.defaultProps) {
Object.keys(type.defaultProps).forEach(property => {
const value = type.defaultProps[property];

if (value === undefined) {
return;
}

if (!props[property]) {
props[property] = { property };
}

props[property].defaultValue = value;
});
}

return props;
};

export const multiLineText = input => {
if (!input) return input;
const text = String(input);
Expand All @@ -100,16 +25,19 @@ export const multiLineText = input => {
};

export default function PropTable(props) {
const { type, maxPropObjectKeys, maxPropArrayLength, maxPropStringLength } = props;
const {
type,
maxPropObjectKeys,
maxPropArrayLength,
maxPropStringLength,
propDefinitions,
} = props;

if (!type) {
return null;
}

const accumProps = hasDocgen(type) ? propsFromDocgen(type) : propsFromPropTypes(type);
const array = Object.values(accumProps);

if (!array.length) {
if (!propDefinitions.length) {
return <small>No propTypes defined!</small>;
}

Expand All @@ -131,7 +59,7 @@ export default function PropTable(props) {
</tr>
</thead>
<tbody>
{array.map(row => (
{propDefinitions.map(row => (
<tr key={row.property}>
<Td bordered code>
{row.property}
Expand All @@ -158,10 +86,20 @@ export default function PropTable(props) {
PropTable.displayName = 'PropTable';
PropTable.defaultProps = {
type: null,
propDefinitions: [],
};
PropTable.propTypes = {
type: PropTypes.func,
maxPropObjectKeys: PropTypes.number.isRequired,
maxPropArrayLength: PropTypes.number.isRequired,
maxPropStringLength: PropTypes.number.isRequired,
propDefinitions: PropTypes.arrayOf(
PropTypes.shape({
property: PropTypes.string.isRequired,
propType: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
required: PropTypes.bool.isRequired,
description: PropTypes.string,
defaultValue: PropTypes.any,
})
),
};
3 changes: 1 addition & 2 deletions addons/info/src/components/Story.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { baseFonts } from '@storybook/components';

import marksy from 'marksy';

import PropTable from './PropTable';
import Node from './Node';
import { Pre } from './markdown';

Expand Down Expand Up @@ -339,7 +338,7 @@ export default class Story extends React.Component {
// eslint-disable-next-line react/no-array-index-key
<div key={`${getName(type)}_${i}`}>
<h2 style={this.state.stylesheet.propTableHead}>"{getName(type)}" Component</h2>
<PropTable
<this.props.PropTable
type={type}
maxPropObjectKeys={maxPropObjectKeys}
maxPropArrayLength={maxPropArrayLength}
Expand Down
94 changes: 94 additions & 0 deletions addons/info/src/components/makeTableComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/* eslint-disable no-underscore-dangle */

import PropTypes from 'prop-types';
import React from 'react';

const PropTypesMap = new Map();

Object.keys(PropTypes).forEach(typeName => {
const type = PropTypes[typeName];

PropTypesMap.set(type, typeName);
PropTypesMap.set(type.isRequired, typeName);
});

const isNotEmpty = obj => obj && obj.props && Object.keys(obj.props).length > 0;

const hasDocgen = type => isNotEmpty(type.__docgenInfo);

const propsFromDocgen = type => {
const props = {};
const docgenInfoProps = type.__docgenInfo.props;

Object.keys(docgenInfoProps).forEach(property => {
const docgenInfoProp = docgenInfoProps[property];
const defaultValueDesc = docgenInfoProp.defaultValue || {};
const propType = docgenInfoProp.flowType || docgenInfoProp.type || 'other';

props[property] = {
property,
propType,
required: docgenInfoProp.required,
description: docgenInfoProp.description,
defaultValue: defaultValueDesc.value,
};
});

return props;
};

const propsFromPropTypes = type => {
const props = {};

if (type.propTypes) {
Object.keys(type.propTypes).forEach(property => {
const typeInfo = type.propTypes[property];
const required = typeInfo.isRequired === undefined;
const docgenInfo =
type.__docgenInfo && type.__docgenInfo.props && type.__docgenInfo.props[property];
const description = docgenInfo ? docgenInfo.description : null;
let propType = PropTypesMap.get(typeInfo) || 'other';

if (propType === 'other') {
if (docgenInfo && docgenInfo.type) {
propType = docgenInfo.type.name;
}
}

props[property] = { property, propType, required, description };
});
}

if (type.defaultProps) {
Object.keys(type.defaultProps).forEach(property => {
const value = type.defaultProps[property];

if (value === undefined) {
return;
}

if (!props[property]) {
props[property] = { property };
}

props[property].defaultValue = value;
});
}

return props;
};

export default function makeTableComponent(Component) {
return props => {
if (!props.type) { // eslint-disable-line
return null;
}

const propDefinitionsMap = hasDocgen(props.type)
? propsFromDocgen(props.type)
: propsFromPropTypes(props.type);
const propDefinitions = Object.values(propDefinitionsMap);

return <Component propDefinitions={propDefinitions} {...props} />;
};
}
4 changes: 4 additions & 0 deletions addons/info/src/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';
import deprecate from 'util-deprecate';
import Story from './components/Story';
import PropTable from './components/PropTable';
import makeTableComponent from './components/makeTableComponent';
import { H1, H2, H3, H4, H5, H6, Code, P, UL, A, LI } from './components/markdown';

const defaultOptions = {
inline: false,
header: true,
source: true,
propTables: [],
TableComponent: PropTable,
maxPropsIntoLine: 3,
maxPropObjectKeys: 3,
maxPropArrayLength: 3,
Expand Down Expand Up @@ -53,6 +56,7 @@ function addInfo(storyFn, context, infoOptions) {
showSource: Boolean(options.source),
propTables: options.propTables,
propTablesExclude: options.propTablesExclude,
PropTable: makeTableComponent(options.TableComponent),
styles: typeof options.styles === 'function' ? options.styles : s => s,
marksyConf,
maxPropObjectKeys: options.maxPropObjectKeys,
Expand Down
Loading