Skip to content

[idea] React-enhanced input/textarea/select should be addons #2255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions src/addons/components/ReactInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* Copyright 2013-2014 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 ReactInput
*/

"use strict";

var DOMPropertyOperations = require('DOMPropertyOperations');
var LinkedValueUtils = require('LinkedValueUtils');
var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactDescriptor = require('ReactDescriptor');
var ReactDOMInput = require('ReactDOMInput');
var ReactMount = require('ReactMount');
var ReactUpdates = require('ReactUpdates');

var merge = require('merge');

// Store a reference to the <input> `ReactDOMComponent`. TODO: use string
var input = ReactDescriptor.createFactory(ReactDOMInput.type);

var instancesByReactID = {};

function forceUpdateIfMounted() {
/*jshint validthis:true */
if (this.isMounted()) {
this.forceUpdate();
}
}

/**
* Implements an <input> native component that allows setting these optional
* props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
*
* If `checked` or `value` are not supplied (or null/undefined), user actions
* that affect the checked state or value will trigger updates to the element.
*
* If they are supplied (and not null/undefined), the rendered element will not
* trigger updates to the element. Instead, the props must change in order for
* the rendered element to be updated.
*
* The rendered element will be initialized as unchecked (or `defaultChecked`)
* with an empty value (or `defaultValue`).
*
* @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
*/
var ReactInput = ReactCompositeComponent.createClass({
displayName: 'ReactInput',

mixins: [LinkedValueUtils.Mixin],

getInitialState: function() {
var checked = LinkedValueUtils.getChecked(this);
var value = LinkedValueUtils.getValue(this);
return {
// We don't have to save the initial value, but do it to play nice if
// `ReactDOMInput` decides to use `shouldComponentUpdate`.
initialChecked: checked != null ? checked : this.props.defaultChecked,
initialValue: value != null ? value : this.props.defaultValue
};
},

render: function() {
// Clone `this.props` so we don't mutate the input.
var props = merge(this.props);

props.initialChecked = this.state.initialChecked;
props.initialValue = this.state.initialValue;
props.onChange = this._handleChange;

return input(props, props.children);
},

componentDidMount: function() {
var id = ReactMount.getID(this.getDOMNode());
instancesByReactID[id] = this;
},

componentWillUnmount: function() {
var rootNode = this.getDOMNode();
var id = ReactMount.getID(rootNode);
delete instancesByReactID[id];
},

componentDidUpdate: function(prevProps, prevState, prevContext) {
var rootNode = this.getDOMNode();

var checked = LinkedValueUtils.getValue(this);
if (checked != null) {
DOMPropertyOperations.setValueForProperty(rootNode, 'checked', checked);
}

var value = LinkedValueUtils.getValue(this);
if (value != null) {
DOMPropertyOperations.setValueForProperty(rootNode, 'value', value);
}
},

_handleChange: function(event) {
var returnValue;
var onChange = LinkedValueUtils.getOnChange(this);
if (onChange) {
returnValue = onChange.call(this, event);
}
// Here we use setImmediate to wait until all updates have propagated, which
// is important when using controlled components within layers:
// https://github.com/facebook/react/issues/1698
ReactUpdates.setImmediate(forceUpdateIfMounted, this);

var name = this.props.name;
if (this.props.type === 'radio' && name != null) {
var rootNode = this.getDOMNode();
var queryRoot = rootNode;

while (queryRoot.parentNode) {
queryRoot = queryRoot.parentNode;
}

// If `rootNode.form` was non-null, then we could try `form.elements`,
// but that sometimes behaves strangely in IE8. We could also try using
// `form.getElementsByName`, but that will only return direct children
// and won't include inputs that use the HTML5 `form=` attribute. Since
// the input might not even be in a form, let's just use the global
// `querySelectorAll` to ensure we don't miss anything.
var group = queryRoot.querySelectorAll(
'input[name=' + JSON.stringify('' + name) + '][type="radio"]');

for (var i = 0, groupLen = group.length; i < groupLen; i++) {
var otherNode = group[i];
if (otherNode === rootNode ||
otherNode.form !== rootNode.form) {
continue;
}
var otherID = ReactMount.getID(otherNode);
invariant(
otherID,
'ReactDOMInput: Mixing React and non-React radio inputs with the ' +
'same `name` is not supported.'
);
var otherInstance = instancesByReactID[otherID];
invariant(
otherInstance,
'ReactDOMInput: Unknown radio button ID %s.',
otherID
);
// If this is a controlled radio button group, forcing the input that
// was previously checked to update will cause it to be come re-checked
// as appropriate.
ReactUpdates.setImmediate(forceUpdateIfMounted, otherInstance);
}
}

return returnValue;
}

});

module.exports = ReactInput;
189 changes: 189 additions & 0 deletions src/addons/components/ReactSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/**
* Copyright 2013-2014 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 ReactSelect
*/

"use strict";

var DOMPropertyOperations = require('DOMPropertyOperations');
var LinkedValueUtils = require('LinkedValueUtils');
var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactDescriptor = require('ReactDescriptor');
var ReactDOMSelect = require('ReactDOMSelect');
var ReactUpdates = require('ReactUpdates');

var merge = require('merge');

// Store a reference to the <select> `ReactDOMComponent`. TODO: use string
var select = ReactDescriptor.createFactory(ReactDOMSelect.type);

function updateWithPendingValueIfMounted() {
/*jshint validthis:true */
if (this.isMounted()) {
this.setState({value: this._pendingValue});
this._pendingValue = 0;
}
}

/**
* Validation function for `value` and `defaultValue`.
* @private
*/
function selectValueType(props, propName, componentName) {
if (props[propName] == null) {
return;
}
if (props.multiple) {
if (!Array.isArray(props[propName])) {
return new Error(
`The \`${propName}\` prop supplied to <ReactSelect> must be an array if ` +
`\`multiple\` is true.`
);
}
} else {
if (Array.isArray(props[propName])) {
return new Error(
`The \`${propName}\` prop supplied to <ReactSelect> must be a scalar ` +
`value if \`multiple\` is false.`
);
}
}
}

/**
* If `value` is supplied, updates <option> elements on mount and update.
* @param {ReactComponent} component Instance of ReactDOMSelect
* @param {?*} propValue For uncontrolled components, null/undefined. For
* controlled components, a string (or with `multiple`, a list of strings).
* @private
*/
function updateOptions(component, propValue) {
var multiple = component.props.multiple;
var value = propValue != null ? propValue : component.state.value;
var options = component.getDOMNode().options;
var selectedValue, i, l;
if (multiple) {
selectedValue = {};
for (i = 0, l = value.length; i < l; ++i) {
selectedValue['' + value[i]] = true;
}
} else {
selectedValue = '' + value;
}
for (i = 0, l = options.length; i < l; i++) {
var selected = multiple ?
selectedValue.hasOwnProperty(options[i].value) :
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 ReactSelect = ReactCompositeComponent.createClass({
displayName: 'ReactSelect',

mixins: [LinkedValueUtils.Mixin],

propTypes: {
defaultValue: selectValueType,
value: selectValueType
},

getInitialState: function() {
return {value: this.props.defaultValue || (this.props.multiple ? [] : '')};
},

componentWillMount: function() {
this._pendingValue = null;
},

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]});
}
},

render: function() {
// Clone `this.props` so we don't mutate the input.
var props = merge(this.props);

props.initialValue = null;
props.value = null;
props.onChange = this._handleChange;

return select(props, props.children);
},

componentDidMount: function() {
updateOptions(this, LinkedValueUtils.getValue(this));
},

componentDidUpdate: function(prevProps) {
var value = LinkedValueUtils.getValue(this);
var prevMultiple = !!prevProps.multiple;
var multiple = !!this.props.multiple;
if (value != null || prevMultiple !== multiple) {
updateOptions(this, value);
}
},

_handleChange: function(event) {
var returnValue;
var onChange = LinkedValueUtils.getOnChange(this);
if (onChange) {
returnValue = onChange.call(this, event);
}

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._pendingValue = selectedValue;
ReactUpdates.setImmediate(updateWithPendingValueIfMounted, this);
return returnValue;
}

});

module.exports = ReactSelect;
Loading