From 7b8f765fef0e3645b00fddd727706524c5771a1d Mon Sep 17 00:00:00 2001 From: Terrence Wong Date: Sat, 2 Dec 2017 13:48:34 +0100 Subject: [PATCH] Add `TableComponent` option for addon-info --- addons/info/README.md | 99 ++++++++++++++++- addons/info/src/components/PropTable.js | 100 ++++-------------- addons/info/src/components/Story.js | 3 +- .../info/src/components/makeTableComponent.js | 94 ++++++++++++++++ addons/info/src/index.js | 4 + .../src/components/TableComponent.js | 51 +++++++++ .../src/stories/addon-info.stories.js | 8 ++ 7 files changed, 275 insertions(+), 84 deletions(-) create mode 100644 addons/info/src/components/makeTableComponent.js create mode 100644 examples/cra-kitchen-sink/src/components/TableComponent.js diff --git a/addons/info/README.md b/addons/info/README.md index b429a534c386..dd1735005b23 100644 --- a/addons/info/README.md +++ b/addons/info/README.md @@ -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 } ``` @@ -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 ) +); +``` + ### 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. diff --git a/addons/info/src/components/PropTable.js b/addons/info/src/components/PropTable.js index 68955515407c..222666041a57 100644 --- a/addons/info/src/components/PropTable.js +++ b/addons/info/src/components/PropTable.js @@ -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); @@ -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 No propTypes defined!; } @@ -131,7 +59,7 @@ export default function PropTable(props) { - {array.map(row => ( + {propDefinitions.map(row => ( {row.property} @@ -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, + }) + ), }; diff --git a/addons/info/src/components/Story.js b/addons/info/src/components/Story.js index 8d09798e320c..bd6e8f992f85 100644 --- a/addons/info/src/components/Story.js +++ b/addons/info/src/components/Story.js @@ -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'; @@ -339,7 +338,7 @@ export default class Story extends React.Component { // eslint-disable-next-line react/no-array-index-key

"{getName(type)}" Component

- { + 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 ; + }; +} diff --git a/addons/info/src/index.js b/addons/info/src/index.js index f4a645bf8ec8..a89ea2ea9c93 100644 --- a/addons/info/src/index.js +++ b/addons/info/src/index.js @@ -1,6 +1,8 @@ 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 = { @@ -8,6 +10,7 @@ const defaultOptions = { header: true, source: true, propTables: [], + TableComponent: PropTable, maxPropsIntoLine: 3, maxPropObjectKeys: 3, maxPropArrayLength: 3, @@ -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, diff --git a/examples/cra-kitchen-sink/src/components/TableComponent.js b/examples/cra-kitchen-sink/src/components/TableComponent.js new file mode 100644 index 000000000000..1d66f0a2a95b --- /dev/null +++ b/examples/cra-kitchen-sink/src/components/TableComponent.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const Red = props => ; +const TableComponent = ({ propDefinitions }) => { + const props = propDefinitions.map( + ({ property, propType, required, description, defaultValue }) => ( + + + {property} + {required ? * : null} + + {propType.name} + {defaultValue} + {description} + + ) + ); + + return ( + + + + + + + + + + {props} +
nametypedefaultdescription
+ ); +}; + +TableComponent.defaultProps = { + propDefinitions: [], +}; + +TableComponent.propTypes = { + 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, + }) + ), +}; + +export default TableComponent; diff --git a/examples/cra-kitchen-sink/src/stories/addon-info.stories.js b/examples/cra-kitchen-sink/src/stories/addon-info.stories.js index ba836f024db1..959e6ea1e979 100644 --- a/examples/cra-kitchen-sink/src/stories/addon-info.stories.js +++ b/examples/cra-kitchen-sink/src/stories/addon-info.stories.js @@ -6,6 +6,7 @@ import { action } from '@storybook/addon-actions'; import DocgenButton from '../components/DocgenButton'; import FlowTypeButton from '../components/FlowTypeButton'; import BaseButton from '../components/BaseButton'; +import TableComponent from '../components/TableComponent'; storiesOf('Addon Info.React Docgen', module) .add( @@ -95,6 +96,13 @@ storiesOf('Addon Info.Options.styles', module).add( })(() => ) ); +storiesOf('Addon Info.Options.TableComponent', module).add( + 'Use a custom component for the table', + withInfo({ + TableComponent, + })(() => ) +); + storiesOf('Addon Info.Decorator', module) .addDecorator((story, context) => withInfo('Info could be used as a global or local decorator as well.')(story)(context)