@@ -28,46 +28,43 @@ import { Component } from 'react'
28
28
29
29
const getDefaultPropName = ( prop ) => `default${ prop [ 0 ] . toUpperCase ( ) + prop . slice ( 1 ) } `
30
30
31
+ /**
32
+ * Return the auto controlled state value for a give prop. The initial value is chosen in this order:
33
+ * - default props
34
+ * - then, regular props
35
+ * - then, `checked` defaults to false
36
+ * - then, `value` defaults to '' or [] if props.multiple
37
+ * - else, undefined
38
+ *
39
+ * @param {object } props A props object
40
+ * @param {string } propName A prop name
41
+ * @param {boolean } [includeDefaultProps=false] Whether or not to heed the default prop value
42
+ */
43
+ export const getAutoControlledStateValue = ( props , propName , includeDefaultProps = false ) => {
44
+ const defaultPropName = getDefaultPropName ( propName )
45
+ const prop = props [ propName ]
46
+ const defaultProp = props [ defaultPropName ]
47
+
48
+ const hasProp = prop !== undefined
49
+ const hasDefaultProp = defaultProp !== undefined
50
+
51
+ // defaultProps & props
52
+ if ( includeDefaultProps && ! hasProp && hasDefaultProp ) return defaultProp
53
+ if ( hasProp ) return prop
54
+
55
+ // React doesn't allow changing from uncontrolled to controlled components,
56
+ // default checked/value if they were not present.
57
+ if ( propName === 'checked' ) return false
58
+ if ( propName === 'value' ) return props . multiple ? [ ] : ''
59
+
60
+ // otherwise, undefined
61
+ }
62
+
31
63
export default class AutoControlledComponent extends Component {
32
64
componentWillMount ( ) {
33
65
if ( super . componentWillMount ) super . componentWillMount ( )
34
66
const { autoControlledProps } = this . constructor
35
67
36
- // Auto controlled props are copied to state.
37
- // Set initial state by copying auto controlled props to state.
38
- // Also look for the default prop for any auto controlled props (foo => defaultFoo)
39
- // so we can set initial values from defaults.
40
- this . state = _ . transform ( autoControlledProps , ( res , prop ) => {
41
- const defaultPropName = getDefaultPropName ( prop )
42
-
43
- // try to set initial state in this order:
44
- // - default props
45
- // - then, regular props
46
- // - then, `checked` defaults to false
47
- // - then, `value` defaults to null
48
- // React doesn't allow changing from uncontrolled to controlled components
49
- // this is why we default checked/value if they are not present.
50
- if ( _ . has ( this . props , defaultPropName ) ) {
51
- res [ prop ] = this . props [ defaultPropName ]
52
- } else if ( _ . has ( this . props , prop ) ) {
53
- res [ prop ] = this . props [ prop ]
54
- } else if ( prop === 'checked' ) {
55
- res [ prop ] = false
56
- } else if ( prop === 'value' ) {
57
- res [ prop ] = this . props . multiple ? [ ] : '' // eslint-disable-line react/prop-types
58
- }
59
-
60
- if ( process . env . NODE_ENV !== 'production' ) {
61
- const { name } = this . constructor
62
- // prevent defaultFoo={} along side foo={}
63
- if ( defaultPropName in this . props && prop in this . props ) {
64
- console . error (
65
- `${ name } prop "${ prop } " is auto controlled. Specify either ${ defaultPropName } or ${ prop } , but not both.`
66
- )
67
- }
68
- }
69
- } , { } )
70
-
71
68
if ( process . env . NODE_ENV !== 'production' ) {
72
69
const { defaultProps, name, propTypes } = this . constructor
73
70
// require static autoControlledProps
@@ -116,14 +113,48 @@ export default class AutoControlledComponent extends Component {
116
113
] . join ( ' ' ) )
117
114
}
118
115
}
116
+
117
+ // Auto controlled props are copied to state.
118
+ // Set initial state by copying auto controlled props to state.
119
+ // Also look for the default prop for any auto controlled props (foo => defaultFoo)
120
+ // so we can set initial values from defaults.
121
+ this . state = autoControlledProps . reduce ( ( acc , prop ) => {
122
+ acc [ prop ] = getAutoControlledStateValue ( this . props , prop , true )
123
+
124
+ if ( process . env . NODE_ENV !== 'production' ) {
125
+ const defaultPropName = getDefaultPropName ( prop )
126
+ const { name } = this . constructor
127
+ // prevent defaultFoo={} along side foo={}
128
+ if ( defaultPropName in this . props && prop in this . props ) {
129
+ console . error (
130
+ `${ name } prop "${ prop } " is auto controlled. Specify either ${ defaultPropName } or ${ prop } , but not both.`
131
+ )
132
+ }
133
+ }
134
+
135
+ return acc
136
+ } , { } )
119
137
}
120
138
121
139
componentWillReceiveProps ( nextProps ) {
122
140
if ( super . componentWillReceiveProps ) super . componentWillReceiveProps ( nextProps )
141
+ const { autoControlledProps } = this . constructor
142
+
143
+ // Solve the next state for autoControlledProps
144
+ const newState = autoControlledProps . reduce ( ( acc , prop ) => {
145
+ const isNextUndefined = _ . isUndefined ( nextProps [ prop ] )
146
+ const propWasRemoved = ! _ . isUndefined ( this . props [ prop ] ) && isNextUndefined
147
+
148
+ // if next is defined then use its value
149
+ if ( ! isNextUndefined ) acc [ prop ] = nextProps [ prop ]
123
150
124
- // props always win, update state with all auto controlled prop
125
- const newState = _ . pick ( nextProps , this . constructor . autoControlledProps )
126
- if ( ! _ . isEmpty ( newState ) ) this . setState ( newState )
151
+ // reinitialize state for props just removed / set undefined
152
+ else if ( propWasRemoved ) acc [ prop ] = getAutoControlledStateValue ( nextProps , prop )
153
+
154
+ return acc
155
+ } , { } )
156
+
157
+ if ( Object . keys ( newState ) . length > 0 ) this . setState ( newState )
127
158
}
128
159
129
160
/**
@@ -147,12 +178,19 @@ export default class AutoControlledComponent extends Component {
147
178
}
148
179
}
149
180
150
- // pick auto controlled props
151
- // omit props from parent
152
- let newState = _ . omit ( _ . pick ( maybeState , autoControlledProps ) , _ . keys ( this . props ) )
181
+ let newState = Object . keys ( maybeState ) . reduce ( ( acc , prop ) => {
182
+ // ignore props defined by the parent
183
+ if ( this . props [ prop ] !== undefined ) return acc
184
+
185
+ // ignore props not listed in auto controlled props
186
+ if ( autoControlledProps . indexOf ( prop ) === - 1 ) return acc
187
+
188
+ acc [ prop ] = maybeState [ prop ]
189
+ return acc
190
+ } , { } )
153
191
154
192
if ( state ) newState = { ...newState , ...state }
155
193
156
- if ( ! _ . isEmpty ( newState ) ) this . setState ( newState )
194
+ if ( Object . keys ( newState ) . length > 0 ) this . setState ( newState )
157
195
}
158
196
}
0 commit comments