Controlled <select> and <option> Components

This implements a `<select>` component that supports `value` and `defaultValue`. It also changes `<option>` to warn when the `selected` prop is supplied.
This commit is contained in:
Tim Yung
2013-07-16 11:37:04 -07:00
committed by Paul O’Shannessy
parent 5d4f903482
commit ed54fff204
3 changed files with 210 additions and 0 deletions
+4
View File
@@ -21,6 +21,8 @@
var ReactDOM = require('ReactDOM');
var ReactDOMForm = require('ReactDOMForm');
var ReactDOMInput = require('ReactDOMInput');
var ReactDOMOption = require('ReactDOMOption');
var ReactDOMSelect = require('ReactDOMSelect');
var ReactDOMTextarea = require('ReactDOMTextarea');
var DefaultDOMPropertyConfig = require('DefaultDOMPropertyConfig');
@@ -53,6 +55,8 @@ function inject() {
ReactDOM.injection.injectComponentClasses({
form: ReactDOMForm,
input: ReactDOMInput,
option: ReactDOMOption,
select: ReactDOMSelect,
textarea: ReactDOMTextarea
});
+50
View File
@@ -0,0 +1,50 @@
/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule ReactDOMOption
*/
"use strict";
var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactDOM = require('ReactDOM');
// Store a reference to the <option> `ReactNativeComponent`.
var option = ReactDOM.option;
/**
* Implements an <option> native component that warns when `selected` is set.
*/
var ReactDOMOption = ReactCompositeComponent.createClass({
componentWillMount: function() {
// TODO (yungsters): Remove support for `selected` in <option>.
if (this.props.selected != null) {
if (__DEV__) {
console.warn(
'Use the `defaultValue` or `value` props on <select> instead of ' +
'setting `selected` on <option>.'
);
}
}
},
render: function() {
return option(this.props, this.props.children);
}
});
module.exports = ReactDOMOption;
+156
View File
@@ -0,0 +1,156 @@
/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule ReactDOMSelect
*/
"use strict";
var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactDOM = require('ReactDOM');
var invariant = require('invariant');
var merge = require('merge');
// Store a reference to the <select> `ReactNativeComponent`.
var select = ReactDOM.select;
/**
* Validation function for `value` and `defaultValue`.
* @private
*/
function selectValueType(props, propName, componentName) {
if (props[propName] == null) {
return;
}
if (props.multiple) {
invariant(
Array.isArray(props[propName]),
'The `%s` prop supplied to <select> must be an array if `multiple` is ' +
'true.'
);
} else {
invariant(
!Array.isArray(props[propName]),
'The `%s` prop supplied to <select> must be a scalar value if ' +
'`multiple` is false.'
);
}
}
/**
* If `value` is supplied, updates <option> elements on mount and update.
* @private
*/
function updateOptions() {
/*jshint validthis:true */
if (this.props.value == null) {
return;
}
var options = this.getDOMNode().options;
var selectedValue = '' + this.props.value;
for (var i = 0, l = options.length; i < l; i++) {
var selected = this.props.multiple ?
selectedValue.indexOf(options[i].value) >= 0 :
selected = options[i].value === selectedValue;
if (selected !== options[i].selected) {
options[i].selected = selected;
}
}
}
/**
* Implements a <select> native component that allows optionally setting the
* props `value` and `defaultValue`. If `multiple` is false, the prop must be a
* string. If `multiple` is true, the prop must be an array of strings.
*
* If `value` is not supplied (or null/undefined), user actions that change the
* selected option will trigger updates to the rendered options.
*
* If it is supplied (and not null/undefined), the rendered options will not
* update in response to user actions. Instead, the `value` prop must change in
* order for the rendered options to update.
*
* If `defaultValue` is provided, any options with the supplied values will be
* selected.
*/
var ReactDOMSelect = ReactCompositeComponent.createClass({
propTypes: {
defaultValue: selectValueType,
value: selectValueType
},
getInitialState: function() {
return {value: this.props.defaultValue || (this.props.multiple ? [] : '')};
},
componentWillReceiveProps: function(nextProps) {
if (!this.props.multiple && nextProps.multiple) {
this.setState({value: [this.state.value]});
} else if (this.props.multiple && !nextProps.multiple) {
this.setState({value: this.state.value[0]});
}
},
shouldComponentUpdate: function() {
// Defer any updates to this component during the `onChange` handler.
return !this._isChanging;
},
render: function() {
// Clone `this.props` so we don't mutate the input.
var props = merge(this.props);
props.onChange = this.handleChange;
props.value = null;
return select(props, this.props.children);
},
componentDidMount: updateOptions,
componentDidUpdate: updateOptions,
handleChange: function(event) {
var returnValue;
if (this.props.onChange) {
this._isChanging = true;
returnValue = this.props.onChange(event);
this._isChanging = false;
}
var selectedValue;
if (this.props.multiple) {
selectedValue = [];
var options = event.target.options;
for (var i = 0, l = options.length; i < l; i++) {
if (options[i].selected) {
selectedValue.push(options[i].value);
}
}
} else {
selectedValue = event.target.value;
}
this.setState({value: selectedValue});
return returnValue;
}
});
module.exports = ReactDOMSelect;