Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

Commit c60dfd5

Browse files
authored
feat: add generateKey option to factories (#3)
* feat: add generateKey option to factories * chore(codecov): ignore test directory * chore(circle): do no install puppeteer
1 parent 6b4f844 commit c60dfd5

File tree

9 files changed

+135
-93
lines changed

9 files changed

+135
-93
lines changed

.circleci/config.yml

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,6 @@ jobs:
1010
environment:
1111
TZ: "/usr/share/zoneinfo/America/Los_Angeles"
1212
steps:
13-
# Chrome HeadlessBrowser is missing deps on Debian, see:
14-
# https://github.com/GoogleChrome/puppeteer/issues/290
15-
- run:
16-
name: Update Puppeteer Dependencies
17-
command: |
18-
sudo apt-get update
19-
sudo apt-get install --yes --quiet gconf-service libasound2 libatk1.0-0 libc6 \
20-
libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 \
21-
libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 \
22-
libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \
23-
libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation \
24-
libappindicator1 libnss3 lsb-release xdg-utils wget
2513
- run:
2614
name: Update yarn
2715
command: |

codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ coverage:
22
ignore:
33
- docs/*
44
- src/lib/*
5+
- test/*

specifications/shorthand-api.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,39 @@
55

66

77
- [Why?](#why)
8+
- [DOM Structure](#dom-structure)
9+
- [A Moving Target](#a-moving-target)
10+
- [Ownership](#ownership)
11+
- [Intuition & Effort](#intuition--effort)
812
- [Proposals](#proposals)
9-
- [[Goal]](#goal)
13+
- [Goal](#goal)
1014

1115
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
1216

1317
## Why?
1418

15-
## Proposals
19+
### DOM Structure
20+
21+
In traditional UI libraries, developers are required to memorize or copy and paste specific various brittle HTML structures to use components. The developer should be focused on a higher level of describing features.
22+
23+
#### A Moving Target
24+
25+
These structures are often required for the component to work correctly. This makes them brittle and a common source of bugs. This is only exacerbated by the fact that different variations of the same component often require slightly different structures.
26+
27+
The component should own and isolate the brittle nature of required markup.
28+
29+
#### Ownership
1630

17-
### [Goal]
31+
Developers require components to have certain styles and behaviors. Components may require a specific DOM structure to achieve those styles and behaviors. Therefore, component DOM structure is the concern and responsibility of the component, not the user.
32+
33+
### Intuition & Effort
34+
35+
When describing a component to another human we use natural language, we don't speak HTML. When defining a component via an API we should strive to use the same natural language. This frees the developer's mind to spend its effort creating actual features opposed to creating implementations of features.
36+
37+
Providing a high level API of natural language allows developers to use intuition and minimal effort to create features.
38+
39+
## Proposals
1840

19-
[Description]
41+
See the [docs][1].
2042

21-
[Implementation]
43+
[1]: https://stardust-ui.github.io/react

src/components/Accordion/Accordion.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ class Accordion extends AutoControlledComponent<any, any> {
124124

125125
children.push(
126126
AccordionTitle.create(title, {
127-
autoGenerateKey: true,
127+
generateKey: true,
128128
defaultProps: { active, index },
129129
overrideProps: this.handleTitleOverrides,
130130
}),
@@ -133,7 +133,7 @@ class Accordion extends AutoControlledComponent<any, any> {
133133
AccordionContent.create(
134134
{ content },
135135
{
136-
autoGenerateKey: true,
136+
generateKey: true,
137137
defaultProps: { active },
138138
},
139139
),

src/components/Header/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class Header extends UIComponent<any, any> {
5353
)
5454
}
5555

56-
const subheaderElement = HeaderSubheader.create(subheader, { autoGenerateKey: false })
56+
const subheaderElement = HeaderSubheader.create(subheader, { generateKey: false })
5757

5858
return (
5959
<ElementType {...rest} className={classes.root}>

src/lib/factories.tsx

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,64 @@ import _ from 'lodash'
22
import cx from 'classnames'
33
import React, { cloneElement, isValidElement } from 'react'
44

5+
interface IProps {
6+
[key: string]: any
7+
}
8+
9+
type ShorthandFunction = (
10+
Component: React.ReactType,
11+
props: IProps,
12+
children: any,
13+
) => React.ReactElement<IProps>
14+
type ShorthandValue = string & number & IProps & React.ReactElement<IProps> & ShorthandFunction
15+
type MapValueToProps = (value: ShorthandValue) => IProps
16+
17+
interface ICreateShorthandOptions {
18+
/** Default props object */
19+
defaultProps?: IProps
20+
21+
/** Override props object or function (called with regular props) */
22+
overrideProps?: IProps | ((props: IProps) => IProps)
23+
24+
/** Whether or not automatic key generation is allowed */
25+
generateKey?: boolean
26+
}
27+
const CREATE_SHORTHAND_DEFAULT_OPTIONS: ICreateShorthandOptions = {
28+
defaultProps: {},
29+
overrideProps: {},
30+
generateKey: true,
31+
}
32+
533
// ============================================================
634
// Factories
735
// ============================================================
836

9-
/**
10-
* A more robust React.createElement. It can create elements from primitive values.
11-
*
12-
* @param {function|string} Component A ReactClass or string
13-
* @param {function} mapValueToProps A function that maps a primitive value to the Component props
14-
* @param {string|object|function} val The value to create a ReactElement from
15-
* @param {Object} [options={}]
16-
* @param {object} [options.defaultProps={}] Default props object
17-
* @param {object|function} [options.overrideProps={}] Override props object or function (called with regular props)
18-
* @returns {object|null}
19-
*/
37+
/** A more robust React.createElement. It can create elements from primitive values. */
2038
export function createShorthand(
21-
Component: any,
22-
mapValueToProps: any,
23-
val?: any,
24-
options: any = {},
25-
) {
39+
Component: React.ReactType,
40+
mapValueToProps: MapValueToProps,
41+
value?: ShorthandValue,
42+
options: ICreateShorthandOptions = CREATE_SHORTHAND_DEFAULT_OPTIONS,
43+
): React.ReactElement<IProps> | null {
2644
if (typeof Component !== 'function' && typeof Component !== 'string') {
2745
throw new Error('createShorthand() Component must be a string or function.')
2846
}
2947
// short circuit noop values
30-
if (_.isNil(val) || _.isBoolean(val)) return null
48+
if (_.isNil(value) || typeof value === 'boolean') return null
3149

32-
const valIsString = _.isString(val)
33-
const valIsNumber = _.isNumber(val)
34-
const valIsFunction = _.isFunction(val)
35-
const valIsReactElement = isValidElement(val)
36-
const valIsPropsObject = _.isPlainObject(val)
37-
const valIsPrimitiveValue = valIsString || valIsNumber || _.isArray(val)
50+
const valIsPrimitive = typeof value === 'string' || typeof value === 'number'
51+
const valIsPropsObject = _.isPlainObject(value)
52+
const valIsReactElement = isValidElement(value)
53+
const valIsFunction = typeof value === 'function'
3854

3955
// unhandled type return null
40-
if (!valIsFunction && !valIsReactElement && !valIsPropsObject && !valIsPrimitiveValue) {
56+
if (!valIsPrimitive && !valIsPropsObject && !valIsReactElement && !valIsFunction) {
4157
if (process.env.NODE_ENV !== 'production') {
4258
console.error(
4359
[
44-
'Shorthand value must be a string|number|array|object|ReactElement|function.',
45-
' Use null|undefined|boolean for none',
46-
` Received ${typeof val}.`,
60+
'Shorthand value must be a string|number|object|ReactElement|function.',
61+
' Use null|undefined|boolean for none.',
62+
` Received: ${value}`,
4763
].join(''),
4864
)
4965
}
@@ -57,15 +73,17 @@ export function createShorthand(
5773

5874
// User's props
5975
const usersProps =
60-
(valIsReactElement && val.props) ||
61-
(valIsPropsObject && val) ||
62-
(valIsPrimitiveValue && mapValueToProps(val))
76+
(valIsReactElement && value.props) ||
77+
(valIsPropsObject && value) ||
78+
(valIsPrimitive && mapValueToProps(value)) ||
79+
{}
6380

6481
// Override props
65-
let { overrideProps = {} } = options
66-
overrideProps = _.isFunction(overrideProps)
67-
? overrideProps({ ...defaultProps, ...usersProps })
68-
: overrideProps
82+
let { overrideProps } = options
83+
overrideProps =
84+
typeof overrideProps === 'function'
85+
? overrideProps({ ...defaultProps, ...usersProps })
86+
: overrideProps || {}
6987

7088
// Merge props
7189
const props = { ...defaultProps, ...usersProps, ...overrideProps }
@@ -88,25 +106,26 @@ export function createShorthand(
88106
// ----------------------------------------
89107
// Get key
90108
// ----------------------------------------
109+
const { generateKey = true } = options
91110

92111
// Use key or generate key
93-
if (_.isNil(props.key) && (valIsString || valIsNumber)) {
112+
if (generateKey && _.isNil(props.key) && valIsPrimitive) {
94113
// use string/number shorthand values as the key
95-
props.key = val
114+
props.key = value
96115
}
97116

98117
// ----------------------------------------
99118
// Create Element
100119
// ----------------------------------------
101120

102121
// Clone ReactElements
103-
if (valIsReactElement) return cloneElement(val, props)
122+
if (valIsReactElement) return cloneElement(value, props)
104123

105124
// Create ReactElements from built up props
106-
if (valIsPrimitiveValue || valIsPropsObject) return <Component {...props} />
125+
if (valIsPrimitive || valIsPropsObject) return <Component {...props} />
107126

108127
// Call functions with args similar to createElement()
109-
if (valIsFunction) return val(Component, props, props.children)
128+
if (valIsFunction) return value(Component, props, props.children)
110129
}
111130

112131
// ============================================================

src/lib/renderComponent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import getUnhandledProps from './getUnhandledProps'
88
import callable from './callable'
99

1010
export interface IRenderResultConfig<P> {
11-
ElementType: React.ComponentType<P>
11+
ElementType: React.ReactType<P>
1212
rest: { [key: string]: any }
1313
classes: { [key: string]: string }
1414
}

test/specs/commonTests/implementsShorthandProp.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const shorthandComponentName = ShorthandComponent => {
2525
* @param {string|function} options.ShorthandComponent The component that should be rendered from the shorthand value.
2626
* @param {boolean} [options.alwaysPresent] Whether or not the shorthand exists by default.
2727
* @param {boolean} [options.assertExactMatch] Selects an assertion method, `contain` will be used if true.
28+
* @param {boolean} [options.generateKey=false] Whether or not automatic key generation is allowed for the shorthand component.
2829
* @param {function} options.mapValueToProps A function that maps a primitive value to the Component props.
2930
* @param {Object} [options.requiredProps={}] Props required to render the component.
3031
* @param {Object} [options.shorthandDefaultProps] Default props for the shorthand component.
@@ -34,6 +35,7 @@ export default (Component, options: any = {}) => {
3435
const {
3536
alwaysPresent,
3637
assertExactMatch = true,
38+
generateKey = false,
3739
mapValueToProps,
3840
propKey,
3941
ShorthandComponent,
@@ -52,13 +54,21 @@ export default (Component, options: any = {}) => {
5254

5355
const name = shorthandComponentName(ShorthandComponent)
5456
const assertValidShorthand = value => {
55-
const shorthandElement = createShorthand(ShorthandComponent, mapValueToProps, value, {
57+
const expectedShorthandElement = createShorthand(ShorthandComponent, mapValueToProps, value, {
5658
defaultProps: shorthandDefaultProps,
5759
overrideProps: shorthandOverrideProps,
60+
generateKey,
5861
})
5962
const element = createElement(Component, { ...requiredProps, [propKey]: value })
63+
const wrapper = shallow(element)
6064

61-
shallow(element).should[assertMethod](shorthandElement)
65+
wrapper.should[assertMethod](expectedShorthandElement)
66+
67+
// Enzyme's .key() method is not consistent with React for elements with
68+
// no key (`undefined` vs `null`), so use the underlying element instead
69+
// Will fail if more than one element of this type is found
70+
const shorthandElement = wrapper.find(ShorthandComponent).getElement()
71+
expect(shorthandElement.key).to.equal(expectedShorthandElement.key, "key doesn't match")
6272
}
6373

6474
if (alwaysPresent || (Component.defaultProps && Component.defaultProps[propKey])) {

0 commit comments

Comments
 (0)