/* * The MIT License (MIT) * * Copyright (c) 2015 - present Instructure, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ import { ComponentType, ReactElement, Children as ReactChildren } from 'react' import { makeRequirable } from './makeRequirable' const Children = { /** * Validate that the children of a component are one of the specified types. * * ```js * import { Children } from '@instructure/ui-prop-types' * * class Example extends Component { * static propTypes = { * children: Children.oneOf([Foo, Bar, Baz]) * } * * render () { * return
{this.props.children}
* } * } * ``` * * This will allow children such as: * * ```jsx * * * * ``` * * OR * * ```jsx * * * * * ``` * * But will fail on something like: * * ```jsx * *

Example

* *
* ``` * @returns {function} A validator function that returns Error if validation failed */ oneOf(validTypes: (string | ComponentType | null)[]) { function validator( props: Record, propName: string, componentName: string ) { const children = ReactChildren.toArray(props[propName]) const validTypeNames = validTypes.map( (type: string | React.ComponentType | null) => type ? getDisplayName(type) : type ) for (let i = 0; i < children.length; i++) { const child = children[i] if (child && (child as ReactElement).type) { const childName = getDisplayName((child as ReactElement).type) if (validTypeNames.indexOf(childName) < 0) { return new Error( `Expected one of ${validTypeNames.join( ', ' )} in ${componentName} but found '${childName}'` ) } } else if (child) { return new Error( `Expected one of ${validTypeNames.join( ', ' )} in ${componentName} but found an element with unknown type: ${child}` ) } } return null } validator.isRequired = makeRequirable(validator) return validator }, /** * Ensures that there is exactly one of each specified child * * ```js * import { Children } from '@instructure/ui-prop-types' * * class Example extends Component { * static propTypes = { * children: Children.oneOfEach([Foo, Bar, Baz]) * } * * render () { * return
{this.props.children}
* } * } * ``` * * This will enforce the following: * * ```jsx * * * * * * ``` * An error will be thrown * - If any of the children are not provided (ex. Foo, Bar, but missing Baz) * - If multiple children of the same type are provided (ex. Foo, Foo, Bar, and Baz) * * @param {Array} validTypes - Array of child types * @returns {function} A validator function that returns error if validation * failed, null otherwise */ oneOfEach(validTypes: (string | ComponentType)[]) { return function ( props: Record, propName: string, componentName: string ) { const children = ReactChildren.toArray(props[propName]) const instanceCount: Record = {} const validTypeNames = validTypes.map( (type: string | React.ComponentType) => { const typeName = getDisplayName(type) instanceCount[typeName] = 0 return typeName } ) for (let i = 0; i < children.length; i++) { const child = children[i] if (child && (child as ReactElement).type) { const childName = getDisplayName((child as ReactElement).type) if (validTypeNames.indexOf(childName) < 0) { return new Error( `Expected one of ${validTypeNames.join( ', ' )} in ${componentName} but found '${childName}'` ) } instanceCount[childName] = (instanceCount[childName] || 0) + 1 } else if (child) { return new Error( `Expected one of ${validTypeNames.join( ', ' )} in ${componentName} but found an element of unknown type: ${child}` ) } } const errors: string[] = [] Object.keys(instanceCount).forEach((childName) => { if (instanceCount[childName] > 1) { errors.push( `${instanceCount[childName]} children of type ${childName}` ) } if (instanceCount[childName] === 0) { errors.push(`0 children of type ${childName}`) } }) if (errors.length > 0) { return new Error( `Expected exactly one of each ${validTypeNames.join( ', ' )} in ${componentName} but found:${errors.join('\n')}` ) } return null } }, /** * Validate the type and order of children for a component. * * ```js * import { Children } from '@instructure/ui-prop-types' * * class Example extends Component { * static propTypes = { * children: Children.enforceOrder([Foo, Bar, Baz]) * } * * render () { * return
{this.props.children}
* } * } * ``` * * This will enforce the following: * * ```jsx * * * * * * ``` * * This validator will also allow various permutations of the order. * * ```js * import { Children } from '@instructure/ui-prop-types' * * class Example extends Component { * static propTypes = { * children: Children.enforceOrder( * [Foo, Bar, Baz], * [Foo, Bar], * [Bar, Baz], * ) * } * * render () { * return
{this.props.children}
* } * } * ``` * * This will enforce one of the following: * * ```jsx * * * * * * ``` * * OR * * ```jsx * * * * * ``` * * OR * * ```jsx * * * * * ``` * * @param {...Array} validTypeGroups One or more Arrays of valid types * @returns {function} A validator function that returns error if validation * failed, null otherwise */ enforceOrder(...validTypeGroups: (string | ComponentType)[][]) { function validateTypes(childNames: string[], typeNames: string[]) { for (let i = 0; i < childNames.length; i++) { if (childNames[i] !== typeNames[i]) { return false } } return true } function formatGroupTypes( componentName: string, typeGroups: (string | ComponentType)[][] ) { return typeGroups .map((types: (string | ComponentType)[]) => formatTypes(componentName, types) ) .join('\n\n') } function formatTypes( componentName: string, types: (string | ComponentType)[] ) { const children = types .map((type: string | ComponentType) => { if (type) { return getDisplayName(type) } else { return '??' } }) .map((name) => ` <${name} />`) .join('\n') return `<${componentName}>\n${children}\n` } function validator( props: Record, propName: string, componentName: string ) { const childNames = ReactChildren.toArray(props[propName]).map((child) => { if (child && (child as ReactElement).type) { return getDisplayName((child as ReactElement).type) } else if (child) { return null } return }) // Validate each group, if any of them are valid we're done for (let i = 0; i < validTypeGroups.length; i++) { const validTypeNames = validTypeGroups[i].map( (type: string | React.ComponentType) => { if (type) { return getDisplayName(type) } else { return '??' } } ) if (validateTypes(childNames as string[], validTypeNames)) { return null } } // If we make it through the loop then children are not valid return new Error(`Expected children of ${componentName} in one of the following formats: ${formatGroupTypes(componentName, validTypeGroups)} Instead of: ${formatTypes(componentName, childNames as string[])}`) } validator.isRequired = makeRequirable(validator) return validator } } // TODO: Remove when we further break up ui-utils and bringing this in no longer // creates a circular dep const getDisplayName = (Component: string | ComponentType): string => { return typeof Component === 'string' ? Component : Component.displayName || Component.name } export default Children export { Children }