Skip to content

Commit e4afa8f

Browse files
authored
feat(Input): add onChange(e, props) (#846)
* feat(Input): add onChange(e, props) * test(Input): add onChange(e, props) test
1 parent 87b8d49 commit e4afa8f

2 files changed

Lines changed: 176 additions & 136 deletions

File tree

src/elements/Input/Input.js

Lines changed: 150 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import _ from 'lodash'
2-
import React, { PropTypes } from 'react'
2+
import React, { Component, PropTypes } from 'react'
33
import cx from 'classnames'
44

55
import {
@@ -42,91 +42,7 @@ export const htmlInputPropNames = [
4242
'value',
4343
]
4444

45-
/**
46-
* An Input is a field used to elicit a response from a user
47-
* @see Button
48-
* @see Form
49-
* @see Icon
50-
* @see Label
51-
*/
52-
function Input(props) {
53-
const {
54-
action,
55-
actionPosition,
56-
children,
57-
className,
58-
disabled,
59-
error,
60-
focus,
61-
fluid,
62-
icon,
63-
iconPosition,
64-
inverted,
65-
label,
66-
labelPosition,
67-
loading,
68-
size,
69-
type,
70-
input,
71-
transparent,
72-
} = props
73-
74-
const classes = cx(
75-
'ui',
76-
size,
77-
useValueAndKey(actionPosition, 'action') || useKeyOnly(action, 'action'),
78-
useKeyOnly(disabled, 'disabled'),
79-
useKeyOnly(error, 'error'),
80-
useKeyOnly(focus, 'focus'),
81-
useKeyOnly(fluid, 'fluid'),
82-
useKeyOnly(inverted, 'inverted'),
83-
useValueAndKey(labelPosition, 'labeled') || useKeyOnly(label, 'labeled'),
84-
useKeyOnly(loading, 'loading'),
85-
useKeyOnly(transparent, 'transparent'),
86-
useValueAndKey(iconPosition, 'icon') || useKeyOnly(icon, 'icon'),
87-
className,
88-
'input',
89-
)
90-
const unhandled = getUnhandledProps(Input, props)
91-
92-
const rest = _.omit(unhandled, htmlInputPropNames)
93-
const htmlInputProps = _.pick(props, htmlInputPropNames)
94-
const ElementType = getElementType(Input, props)
95-
96-
if (children) {
97-
return <ElementType {...rest} className={classes}>{children}</ElementType>
98-
}
99-
100-
const actionElement = Button.create(action, elProps => ({
101-
className: cx(
102-
// all action components should have the button className
103-
!_.includes(elProps.className, 'button') && 'button',
104-
),
105-
}))
106-
const iconElement = Icon.create(icon)
107-
const labelElement = Label.create(label, elProps => ({
108-
className: cx(
109-
// all label components should have the label className
110-
!_.includes(elProps.className, 'label') && 'label',
111-
// add 'left|right corner'
112-
_.includes(labelPosition, 'corner') && labelPosition,
113-
),
114-
}))
115-
116-
return (
117-
<ElementType {...rest} className={classes}>
118-
{actionPosition === 'left' && actionElement}
119-
{iconPosition === 'left' && iconElement}
120-
{labelPosition !== 'right' && labelElement}
121-
{createHTMLInput(input || type, htmlInputProps)}
122-
{actionPosition !== 'left' && actionElement}
123-
{iconPosition !== 'left' && iconElement}
124-
{labelPosition === 'right' && labelElement}
125-
</ElementType>
126-
)
127-
}
128-
129-
Input._meta = {
45+
const _meta = {
13046
name: 'Input',
13147
type: META.TYPES.ELEMENT,
13248
props: {
@@ -137,73 +53,173 @@ Input._meta = {
13753
},
13854
}
13955

140-
Input.defaultProps = {
141-
type: 'text',
142-
}
56+
/**
57+
* An Input is a field used to elicit a response from a user
58+
* @see Button
59+
* @see Form
60+
* @see Icon
61+
* @see Label
62+
*/
63+
class Input extends Component {
64+
static propTypes = {
65+
/** An element type to render as (string or function). */
66+
as: customPropTypes.as,
67+
68+
/** An Input can be formatted to alert the user to an action they may perform */
69+
action: PropTypes.oneOfType([
70+
PropTypes.bool,
71+
customPropTypes.itemShorthand,
72+
]),
14373

144-
Input.propTypes = {
145-
/** An element type to render as (string or function). */
146-
as: customPropTypes.as,
74+
/** An action can appear along side an Input on the left or right */
75+
actionPosition: PropTypes.oneOf(_meta.props.actionPosition),
14776

148-
/** An Input can be formatted to alert the user to an action they may perform */
149-
action: PropTypes.oneOfType([
150-
PropTypes.bool,
151-
customPropTypes.itemShorthand,
152-
]),
77+
/** Primary content. */
78+
children: PropTypes.node,
15379

154-
/** An action can appear along side an Input on the left or right */
155-
actionPosition: PropTypes.oneOf(Input._meta.props.actionPosition),
80+
/** Additional classes. */
81+
className: PropTypes.string,
15682

157-
/** Primary content. */
158-
children: PropTypes.node,
83+
/** An Input field can show that it is disabled */
84+
disabled: PropTypes.bool,
15985

160-
/** Additional classes. */
161-
className: PropTypes.string,
86+
/** An Input field can show the data contains errors */
87+
error: PropTypes.bool,
16288

163-
/** An Input field can show that it is disabled */
164-
disabled: PropTypes.bool,
89+
/** An Input field can show a user is currently interacting with it */
90+
focus: PropTypes.bool,
16591

166-
/** An Input field can show the data contains errors */
167-
error: PropTypes.bool,
92+
/** Take on the size of it's container */
93+
fluid: PropTypes.bool,
16894

169-
/** An Input field can show a user is currently interacting with it */
170-
focus: PropTypes.bool,
95+
/** Optional Icon to display inside the Input */
96+
icon: PropTypes.oneOfType([
97+
PropTypes.bool,
98+
customPropTypes.itemShorthand,
99+
]),
171100

172-
/** Take on the size of it's container */
173-
fluid: PropTypes.bool,
101+
/** An Icon can appear inside an Input on the left or right */
102+
iconPosition: PropTypes.oneOf(_meta.props.iconPosition),
174103

175-
/** Optional Icon to display inside the Input */
176-
icon: PropTypes.oneOfType([
177-
PropTypes.bool,
178-
customPropTypes.itemShorthand,
179-
]),
104+
/** Format to appear on dark backgrounds */
105+
inverted: PropTypes.bool,
180106

181-
/** An Icon can appear inside an Input on the left or right */
182-
iconPosition: PropTypes.oneOf(Input._meta.props.iconPosition),
107+
/** Shorthand for creating the HTML Input */
108+
input: customPropTypes.itemShorthand,
183109

184-
/** Format to appear on dark backgrounds */
185-
inverted: PropTypes.bool,
110+
/** Optional Label to display along side the Input */
111+
label: customPropTypes.itemShorthand,
186112

187-
/** Shorthand for creating the HTML Input */
188-
input: customPropTypes.itemShorthand,
113+
/** A Label can appear outside an Input on the left or right */
114+
labelPosition: PropTypes.oneOf(_meta.props.labelPosition),
189115

190-
/** Optional Label to display along side the Input */
191-
label: customPropTypes.itemShorthand,
116+
/** An Icon Input field can show that it is currently loading data */
117+
loading: PropTypes.bool,
192118

193-
/** A Label can appear outside an Input on the left or right */
194-
labelPosition: PropTypes.oneOf(Input._meta.props.labelPosition),
119+
/** Called with (e, props) on change. */
120+
onChange: PropTypes.func,
195121

196-
/** An Icon Input field can show that it is currently loading data */
197-
loading: PropTypes.bool,
122+
/** An Input can vary in size */
123+
size: PropTypes.oneOf(_meta.props.size),
198124

199-
/** An Input can vary in size */
200-
size: PropTypes.oneOf(Input._meta.props.size),
125+
/** Transparent Input has no background */
126+
transparent: PropTypes.bool,
201127

202-
/** Transparent Input has no background */
203-
transparent: PropTypes.bool,
128+
/** The HTML input type */
129+
type: PropTypes.string,
130+
}
204131

205-
/** The HTML input type */
206-
type: PropTypes.string,
132+
static defaultProps = {
133+
type: 'text',
134+
}
135+
136+
static _meta = _meta
137+
138+
handleChange = (e) => {
139+
const { onChange } = this.props
140+
if (onChange) onChange(e, this.props)
141+
}
142+
143+
render() {
144+
const {
145+
action,
146+
actionPosition,
147+
children,
148+
className,
149+
disabled,
150+
error,
151+
focus,
152+
fluid,
153+
icon,
154+
iconPosition,
155+
inverted,
156+
label,
157+
labelPosition,
158+
loading,
159+
onChange,
160+
size,
161+
type,
162+
input,
163+
transparent,
164+
} = this.props
165+
166+
const classes = cx(
167+
'ui',
168+
size,
169+
useValueAndKey(actionPosition, 'action') || useKeyOnly(action, 'action'),
170+
useKeyOnly(disabled, 'disabled'),
171+
useKeyOnly(error, 'error'),
172+
useKeyOnly(focus, 'focus'),
173+
useKeyOnly(fluid, 'fluid'),
174+
useKeyOnly(inverted, 'inverted'),
175+
useValueAndKey(labelPosition, 'labeled') || useKeyOnly(label, 'labeled'),
176+
useKeyOnly(loading, 'loading'),
177+
useKeyOnly(transparent, 'transparent'),
178+
useValueAndKey(iconPosition, 'icon') || useKeyOnly(icon, 'icon'),
179+
className,
180+
'input',
181+
)
182+
const unhandled = getUnhandledProps(Input, this.props)
183+
184+
const rest = _.omit(unhandled, htmlInputPropNames)
185+
186+
const htmlInputProps = _.pick(this.props, htmlInputPropNames)
187+
if (onChange) htmlInputProps.onChange = this.handleChange
188+
189+
const ElementType = getElementType(Input, this.props)
190+
191+
if (children) {
192+
return <ElementType {...rest} className={classes}>{children}</ElementType>
193+
}
194+
195+
const actionElement = Button.create(action, elProps => ({
196+
className: cx(
197+
// all action components should have the button className
198+
!_.includes(elProps.className, 'button') && 'button',
199+
),
200+
}))
201+
const iconElement = Icon.create(icon)
202+
const labelElement = Label.create(label, elProps => ({
203+
className: cx(
204+
// all label components should have the label className
205+
!_.includes(elProps.className, 'label') && 'label',
206+
// add 'left|right corner'
207+
_.includes(labelPosition, 'corner') && labelPosition,
208+
),
209+
}))
210+
211+
return (
212+
<ElementType {...rest} className={classes}>
213+
{actionPosition === 'left' && actionElement}
214+
{iconPosition === 'left' && iconElement}
215+
{labelPosition !== 'right' && labelElement}
216+
{createHTMLInput(input || type, htmlInputProps)}
217+
{actionPosition !== 'left' && actionElement}
218+
{iconPosition !== 'left' && iconElement}
219+
{labelPosition === 'right' && labelElement}
220+
</ElementType>
221+
)
222+
}
207223
}
208224

209225
export default Input

test/specs/elements/Input/Input-test.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import cx from 'classnames'
22
import _ from 'lodash'
33
import React from 'react'
4+
import { sandbox } from 'test/utils'
45

56
import Input, { htmlInputPropNames } from 'src/elements/Input/Input'
67
import * as common from 'test/specs/commonTests'
@@ -56,10 +57,33 @@ describe('Input', () => {
5657
describe('input props', () => {
5758
htmlInputPropNames.forEach(propName => {
5859
it(`passes \`${propName}\` to the <input>`, () => {
59-
shallow(<Input {...{ [propName]: 'foo' }} />)
60+
const propValue = propName === 'onChange' ? () => null : 'foo'
61+
const wrapper = shallow(<Input {...{ [propName]: propValue }} />)
62+
63+
// account for overloading the onChange prop
64+
const expectedValue = propName === 'onChange'
65+
? wrapper.instance().handleChange
66+
: propValue
67+
68+
wrapper
6069
.find('input')
61-
.should.have.prop(propName, 'foo')
70+
.should.have.prop(propName, expectedValue)
6271
})
6372
})
6473
})
74+
75+
describe('onChange', () => {
76+
it('is called with (event, props) on change', () => {
77+
const spy = sandbox.spy()
78+
const event = { fake: 'event' }
79+
const props = { 'data-foo': 'bar', onChange: spy }
80+
81+
const wrapper = shallow(<Input {...props} />)
82+
83+
wrapper.find('input').simulate('change', event)
84+
85+
spy.should.have.been.calledOnce()
86+
spy.should.have.been.calledWithMatch(event, props)
87+
})
88+
})
6589
})

0 commit comments

Comments
 (0)