Fork ReactDOMComponent and the wrappers to Fiber

From now on any change to these have to be replicated to these files.

I will next start changing the implementation to work with Fiber.
This commit is contained in:
Sebastian Markbage
2016-11-18 00:26:23 +00:00
parent 32f5b034ed
commit 9c3e9fbb05
5 changed files with 2116 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,313 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactDOMFiberInput
*/
'use strict';
var DOMPropertyOperations = require('DOMPropertyOperations');
var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var invariant = require('invariant');
var warning = require('warning');
var didWarnValueDefaultValue = false;
var didWarnCheckedDefaultChecked = false;
var didWarnControlledToUncontrolled = false;
var didWarnUncontrolledToControlled = false;
function isControlled(props) {
var usesChecked = props.type === 'checkbox' || props.type === 'radio';
return usesChecked ? props.checked != null : props.value != null;
}
/**
* Implements an <input> host 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 ReactDOMInput = {
getHostProps: function(inst, props) {
var value = props.value;
var checked = props.checked;
var hostProps = Object.assign({
// Make sure we set .type before any other properties (setting .value
// before .type means .value is lost in IE11 and below)
type: undefined,
// Make sure we set .step before .value (setting .value before .step
// means .value is rounded on mount, based upon step precision)
step: undefined,
// Make sure we set .min & .max before .value (to ensure proper order
// in corner cases such as min or max deriving from value, e.g. Issue #7170)
min: undefined,
max: undefined,
}, props, {
defaultChecked: undefined,
defaultValue: undefined,
value: value != null ? value : inst._wrapperState.initialValue,
checked: checked != null ? checked : inst._wrapperState.initialChecked,
});
return hostProps;
},
mountWrapper: function(inst, props) {
if (__DEV__) {
ReactControlledValuePropTypes.checkPropTypes(
'input',
props,
inst._currentElement._owner
);
var owner = inst._currentElement._owner;
if (
props.checked !== undefined &&
props.defaultChecked !== undefined &&
!didWarnCheckedDefaultChecked
) {
warning(
false,
'%s contains an input of type %s with both checked and defaultChecked props. ' +
'Input elements must be either controlled or uncontrolled ' +
'(specify either the checked prop, or the defaultChecked prop, but not ' +
'both). Decide between using a controlled or uncontrolled input ' +
'element and remove one of these props. More info: ' +
'https://fb.me/react-controlled-components',
owner && owner.getName() || 'A component',
props.type
);
didWarnCheckedDefaultChecked = true;
}
if (
props.value !== undefined &&
props.defaultValue !== undefined &&
!didWarnValueDefaultValue
) {
warning(
false,
'%s contains an input of type %s with both value and defaultValue props. ' +
'Input elements must be either controlled or uncontrolled ' +
'(specify either the value prop, or the defaultValue prop, but not ' +
'both). Decide between using a controlled or uncontrolled input ' +
'element and remove one of these props. More info: ' +
'https://fb.me/react-controlled-components',
owner && owner.getName() || 'A component',
props.type
);
didWarnValueDefaultValue = true;
}
}
var defaultValue = props.defaultValue;
inst._wrapperState = {
initialChecked: props.checked != null ? props.checked : props.defaultChecked,
initialValue: props.value != null ? props.value : defaultValue,
listeners: null,
};
if (__DEV__) {
inst._wrapperState.controlled = isControlled(props);
}
},
updateWrapper: function(inst) {
var props = inst._currentElement.props;
if (__DEV__) {
var controlled = isControlled(props);
var owner = inst._currentElement._owner;
if (!inst._wrapperState.controlled && controlled && !didWarnUncontrolledToControlled) {
warning(
false,
'%s is changing an uncontrolled input of type %s to be controlled. ' +
'Input elements should not switch from uncontrolled to controlled (or vice versa). ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://fb.me/react-controlled-components',
owner && owner.getName() || 'A component',
props.type
);
didWarnUncontrolledToControlled = true;
}
if (inst._wrapperState.controlled && !controlled && !didWarnControlledToUncontrolled) {
warning(
false,
'%s is changing a controlled input of type %s to be uncontrolled. ' +
'Input elements should not switch from controlled to uncontrolled (or vice versa). ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://fb.me/react-controlled-components',
owner && owner.getName() || 'A component',
props.type
);
didWarnControlledToUncontrolled = true;
}
}
var checked = props.checked;
if (checked != null) {
DOMPropertyOperations.setValueForProperty(
ReactDOMComponentTree.getNodeFromInstance(inst),
'checked',
checked || false
);
}
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
var value = props.value;
if (value != null) {
// Cast `value` to a string to ensure the value is set correctly. While
// browsers typically do this as necessary, jsdom doesn't.
var newValue = '' + value;
// To avoid side effects (such as losing text selection), only set value if changed
if (newValue !== node.value) {
node.value = newValue;
}
} else {
if (props.value == null && props.defaultValue != null) {
// In Chrome, assigning defaultValue to certain input types triggers input validation.
// For number inputs, the display value loses trailing decimal points. For email inputs,
// Chrome raises "The specified value <x> is not a valid email address".
//
// Here we check to see if the defaultValue has actually changed, avoiding these problems
// when the user is inputting text
//
// https://github.com/facebook/react/issues/7253
if (node.defaultValue !== '' + props.defaultValue) {
node.defaultValue = '' + props.defaultValue;
}
}
if (props.checked == null && props.defaultChecked != null) {
node.defaultChecked = !!props.defaultChecked;
}
}
},
postMountWrapper: function(inst) {
var props = inst._currentElement.props;
// This is in postMount because we need access to the DOM node, which is not
// available until after the component has mounted.
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
// Detach value from defaultValue. We won't do anything if we're working on
// submit or reset inputs as those values & defaultValues are linked. They
// are not resetable nodes so this operation doesn't matter and actually
// removes browser-default values (eg "Submit Query") when no value is
// provided.
switch (props.type) {
case 'submit':
case 'reset':
break;
case 'color':
case 'date':
case 'datetime':
case 'datetime-local':
case 'month':
case 'time':
case 'week':
// This fixes the no-show issue on iOS Safari and Android Chrome:
// https://github.com/facebook/react/issues/7233
node.value = '';
node.value = node.defaultValue;
break;
default:
node.value = node.value;
break;
}
// Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug
// this is needed to work around a chrome bug where setting defaultChecked
// will sometimes influence the value of checked (even after detachment).
// Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=608416
// We need to temporarily unset name to avoid disrupting radio button groups.
var name = node.name;
if (name !== '') {
node.name = '';
}
node.defaultChecked = !node.defaultChecked;
node.defaultChecked = !node.defaultChecked;
if (name !== '') {
node.name = name;
}
},
restoreControlledState: function(inst) {
if (inst._rootNodeID) {
// DOM component is still mounted; update
ReactDOMInput.updateWrapper(inst);
}
var props = inst._currentElement.props;
updateNamedCousins(inst, props);
},
};
function updateNamedCousins(thisInstance, props) {
var name = props.name;
if (props.type === 'radio' && name != null) {
var rootNode = ReactDOMComponentTree.getNodeFromInstance(thisInstance);
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. It might not even be in the
// document. Let's just use the local `querySelectorAll` to ensure we don't
// miss anything.
var group = queryRoot.querySelectorAll(
'input[name=' + JSON.stringify('' + name) + '][type="radio"]');
for (var i = 0; i < group.length; i++) {
var otherNode = group[i];
if (otherNode === rootNode ||
otherNode.form !== rootNode.form) {
continue;
}
// This will throw if radio buttons rendered by different copies of React
// and the same name are rendered into the same form (same as #1939).
// That's probably okay; we don't support it just as we don't support
// mixing React radio buttons with non-React ones.
var otherInstance = ReactDOMComponentTree.getInstanceFromNode(otherNode);
invariant(
otherInstance,
'ReactDOMInput: Mixing React and non-React radio inputs with the ' +
'same `name` is not supported.'
);
// 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.
if (otherInstance._rootNodeID) {
ReactDOMInput.updateWrapper(otherInstance);
}
}
}
}
module.exports = ReactDOMInput;
@@ -0,0 +1,128 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactDOMFiberOption
*/
'use strict';
var React = require('React');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var ReactDOMSelect = require('ReactDOMSelect');
var warning = require('warning');
var didWarnInvalidOptionChildren = false;
function flattenChildren(children) {
var content = '';
// Flatten children and warn if they aren't strings or numbers;
// invalid types are ignored.
React.Children.forEach(children, function(child) {
if (child == null) {
return;
}
if (typeof child === 'string' || typeof child === 'number') {
content += child;
} else if (!didWarnInvalidOptionChildren) {
didWarnInvalidOptionChildren = true;
warning(
false,
'Only strings and numbers are supported as <option> children.'
);
}
});
return content;
}
/**
* Implements an <option> host component that warns when `selected` is set.
*/
var ReactDOMOption = {
mountWrapper: function(inst, props, hostParent) {
// TODO (yungsters): Remove support for `selected` in <option>.
if (__DEV__) {
warning(
props.selected == null,
'Use the `defaultValue` or `value` props on <select> instead of ' +
'setting `selected` on <option>.'
);
}
// Look up whether this option is 'selected'
var selectValue = null;
if (hostParent != null) {
var selectParent = hostParent;
if (selectParent._tag === 'optgroup') {
selectParent = selectParent._hostParent;
}
if (selectParent != null && selectParent._tag === 'select') {
selectValue = ReactDOMSelect.getSelectValueContext(selectParent);
}
}
// If the value is null (e.g., no specified value or after initial mount)
// or missing (e.g., for <datalist>), we don't change props.selected
var selected = null;
if (selectValue != null) {
var value;
if (props.value != null) {
value = props.value + '';
} else {
value = flattenChildren(props.children);
}
selected = false;
if (Array.isArray(selectValue)) {
// multiple
for (var i = 0; i < selectValue.length; i++) {
if ('' + selectValue[i] === value) {
selected = true;
break;
}
}
} else {
selected = ('' + selectValue === value);
}
}
inst._wrapperState = {selected: selected};
},
postMountWrapper: function(inst) {
// value="" should make a value attribute (#6219)
var props = inst._currentElement.props;
if (props.value != null) {
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
node.setAttribute('value', props.value);
}
},
getHostProps: function(inst, props) {
var hostProps = Object.assign({selected: undefined, children: undefined}, props);
// Read state only from initial mount because <select> updates value
// manually; we need the initial state only for server rendering
if (inst._wrapperState.selected != null) {
hostProps.selected = inst._wrapperState.selected;
}
var content = flattenChildren(props.children);
if (content) {
hostProps.children = content;
}
return hostProps;
},
};
module.exports = ReactDOMOption;
@@ -0,0 +1,200 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactDOMFiberSelect
*/
'use strict';
var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var warning = require('warning');
var didWarnValueDefaultValue = false;
function getDeclarationErrorAddendum(owner) {
if (owner) {
var name = owner.getName();
if (name) {
return ' Check the render method of `' + name + '`.';
}
}
return '';
}
var valuePropNames = ['value', 'defaultValue'];
/**
* Validation function for `value` and `defaultValue`.
* @private
*/
function checkSelectPropTypes(inst, props) {
var owner = inst._currentElement._owner;
ReactControlledValuePropTypes.checkPropTypes(
'select',
props,
owner
);
for (var i = 0; i < valuePropNames.length; i++) {
var propName = valuePropNames[i];
if (props[propName] == null) {
continue;
}
var isArray = Array.isArray(props[propName]);
if (props.multiple && !isArray) {
warning(
false,
'The `%s` prop supplied to <select> must be an array if ' +
'`multiple` is true.%s',
propName,
getDeclarationErrorAddendum(owner)
);
} else if (!props.multiple && isArray) {
warning(
false,
'The `%s` prop supplied to <select> must be a scalar ' +
'value if `multiple` is false.%s',
propName,
getDeclarationErrorAddendum(owner)
);
}
}
}
/**
* @param {ReactDOMComponent} inst
* @param {boolean} multiple
* @param {*} propValue A stringable (with `multiple`, a list of stringables).
* @private
*/
function updateOptions(inst, multiple, propValue) {
var options = ReactDOMComponentTree.getNodeFromInstance(inst).options;
if (multiple) {
let selectedValue = {};
for (let i = 0; i < propValue.length; i++) {
selectedValue['' + propValue[i]] = true;
}
for (let i = 0; i < options.length; i++) {
var selected = selectedValue.hasOwnProperty(options[i].value);
if (options[i].selected !== selected) {
options[i].selected = selected;
}
}
} else {
// Do not set `select.value` as exact behavior isn't consistent across all
// browsers for all cases.
let selectedValue = '' + propValue;
for (let i = 0; i < options.length; i++) {
if (options[i].value === selectedValue) {
options[i].selected = true;
return;
}
}
if (options.length) {
options[0].selected = true;
}
}
}
/**
* Implements a <select> host component that allows optionally setting the
* props `value` and `defaultValue`. If `multiple` is false, the prop must be a
* stringable. If `multiple` is true, the prop must be an array of stringables.
*
* 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 = {
getHostProps: function(inst, props) {
return Object.assign({}, props, {
value: undefined,
});
},
mountWrapper: function(inst, props) {
if (__DEV__) {
checkSelectPropTypes(inst, props);
}
var value = props.value;
inst._wrapperState = {
initialValue: value != null ? value : props.defaultValue,
listeners: null,
wasMultiple: Boolean(props.multiple),
};
if (
props.value !== undefined &&
props.defaultValue !== undefined &&
!didWarnValueDefaultValue
) {
warning(
false,
'Select elements must be either controlled or uncontrolled ' +
'(specify either the value prop, or the defaultValue prop, but not ' +
'both). Decide between using a controlled or uncontrolled select ' +
'element and remove one of these props. More info: ' +
'https://fb.me/react-controlled-components'
);
didWarnValueDefaultValue = true;
}
},
getSelectValueContext: function(inst) {
// ReactDOMOption looks at this initial value so the initial generated
// markup has correct `selected` attributes
return inst._wrapperState.initialValue;
},
postUpdateWrapper: function(inst) {
var props = inst._currentElement.props;
// After the initial mount, we control selected-ness manually so don't pass
// this value down
inst._wrapperState.initialValue = undefined;
var wasMultiple = inst._wrapperState.wasMultiple;
inst._wrapperState.wasMultiple = Boolean(props.multiple);
var value = props.value;
if (value != null) {
updateOptions(inst, Boolean(props.multiple), value);
} else if (wasMultiple !== Boolean(props.multiple)) {
// For simplicity, reapply `defaultValue` if `multiple` is toggled.
if (props.defaultValue != null) {
updateOptions(inst, Boolean(props.multiple), props.defaultValue);
} else {
// Revert the select back to its default unselected state.
updateOptions(inst, Boolean(props.multiple), props.multiple ? [] : '');
}
}
},
restoreControlledState: function(inst) {
if (inst._rootNodeID) {
var props = inst._currentElement.props;
var value = props.value;
if (value != null) {
updateOptions(inst, Boolean(props.multiple), value);
}
}
},
};
module.exports = ReactDOMSelect;
@@ -0,0 +1,172 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactDOMFiberTextarea
*/
'use strict';
var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var invariant = require('invariant');
var warning = require('warning');
var didWarnValDefaultVal = false;
/**
* Implements a <textarea> host component that allows setting `value`, and
* `defaultValue`. This differs from the traditional DOM API because value is
* usually set as PCDATA children.
*
* If `value` is not supplied (or null/undefined), user actions that affect the
* value will trigger updates to the element.
*
* If `value` is supplied (and not null/undefined), the rendered element will
* not trigger updates to the element. Instead, the `value` prop must change in
* order for the rendered element to be updated.
*
* The rendered element will be initialized with an empty value, the prop
* `defaultValue` if specified, or the children content (deprecated).
*/
var ReactDOMTextarea = {
getHostProps: function(inst, props) {
invariant(
props.dangerouslySetInnerHTML == null,
'`dangerouslySetInnerHTML` does not make sense on <textarea>.'
);
// Always set children to the same thing. In IE9, the selection range will
// get reset if `textContent` is mutated. We could add a check in setTextContent
// to only set the value if/when the value differs from the node value (which would
// completely solve this IE9 bug), but Sebastian+Ben seemed to like this solution.
// The value can be a boolean or object so that's why it's forced to be a string.
var hostProps = Object.assign({}, props, {
value: undefined,
defaultValue: undefined,
children: '' + inst._wrapperState.initialValue,
});
return hostProps;
},
mountWrapper: function(inst, props) {
if (__DEV__) {
ReactControlledValuePropTypes.checkPropTypes(
'textarea',
props,
inst._currentElement._owner
);
if (
props.value !== undefined &&
props.defaultValue !== undefined &&
!didWarnValDefaultVal
) {
warning(
false,
'Textarea elements must be either controlled or uncontrolled ' +
'(specify either the value prop, or the defaultValue prop, but not ' +
'both). Decide between using a controlled or uncontrolled textarea ' +
'and remove one of these props. More info: ' +
'https://fb.me/react-controlled-components'
);
didWarnValDefaultVal = true;
}
}
var value = props.value;
var initialValue = value;
// Only bother fetching default value if we're going to use it
if (value == null) {
var defaultValue = props.defaultValue;
// TODO (yungsters): Remove support for children content in <textarea>.
var children = props.children;
if (children != null) {
if (__DEV__) {
warning(
false,
'Use the `defaultValue` or `value` props instead of setting ' +
'children on <textarea>.'
);
}
invariant(
defaultValue == null,
'If you supply `defaultValue` on a <textarea>, do not pass children.'
);
if (Array.isArray(children)) {
invariant(
children.length <= 1,
'<textarea> can only have at most one child.'
);
children = children[0];
}
defaultValue = '' + children;
}
if (defaultValue == null) {
defaultValue = '';
}
initialValue = defaultValue;
}
inst._wrapperState = {
initialValue: '' + initialValue,
listeners: null,
};
},
updateWrapper: function(inst) {
var props = inst._currentElement.props;
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
var value = props.value;
if (value != null) {
// Cast `value` to a string to ensure the value is set correctly. While
// browsers typically do this as necessary, jsdom doesn't.
var newValue = '' + value;
// To avoid side effects (such as losing text selection), only set value if changed
if (newValue !== node.value) {
node.value = newValue;
}
if (props.defaultValue == null) {
node.defaultValue = newValue;
}
}
if (props.defaultValue != null) {
node.defaultValue = props.defaultValue;
}
},
postMountWrapper: function(inst) {
// This is in postMount because we need access to the DOM node, which is not
// available until after the component has mounted.
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
var textContent = node.textContent;
// Only set node.value if textContent is equal to the expected
// initial value. In IE10/IE11 there is a bug where the placeholder attribute
// will populate textContent as well.
// https://developer.microsoft.com/microsoft-edge/platform/issues/101525/
if (textContent === inst._wrapperState.initialValue) {
node.value = textContent;
}
},
restoreControlledState: function(inst) {
if (inst._rootNodeID) {
// DOM component is still mounted; update
ReactDOMTextarea.updateWrapper(inst);
}
},
};
module.exports = ReactDOMTextarea;