diff --git a/docs/app/Examples/views/Feed/Content/Date.js b/docs/app/Examples/views/Feed/Content/Date.js index 2e2dd0c387..3257420f03 100644 --- a/docs/app/Examples/views/Feed/Content/Date.js +++ b/docs/app/Examples/views/Feed/Content/Date.js @@ -19,7 +19,7 @@ const Date = () => { - + 3 days ago You added Jenny Hess to your coworker group. diff --git a/docs/app/Examples/views/Feed/Content/DateSummary.js b/docs/app/Examples/views/Feed/Content/DateSummary.js index ff00dbdb1f..ecccf4c66e 100644 --- a/docs/app/Examples/views/Feed/Content/DateSummary.js +++ b/docs/app/Examples/views/Feed/Content/DateSummary.js @@ -24,7 +24,7 @@ const DateSummary = () => { You added Jenny Hess to your coworker group. - + diff --git a/src/lib/META.js b/src/lib/META.js index 952cd0aa61..975192de3a 100644 --- a/src/lib/META.js +++ b/src/lib/META.js @@ -2,6 +2,7 @@ import _ from 'lodash/fp' export const TYPES = { ADDON: 'addon', + PART: 'part', COLLECTION: 'collection', ELEMENT: 'element', VIEW: 'view', diff --git a/src/modules/Accordion/AccordionContent.js b/src/modules/Accordion/AccordionContent.js index 07c5954160..aba2a244d2 100644 --- a/src/modules/Accordion/AccordionContent.js +++ b/src/modules/Accordion/AccordionContent.js @@ -1,44 +1,53 @@ import React, { PropTypes } from 'react' import cx from 'classnames' +import { ContentPart } from '../../parts' import { getElementType, getUnhandledProps, META, useKeyOnly } from '../../lib' +// ======================================================== +// Brainstorming ways to abstract className buildup, etc. +// +// const AccordionContent = createContentPart({ +// cx: ({ active }) => [ +// useKeyOnly(active, 'active'), +// ], +// propTypes: { +// /** Whether or not the content is visible. */ +// active: PropTypes.bool, +// }, +// _meta: { +// name: 'AccordionContent', +// type: META.TYPES.MODULE, +// parent: 'Accordion', +// }, +// }) +// -------------------------------------------------------- + function AccordionContent(props) { const { active, children, className } = props const classes = cx( - 'content', useKeyOnly(active, 'active'), className ) + const rest = getUnhandledProps(AccordionContent, props) const ElementType = getElementType(AccordionContent, props) + return ( - + {children} - + ) } -AccordionContent.displayName = 'AccordionContent' - AccordionContent.propTypes = { - /** An element type to render as (string or function). */ - as: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func, - ]), - + ...ContentPart.propTypes, /** Whether or not the content is visible. */ active: PropTypes.bool, - - /** Primary content of the Content. */ - children: PropTypes.node, - - /** Classes to add to the content className. */ - className: PropTypes.string, } AccordionContent._meta = { + ...ContentPart._meta, name: 'AccordionContent', type: META.TYPES.MODULE, parent: 'Accordion', diff --git a/src/modules/Modal/Modal.js b/src/modules/Modal/Modal.js index 040f86a449..e23086e69d 100644 --- a/src/modules/Modal/Modal.js +++ b/src/modules/Modal/Modal.js @@ -2,10 +2,15 @@ import _ from 'lodash' import React, { PropTypes, Component } from 'react' import cx from 'classnames' -import ModalHeader from './ModalHeader' +import { + ActionsPart, + DescriptionPart, + HeaderPart, +} from '../../parts' +// import ModalHeader from './ModalHeader' import ModalContent from './ModalContent' -import ModalActions from './ModalActions' -import ModalDescription from './ModalDescription' +// import ModalActions from './ModalActions' +// import ModalDescription from './ModalDescription' import Portal from 'react-portal' import { @@ -78,10 +83,13 @@ class Modal extends Component { } static _meta = _meta - static Header = ModalHeader + // static Header = ModalHeader + static Header = HeaderPart static Content = ModalContent - static Description = ModalDescription - static Actions = ModalActions + // static Description = ModalDescription + static Description = DescriptionPart + // static Actions = ModalActions + static Actions = ActionsPart state = {} diff --git a/src/modules/Modal/ModalActions.js b/src/modules/Modal/ModalActions.js index 4c5817b19b..7b8ce49f98 100644 --- a/src/modules/Modal/ModalActions.js +++ b/src/modules/Modal/ModalActions.js @@ -1,44 +1,44 @@ -import React, { PropTypes } from 'react' -import classNames from 'classnames' - -import { getElementType, getUnhandledProps, META } from '../../lib' - -function ModalActions(props) { - const { children, className } = props - - const classes = classNames( - className, - 'actions' - ) - - const rest = getUnhandledProps(ModalActions, props) - const ElementType = getElementType(ModalActions, props) - - return ( - - {children} - - ) -} - -ModalActions._meta = { - name: 'ModalActions', - type: META.TYPES.MODULE, - parent: 'Modal', -} - -ModalActions.propTypes = { - /** An element type to render as (string or function). */ - as: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func, - ]), - - /** Primary content of the modal actions */ - children: PropTypes.any, - - /** Classes to add to the modal actions className */ - className: PropTypes.string, -} - -export default ModalActions +// import React, { PropTypes } from 'react' +// import classNames from 'classnames' +// +// import { getElementType, getUnhandledProps, META } from '../../lib' +// +// function ModalActions(props) { +// const { children, className } = props +// +// const classes = classNames( +// className, +// 'actions' +// ) +// +// const rest = getUnhandledProps(ModalActions, props) +// const ElementType = getElementType(ModalActions, props) +// +// return ( +// +// {children} +// +// ) +// } +// +// ModalActions._meta = { +// name: 'ModalActions', +// type: META.TYPES.MODULE, +// parent: 'Modal', +// } +// +// ModalActions.propTypes = { +// /** An element type to render as (string or function). */ +// as: PropTypes.oneOfType([ +// PropTypes.string, +// PropTypes.func, +// ]), +// +// /** Primary content of the modal actions */ +// children: PropTypes.any, +// +// /** Classes to add to the modal actions className */ +// className: PropTypes.string, +// } +// +// export default ModalActions diff --git a/src/modules/Modal/ModalDescription.js b/src/modules/Modal/ModalDescription.js index a758b06ba3..b8aa0d20e3 100644 --- a/src/modules/Modal/ModalDescription.js +++ b/src/modules/Modal/ModalDescription.js @@ -1,43 +1,43 @@ -import React, { PropTypes } from 'react' -import cx from 'classnames' - -import { getElementType, getUnhandledProps, META } from '../../lib' - -function ModalDescription(props) { - const { children, className } = props - - const classes = cx( - className, - 'description' - ) - - const rest = getUnhandledProps(ModalDescription, props) - const ElementType = getElementType(ModalDescription, props) - return ( - - {children} - - ) -} - -ModalDescription._meta = { - name: 'ModalDescription', - type: META.TYPES.MODULE, - parent: 'Modal', -} - -ModalDescription.propTypes = { - /** An element type to render as (string or function). */ - as: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func, - ]), - - /** Primary content */ - children: PropTypes.any, - - /** Classes to add to the className */ - className: PropTypes.string, -} - -export default ModalDescription +// import React, { PropTypes } from 'react' +// import cx from 'classnames' +// +// import { getElementType, getUnhandledProps, META } from '../../lib' +// +// function ModalDescription(props) { +// const { children, className } = props +// +// const classes = cx( +// className, +// 'description' +// ) +// +// const rest = getUnhandledProps(ModalDescription, props) +// const ElementType = getElementType(ModalDescription, props) +// return ( +// +// {children} +// +// ) +// } +// +// ModalDescription._meta = { +// name: 'ModalDescription', +// type: META.TYPES.MODULE, +// parent: 'Modal', +// } +// +// ModalDescription.propTypes = { +// /** An element type to render as (string or function). */ +// as: PropTypes.oneOfType([ +// PropTypes.string, +// PropTypes.func, +// ]), +// +// /** Primary content */ +// children: PropTypes.any, +// +// /** Classes to add to the className */ +// className: PropTypes.string, +// } +// +// export default ModalDescription diff --git a/src/modules/Modal/ModalHeader.js b/src/modules/Modal/ModalHeader.js index 6141a5c556..2c3dbc4bda 100644 --- a/src/modules/Modal/ModalHeader.js +++ b/src/modules/Modal/ModalHeader.js @@ -1,44 +1,44 @@ -import React, { PropTypes } from 'react' -import classNames from 'classnames' - -import { getElementType, getUnhandledProps, META } from '../../lib' - -function ModalHeader(props) { - const { children, className } = props - - const classes = classNames( - className, - 'header' - ) - - const rest = getUnhandledProps(ModalHeader, props) - const ElementType = getElementType(ModalHeader, props) - - return ( - - {children} - - ) -} - -ModalHeader._meta = { - name: 'ModalHeader', - type: META.TYPES.MODULE, - parent: 'Modal', -} - -ModalHeader.propTypes = { - /** An element type to render as (string or function). */ - as: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func, - ]), - - /** Primary content of the modal header */ - children: PropTypes.any, - - /** Classes to add to the modal header className */ - className: PropTypes.string, -} - -export default ModalHeader +// import React, { PropTypes } from 'react' +// import classNames from 'classnames' +// +// import { getElementType, getUnhandledProps, META } from '../../lib' +// +// function ModalHeader(props) { +// const { children, className } = props +// +// const classes = classNames( +// className, +// 'header' +// ) +// +// const rest = getUnhandledProps(ModalHeader, props) +// const ElementType = getElementType(ModalHeader, props) +// +// return ( +// +// {children} +// +// ) +// } +// +// ModalHeader._meta = { +// name: 'ModalHeader', +// type: META.TYPES.MODULE, +// parent: 'Modal', +// } +// +// ModalHeader.propTypes = { +// /** An element type to render as (string or function). */ +// as: PropTypes.oneOfType([ +// PropTypes.string, +// PropTypes.func, +// ]), +// +// /** Primary content of the modal header */ +// children: PropTypes.any, +// +// /** Classes to add to the modal header className */ +// className: PropTypes.string, +// } +// +// export default ModalHeader diff --git a/src/parts/createPartFactory.js b/src/parts/createPartFactory.js new file mode 100644 index 0000000000..945bacfdef --- /dev/null +++ b/src/parts/createPartFactory.js @@ -0,0 +1,106 @@ +import cx from 'classnames' +import _ from 'lodash' +import React, { PropTypes } from 'react' + +import { META, getElementType, getUnhandledProps } from '../lib' + +// memoize component part functions +const cache = {} +const getCacheKey = (partName, defaultProps) => [ + partName, + ..._.map(defaultProps, (val, key) => `${key}:${val}`), +].join('_') + +/** + * Returns a memoized component part factory. The factory accepts defaultProps. + * It is useful for generating simple sub components (i.e. FeedDate) with consistent functionality: + * - spreads unhandled props + * - merges className + * - renders children + * - uses dynamic element type + * - provides elementType prop + * - provides childKey prop + * @param {string} partName Used as to create the displayName and className. + * @returns {function} + * @example + * // make a part factory + * const createTitlePart = createPartFactory('title') + * + * // Create a Title in some component, passing optional defaultProps... + * const Title = createTitlePart({ + * className: 'foo', + * childKey: 'unique', + * elementType: 'a', + * href='http://google.com' + * }) + * + * // Default use + * + * + * // renders + * <a href='http://google.com className='unique' /> + * + * // User's can use it like so + * <Title + * className='is merged' + * childKey='is overridden' + * elementType='h1' + * data-custom='is spread' + * > + * A customized title + * + * + * // renders + *

+ * A customized title + *

+ */ +const createPartFactory = (partName) => (defaultProps = {}) => { + // memoize component parts + const cacheKey = getCacheKey(partName, defaultProps) + const cached = cache[cacheKey] + if (cached) return cached + + function ComponentPart(props) { + const { childKey, className } = props + const classes = cx(partName, className) + const rest = getUnhandledProps(ComponentPart, props) + const ElementType = getElementType(ComponentPart, props) + + return ( + + {props.children} + + ) + } + + ComponentPart.defaultProps = defaultProps + ComponentPart.displayName = `${_.capitalize(partName)}Part` + + ComponentPart._meta = { + type: META.TYPES.PART, + name: partName, + } + + ComponentPart.propTypes = { + /** An element type to render as (string or function). */ + as: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.func, + ]), + + /** Primary content. */ + children: PropTypes.node, + + /** Classes to add to className. */ + className: PropTypes.string, + + /** React key to use if this component is used in an an array */ + childKey: PropTypes.string, + } + + cache[cacheKey] = ComponentPart + return ComponentPart +} + +export default createPartFactory diff --git a/src/parts/index.js b/src/parts/index.js new file mode 100644 index 0000000000..a161006cf1 --- /dev/null +++ b/src/parts/index.js @@ -0,0 +1,65 @@ +/** + * This file exports every component part with support for className and children. + * + * The top level API is made of components.(i.e. Button, Card, etc.) + * Sub components are used to distinguish parts of components from components. + * See "ui" class here http://semantic-ui.com/introduction/glossary.html#project-terminology. + * + * Example, a List is a component and a List.Item is a component part. + * The List.Item is not in anyway related to the Item component. + * They do not share capabilities. They only share the same name. + */ + +import createPartFactory from './createPartFactory' + +// ---------------------------------------- +// Factories +// Every create*Part factory is a function that accepts defaultProps +// ---------------------------------------- +export const createActionsPart = createPartFactory('actions') +export const createAvatarPart = createPartFactory('avatar') +export const createBarPart = createPartFactory('bar') +export const createContentPart = createPartFactory('content') +export const createDatePart = createPartFactory('date') +export const createDescriptionPart = createPartFactory('description') +export const createDividerPart = createPartFactory('divider') +export const createEventPart = createPartFactory('event') +export const createExtraPart = createPartFactory('extra') +export const createExtraContent = createPartFactory('extra content') +export const createExtraImages = createPartFactory('extra images') +export const createHeaderPart = createPartFactory('Header') +export const createImageContent = createPartFactory('image content') +export const createItemPart = createPartFactory('item') +export const createMenuPart = createPartFactory('menu') +export const createMetaPart = createPartFactory('meta') +export const createLabelPart = createPartFactory('label') +export const createSummaryPart = createPartFactory('summary') +export const createTitlePart = createPartFactory('title') +export const createUserPart = createPartFactory('user') +export const createValuePart = createPartFactory('value') + +// ---------------------------------------- +// Parts +// Default component parts +// ---------------------------------------- +export const ActionsPart = createActionsPart('actions') +export const AvatarPart = createAvatarPart('avatar') +export const BarPart = createBarPart('bar') +export const ContentPart = createContentPart('content') +export const DatePart = createDatePart('date') +export const DescriptionPart = createDescriptionPart('description') +export const DividerPart = createDividerPart('divider') +export const EventPart = createEventPart('event') +export const ExtraPart = createExtraPart('extra') +export const ExtraContent = createExtraContent('extra content') +export const ExtraImages = createExtraImages('extra images') +export const HeaderPart = createHeaderPart('Header') +export const ImageContent = createImageContent('image content') +export const ItemPart = createItemPart('item') +export const MenuPart = createMenuPart('menu') +export const MetaPart = createMetaPart('meta') +export const LabelPart = createLabelPart('label') +export const SummaryPart = createSummaryPart('summary') +export const TitlePart = createTitlePart('title') +export const UserPart = createUserPart('user') +export const ValuePart = createValuePart('value') diff --git a/src/views/Feed/Feed.js b/src/views/Feed/Feed.js index d1c07eb653..24f20dd71a 100644 --- a/src/views/Feed/Feed.js +++ b/src/views/Feed/Feed.js @@ -9,15 +9,20 @@ import { META, SUI, } from '../../lib' +import { + DatePart, + createUserPart, +} from '../../parts' + import FeedContent from './FeedContent' -import FeedDate from './FeedDate' +// import FeedDate from './FeedDate' import FeedEvent from './FeedEvent' import FeedExtra from './FeedExtra' import FeedLabel from './FeedLabel' import FeedLike from './FeedLike' import FeedMeta from './FeedMeta' import FeedSummary from './FeedSummary' -import FeedUser from './FeedUser' +// import FeedUser from './FeedUser' function Feed(props) { const { children, className, events, size } = props @@ -91,13 +96,15 @@ Feed.propTypes = { } Feed.Content = FeedContent -Feed.Date = FeedDate +// Feed.Date = FeedDate +Feed.Date = DatePart Feed.Event = FeedEvent Feed.Extra = FeedExtra Feed.Label = FeedLabel Feed.Like = FeedLike Feed.Meta = FeedMeta Feed.Summary = FeedSummary -Feed.User = FeedUser +// Feed.User = FeedUser +Feed.User = createUserPart({ elementType: 'a' }) export default Feed diff --git a/src/views/Feed/FeedContent.js b/src/views/Feed/FeedContent.js index 42647300f1..482d07e141 100644 --- a/src/views/Feed/FeedContent.js +++ b/src/views/Feed/FeedContent.js @@ -7,7 +7,9 @@ import { getUnhandledProps, META, } from '../../lib' -import FeedDate from './FeedDate' + +import { createDatePart } from '../../parts' +// import FeedDate from './FeedDate' import FeedExtra from './FeedExtra' import FeedMeta from './FeedMeta' import FeedSummary from './FeedSummary' @@ -17,10 +19,11 @@ function FeedContent(props) { const classes = cx(className, 'content') const rest = getUnhandledProps(FeedContent, props) const ElementType = getElementType(FeedContent, props) + const Date = createDatePart({ elementType: 'div' }) return ( - {date && } + {date && {date}} {summary && } {extraImages && } {extraText && } diff --git a/src/views/Feed/FeedDate.js b/src/views/Feed/FeedDate.js index a6be947f46..764593f820 100644 --- a/src/views/Feed/FeedDate.js +++ b/src/views/Feed/FeedDate.js @@ -1,49 +1,49 @@ -import cx from 'classnames' -import React, { PropTypes } from 'react' - -import { - customPropTypes, - getElementType, - getUnhandledProps, - META, -} from '../../lib' - -function FeedDate(props) { - const { children, className, date } = props - const classes = cx(className, 'date') - const rest = getUnhandledProps(FeedDate, props) - const ElementType = getElementType(FeedDate, props) - - return {children || date} -} - -FeedDate._meta = { - name: 'FeedDate', - parent: 'Feed', - type: META.TYPES.VIEW, -} - -FeedDate.propTypes = { - /** An element type to render as (string or function). */ - as: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func, - ]), - - /** Primary content of the FeedDate. Mutually exclusive with the date prop. */ - children: customPropTypes.every([ - customPropTypes.disallow(['date']), - PropTypes.node, - ]), - - /** Classes that will be added to the FeedDate className. */ - className: PropTypes.string, - - /** Shorthand for primary content of the FeedDate. Mutually exclusive with the children prop. */ - date: customPropTypes.every([ - customPropTypes.disallow(['children']), - PropTypes.string, - ]), -} - -export default FeedDate +// import cx from 'classnames' +// import React, { PropTypes } from 'react' +// +// import { +// customPropTypes, +// getElementType, +// getUnhandledProps, +// META, +// } from '../../lib' +// +// function FeedDate(props) { +// const { children, className, date } = props +// const classes = cx(className, 'date') +// const rest = getUnhandledProps(FeedDate, props) +// const ElementType = getElementType(FeedDate, props) +// +// return {children || date} +// } +// +// FeedDate._meta = { +// name: 'FeedDate', +// parent: 'Feed', +// type: META.TYPES.VIEW, +// } +// +// FeedDate.propTypes = { +// /** An element type to render as (string or function). */ +// as: PropTypes.oneOfType([ +// PropTypes.string, +// PropTypes.func, +// ]), +// +// /** Primary content of the FeedDate. Mutually exclusive with the date prop. */ +// children: customPropTypes.every([ +// customPropTypes.disallow(['date']), +// PropTypes.node, +// ]), +// +// /** Classes that will be added to the FeedDate className. */ +// className: PropTypes.string, +// +// /** Shorthand for primary content of the FeedDate. Mutually exclusive with the children prop. */ +// date: customPropTypes.every([ +// customPropTypes.disallow(['children']), +// PropTypes.string, +// ]), +// } +// +// export default FeedDate diff --git a/src/views/Feed/FeedUser.js b/src/views/Feed/FeedUser.js index c723e1624f..b6e7d7b23d 100644 --- a/src/views/Feed/FeedUser.js +++ b/src/views/Feed/FeedUser.js @@ -1,53 +1,53 @@ -import cx from 'classnames' -import React, { PropTypes } from 'react' - -import { - customPropTypes, - getElementType, - getUnhandledProps, - META, -} from '../../lib' - -function FeedUser(props) { - const { children, className, user } = props - const classes = cx(className, 'user') - const rest = getUnhandledProps(FeedUser, props) - const ElementType = getElementType(FeedUser, props) - - return {children || user} -} - -FeedUser._meta = { - name: 'FeedUser', - parent: 'Feed', - type: META.TYPES.VIEW, -} - -FeedUser.propTypes = { - /** An element type to render as (string or function). */ - as: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func, - ]), - - /** Primary content of the FeedUser. */ - children: customPropTypes.every([ - customPropTypes.disallow(['user']), - PropTypes.node, - ]), - - /** Classes that will be added to the FeedUser className. */ - className: PropTypes.string, - - /** Shorthand for primary content of the FeedUser. Mutually exclusive with the children prop. */ - user: customPropTypes.every([ - customPropTypes.disallow(['children']), - PropTypes.string, - ]), -} - -FeedUser.defaultProps = { - as: 'a', -} - -export default FeedUser +// import cx from 'classnames' +// import React, { PropTypes } from 'react' +// +// import { +// customPropTypes, +// getElementType, +// getUnhandledProps, +// META, +// } from '../../lib' +// +// function FeedUser(props) { +// const { children, className, user } = props +// const classes = cx(className, 'user') +// const rest = getUnhandledProps(FeedUser, props) +// const ElementType = getElementType(FeedUser, props) +// +// return {children || user} +// } +// +// FeedUser._meta = { +// name: 'FeedUser', +// parent: 'Feed', +// type: META.TYPES.VIEW, +// } +// +// FeedUser.propTypes = { +// /** An element type to render as (string or function). */ +// as: PropTypes.oneOfType([ +// PropTypes.string, +// PropTypes.func, +// ]), +// +// /** Primary content of the FeedUser. */ +// children: customPropTypes.every([ +// customPropTypes.disallow(['user']), +// PropTypes.node, +// ]), +// +// /** Classes that will be added to the FeedUser className. */ +// className: PropTypes.string, +// +// /** Shorthand for primary content of the FeedUser. Mutually exclusive with the children prop. */ +// user: customPropTypes.every([ +// customPropTypes.disallow(['children']), +// PropTypes.string, +// ]), +// } +// +// FeedUser.defaultProps = { +// as: 'a', +// } +// +// export default FeedUser