mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Correctly remove attributes when deleting props
The most obvious manifestation of this bug is visible here: http://jsfiddle.net/spicyj/zzGas/. In short, when props are removed from a component, the underlying HTML element doesn't have the attribute removed. This change should fix it, but unfortunately it (presumably) makes _updateDOMProperties a bit slower.
This commit is contained in:
@@ -35,6 +35,13 @@ var merge = require('merge');
|
||||
*/
|
||||
var OWNER = '{owner}';
|
||||
|
||||
/**
|
||||
* Internal properties that shouldn't be considered when modifying node
|
||||
* attributes.
|
||||
*/
|
||||
var internalPropNames = {children: true};
|
||||
internalPropNames[OWNER] = true;
|
||||
|
||||
/**
|
||||
* Every React component is in one of these life cycles.
|
||||
*/
|
||||
@@ -131,6 +138,11 @@ function appendNestedChildren(parentKey, sourceArray, targetArray) {
|
||||
*/
|
||||
var ReactComponent = {
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
internalPropNames: internalPropNames,
|
||||
|
||||
/**
|
||||
* @param {?object} object
|
||||
* @return {boolean} True if `object` is a valid component.
|
||||
|
||||
@@ -75,6 +75,24 @@ var ReactDOMIDOperations = {
|
||||
DOMPropertyOperations.setValueForProperty(node, name, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a DOM node to remove a property. This should only be used to remove
|
||||
* DOM properties in `DOMProperty`.
|
||||
*
|
||||
* @param {string} id ID of the node to update.
|
||||
* @param {string} name A property name to remove, see `DOMProperty`.
|
||||
* @internal
|
||||
*/
|
||||
deletePropertyByID: function(id, name, value) {
|
||||
var node = ReactDOMNodeCache.getCachedNodeByID(id);
|
||||
invariant(
|
||||
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name),
|
||||
'updatePropertyByID(...): %s',
|
||||
INVALID_PROPERTY_ERRORS[name]
|
||||
);
|
||||
DOMPropertyOperations.deleteValueForProperty(node, name, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* This should almost never be used instead of `updatePropertyByID()` due to
|
||||
* the extra object allocation required by the API. That said, this is useful
|
||||
@@ -95,7 +113,8 @@ var ReactDOMIDOperations = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a DOM node with new style values.
|
||||
* Updates a DOM node with new style values. If a value is specified as '',
|
||||
* the corresponding style property will be unset.
|
||||
*
|
||||
* @param {string} id ID of the node to update.
|
||||
* @param {object} styles Mapping from styles to values.
|
||||
|
||||
@@ -323,6 +323,8 @@ var ReactEventEmitter = {
|
||||
|
||||
getListener: EventPluginHub.getListener,
|
||||
|
||||
deleteListener: EventPluginHub.deleteListener,
|
||||
|
||||
deleteAllListeners: EventPluginHub.deleteAllListeners,
|
||||
|
||||
trapBubbledEvent: trapBubbledEvent,
|
||||
|
||||
@@ -32,7 +32,10 @@ var keyOf = require('keyOf');
|
||||
var merge = require('merge');
|
||||
var mixInto = require('mixInto');
|
||||
|
||||
var internalPropNames = ReactComponent.internalPropNames;
|
||||
|
||||
var putListener = ReactEventEmitter.putListener;
|
||||
var deleteListener = ReactEventEmitter.deleteListener;
|
||||
var registrationNames = ReactEventEmitter.registrationNames;
|
||||
|
||||
// For quickly matching children type, to test if can be treated as content.
|
||||
@@ -206,9 +209,46 @@ ReactNativeComponent.Mixin = {
|
||||
*/
|
||||
_updateDOMProperties: function(nextProps) {
|
||||
var lastProps = this.props;
|
||||
for (var propKey in nextProps) {
|
||||
var nextProp = nextProps[propKey];
|
||||
var lastProp = lastProps[propKey];
|
||||
var propKey;
|
||||
var nextProp;
|
||||
var lastProp;
|
||||
var styleName;
|
||||
var styleUpdates;
|
||||
for (propKey in lastProps) {
|
||||
nextProp = nextProps[propKey];
|
||||
lastProp = lastProps[propKey];
|
||||
if (!lastProps.hasOwnProperty(propKey) || nextProp) {
|
||||
continue;
|
||||
}
|
||||
if (propKey === STYLE) {
|
||||
for (styleName in lastProp) {
|
||||
if (!lastProp.hasOwnProperty(styleName)) {
|
||||
continue;
|
||||
}
|
||||
if (!styleUpdates) {
|
||||
styleUpdates = {};
|
||||
}
|
||||
styleUpdates[styleName] = '';
|
||||
}
|
||||
} else if (propKey === DANGEROUSLY_SET_INNER_HTML ||
|
||||
propKey === CONTENT) {
|
||||
ReactComponent.DOMIDOperations.updateTextContentByID(
|
||||
this._rootNodeID,
|
||||
''
|
||||
);
|
||||
} else if (internalPropNames[propKey]) {
|
||||
} else if (registrationNames[propKey]) {
|
||||
deleteListener(this._rootNodeID, propKey);
|
||||
} else {
|
||||
ReactComponent.DOMIDOperations.deletePropertyByID(
|
||||
this._rootNodeID,
|
||||
propKey
|
||||
);
|
||||
}
|
||||
}
|
||||
for (propKey in nextProps) {
|
||||
nextProp = nextProps[propKey];
|
||||
lastProp = lastProps[propKey];
|
||||
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
|
||||
continue;
|
||||
}
|
||||
@@ -216,8 +256,17 @@ ReactNativeComponent.Mixin = {
|
||||
if (nextProp) {
|
||||
nextProp = nextProps.style = merge(nextProp);
|
||||
}
|
||||
var styleUpdates;
|
||||
for (var styleName in nextProp) {
|
||||
if (lastProp) {
|
||||
for (styleName in lastProp) {
|
||||
if (lastProp.hasOwnProperty(styleName) && !nextProp[styleName]) {
|
||||
if (!styleUpdates) {
|
||||
styleUpdates = {};
|
||||
}
|
||||
styleUpdates[styleName] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
for (styleName in nextProp) {
|
||||
if (!nextProp.hasOwnProperty(styleName)) {
|
||||
continue;
|
||||
}
|
||||
@@ -228,12 +277,6 @@ ReactNativeComponent.Mixin = {
|
||||
styleUpdates[styleName] = nextProp[styleName];
|
||||
}
|
||||
}
|
||||
if (styleUpdates) {
|
||||
ReactComponent.DOMIDOperations.updateStylesByID(
|
||||
this._rootNodeID,
|
||||
styleUpdates
|
||||
);
|
||||
}
|
||||
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
|
||||
var lastHtml = lastProp && lastProp.__html;
|
||||
var nextHtml = nextProp && nextProp.__html;
|
||||
@@ -248,6 +291,7 @@ ReactNativeComponent.Mixin = {
|
||||
this._rootNodeID,
|
||||
'' + nextProp
|
||||
);
|
||||
} else if (internalPropNames[propKey]) {
|
||||
} else if (registrationNames[propKey]) {
|
||||
putListener(this._rootNodeID, propKey, nextProp);
|
||||
} else {
|
||||
@@ -258,6 +302,12 @@ ReactNativeComponent.Mixin = {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (styleUpdates) {
|
||||
ReactComponent.DOMIDOperations.updateStylesByID(
|
||||
this._rootNodeID,
|
||||
styleUpdates
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -100,6 +100,55 @@ describe('ReactNativeComponent', function() {
|
||||
expect(stubStyle.display).toEqual('block');
|
||||
});
|
||||
|
||||
it("should remove attributes", function() {
|
||||
var stub = ReactTestUtils.renderIntoDocument(<img height='17' />);
|
||||
|
||||
expect(stub.getDOMNode().hasAttribute('height')).toBe(true);
|
||||
stub.receiveProps({}, transaction);
|
||||
expect(stub.getDOMNode().hasAttribute('height')).toBe(false);
|
||||
});
|
||||
|
||||
it("should remove properties", function() {
|
||||
var stub = ReactTestUtils.renderIntoDocument(<div className='monkey' />);
|
||||
|
||||
expect(stub.getDOMNode().className).toEqual('monkey');
|
||||
stub.receiveProps({}, transaction);
|
||||
expect(stub.getDOMNode().className).toEqual('');
|
||||
});
|
||||
|
||||
it("should clear a single style prop when changing 'style'", function() {
|
||||
var styles = {display: 'none', color: 'red'};
|
||||
var stub = ReactTestUtils.renderIntoDocument(<div style={styles} />);
|
||||
|
||||
var stubStyle = stub.getDOMNode().style;
|
||||
|
||||
styles = {color: 'green'};
|
||||
stub.receiveProps({ style: styles }, transaction);
|
||||
expect(stubStyle.display).toEqual('');
|
||||
expect(stubStyle.color).toEqual('green');
|
||||
});
|
||||
|
||||
it("should clear all the styles when removing 'style'", function() {
|
||||
var styles = {display: 'none', color: 'red'};
|
||||
var stub = ReactTestUtils.renderIntoDocument(<div style={styles} />);
|
||||
|
||||
var stubStyle = stub.getDOMNode().style;
|
||||
|
||||
stub.receiveProps({}, transaction);
|
||||
expect(stubStyle.display).toEqual('');
|
||||
expect(stubStyle.color).toEqual('');
|
||||
});
|
||||
|
||||
it("should empty element when removing innerHTML", function() {
|
||||
var stub = ReactTestUtils.renderIntoDocument(
|
||||
<div dangerouslySetInnerHTML={{__html: ":)"}} />
|
||||
);
|
||||
|
||||
expect(stub.getDOMNode().innerHTML).toEqual(':)');
|
||||
stub.receiveProps({}, transaction);
|
||||
expect(stub.getDOMNode().innerHTML).toEqual('');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('createOpenTagMarkup', function() {
|
||||
|
||||
+55
-1
@@ -18,6 +18,8 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var keyMirror = require('keyMirror');
|
||||
|
||||
/**
|
||||
* CSS properties which accept numbers but are not in units of "px".
|
||||
*/
|
||||
@@ -30,8 +32,60 @@ var isUnitlessNumber = {
|
||||
zoom: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Most style properties can be unset by doing .style[prop] = '' but IE8
|
||||
* doesn't like doing that with shorthand properties so for the properties that
|
||||
* IE8 breaks on, which are listed here, we instead unset each of the
|
||||
* individual properties. See http://bugs.jquery.com/ticket/12385.
|
||||
* The 4-value 'clock' properties like margin, padding, border-width seem to
|
||||
* behave without any problems. Curiously, list-style works too without any
|
||||
* special prodding.
|
||||
*/
|
||||
var shorthandPropertyExpansions = {
|
||||
background: {
|
||||
backgroundImage: true,
|
||||
backgroundPosition: true,
|
||||
backgroundRepeat: true,
|
||||
backgroundColor: true
|
||||
},
|
||||
border: {
|
||||
borderWidth: true,
|
||||
borderStyle: true,
|
||||
borderColor: true
|
||||
},
|
||||
borderBottom: {
|
||||
borderBottomWidth: true,
|
||||
borderBottomStyle: true,
|
||||
borderBottomColor: true
|
||||
},
|
||||
borderLeft: {
|
||||
borderLeftWidth: true,
|
||||
borderLeftStyle: true,
|
||||
borderLeftColor: true
|
||||
},
|
||||
borderRight: {
|
||||
borderRightWidth: true,
|
||||
borderRightStyle: true,
|
||||
borderRightColor: true
|
||||
},
|
||||
borderTop: {
|
||||
borderTopWidth: true,
|
||||
borderTopStyle: true,
|
||||
borderTopColor: true
|
||||
},
|
||||
font: {
|
||||
fontStyle: true,
|
||||
fontVariant: true,
|
||||
fontWeight: true,
|
||||
fontSize: true,
|
||||
lineHeight: true,
|
||||
fontFamily: true
|
||||
}
|
||||
};
|
||||
|
||||
var CSSProperty = {
|
||||
isUnitlessNumber: isUnitlessNumber
|
||||
isUnitlessNumber: isUnitlessNumber,
|
||||
shorthandPropertyExpansions: shorthandPropertyExpansions
|
||||
};
|
||||
|
||||
module.exports = CSSProperty;
|
||||
|
||||
@@ -19,10 +19,12 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var dangerousStyleValue = require('dangerousStyleValue');
|
||||
var escapeTextForBrowser = require('escapeTextForBrowser');
|
||||
var hyphenate = require('hyphenate');
|
||||
var memoizeStringOnly = require('memoizeStringOnly');
|
||||
var CSSProperty = require('CSSProperty');
|
||||
|
||||
var dangerousStyleValue = require('./dangerousStyleValue');
|
||||
var escapeTextForBrowser = require('./escapeTextForBrowser');
|
||||
var hyphenate = require('./hyphenate');
|
||||
var memoizeStringOnly = require('./memoizeStringOnly');
|
||||
|
||||
var processStyleName = memoizeStringOnly(function(styleName) {
|
||||
return escapeTextForBrowser(hyphenate(styleName));
|
||||
@@ -60,7 +62,8 @@ var CSSPropertyOperations = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the value for multiple styles on a node.
|
||||
* Sets the value for multiple styles on a node. If a value is specified as
|
||||
* '' (empty string), the corresponding style property will be unset.
|
||||
*
|
||||
* @param {DOMElement} node
|
||||
* @param {object} styles
|
||||
@@ -72,7 +75,20 @@ var CSSPropertyOperations = {
|
||||
continue;
|
||||
}
|
||||
var styleValue = styles[styleName];
|
||||
style[styleName] = dangerousStyleValue(styleName, styleValue);
|
||||
if (!styleValue) {
|
||||
var expansion = CSSProperty.shorthandPropertyExpansions[styleName];
|
||||
if (expansion) {
|
||||
// Shorthand property that IE8 won't like unsetting, so unset each
|
||||
// component to placate it
|
||||
for (var individualStyleName in expansion) {
|
||||
style[individualStyleName] = '';
|
||||
}
|
||||
} else {
|
||||
style[styleName] = '';
|
||||
}
|
||||
} else {
|
||||
style[styleName] = dangerousStyleValue(styleName, styleValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-4
@@ -23,6 +23,8 @@
|
||||
|
||||
var invariant = require('invariant');
|
||||
|
||||
var defaultValueCache = {};
|
||||
|
||||
/**
|
||||
* DOMProperty exports lookup objects that can be used like functions:
|
||||
*
|
||||
@@ -98,7 +100,29 @@ var DOMProperty = {
|
||||
*/
|
||||
isCustomAttribute: RegExp.prototype.test.bind(
|
||||
/^(data|aria)-[a-z_][a-z\d_.\-]*$/
|
||||
)
|
||||
),
|
||||
|
||||
/**
|
||||
* Returns the default property value for a DOM property (i.e., not an
|
||||
* attribute). Most default values are '' or false, but not all. Worse yet,
|
||||
* some (in particular, `type`) vary depending on the type of element.
|
||||
*
|
||||
* TODO: Is it worth caching the test elements? Caching the properties
|
||||
* ourselves (as opposed to accessing from a cached test element every time)
|
||||
* looks probably worth it: http://jsperf.com/object-vs-element
|
||||
*/
|
||||
getDefaultValueForProperty: function(nodeName, prop) {
|
||||
var nodeDefaults = defaultValueCache[nodeName];
|
||||
var testElement;
|
||||
if (!nodeDefaults) {
|
||||
defaultValueCache[nodeName] = nodeDefaults = {};
|
||||
}
|
||||
if (!(prop in nodeDefaults)) {
|
||||
testElement = document.createElement(nodeName);
|
||||
nodeDefaults[prop] = testElement[prop];
|
||||
}
|
||||
return nodeDefaults[prop];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -132,7 +156,7 @@ var Properties = {
|
||||
dir: null,
|
||||
disabled: MustUseProperty | HasBooleanValue,
|
||||
enctype: null,
|
||||
height: null,
|
||||
height: MustUseAttribute,
|
||||
href: null,
|
||||
htmlFor: null,
|
||||
max: null,
|
||||
@@ -158,7 +182,7 @@ var Properties = {
|
||||
title: null,
|
||||
type: null,
|
||||
value: MustUseProperty | HasSideEffects,
|
||||
width: null,
|
||||
width: MustUseAttribute,
|
||||
wmode: MustUseAttribute,
|
||||
/**
|
||||
* SVG Properties
|
||||
@@ -212,7 +236,8 @@ var DOMPropertyNames = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Properties that require special mutation methods.
|
||||
* Properties that require special mutation methods. If `value` is undefined,
|
||||
* the mutation method should unset the property.
|
||||
*/
|
||||
var DOMMutationMethods = {
|
||||
/**
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var DOMProperty = require('DOMProperty');
|
||||
var DOMProperty = require("./DOMProperty");
|
||||
|
||||
var escapeTextForBrowser = require('escapeTextForBrowser');
|
||||
var memoizeStringOnly = require('memoizeStringOnly');
|
||||
var escapeTextForBrowser = require("./escapeTextForBrowser");
|
||||
var memoizeStringOnly = require("./memoizeStringOnly");
|
||||
|
||||
var processAttributeNameAndPrefix = memoizeStringOnly(function(name) {
|
||||
return escapeTextForBrowser(name) + '="';
|
||||
@@ -86,6 +86,31 @@ var DOMPropertyOperations = {
|
||||
} else if (DOMProperty.isCustomAttribute(name)) {
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes the value for a property on a node.
|
||||
*
|
||||
* @param {DOMElement} node
|
||||
* @param {string} name
|
||||
*/
|
||||
deleteValueForProperty: function(node, name) {
|
||||
if (DOMProperty.isStandardName[name]) {
|
||||
var mutationMethod = DOMProperty.getMutationMethod[name];
|
||||
if (mutationMethod) {
|
||||
mutationMethod(node, undefined);
|
||||
} else if (DOMProperty.mustUseAttribute[name]) {
|
||||
node.removeAttribute(DOMProperty.getAttributeName[name]);
|
||||
} else {
|
||||
var propName = DOMProperty.getPropertyName[name];
|
||||
node[propName] = DOMProperty.getDefaultValueForProperty(
|
||||
node.nodeName,
|
||||
name
|
||||
);
|
||||
}
|
||||
} else if (DOMProperty.isCustomAttribute(name)) {
|
||||
node.removeAttribute(name);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -312,6 +312,7 @@ var EventPluginHub = {
|
||||
registrationNamesArr: registrationNamesArr,
|
||||
putListener: CallbackRegistry.putListener,
|
||||
getListener: CallbackRegistry.getListener,
|
||||
deleteListener: CallbackRegistry.deleteListener,
|
||||
deleteAllListeners: deleteAllListeners,
|
||||
extractAbstractEvents: extractAbstractEvents,
|
||||
enqueueAbstractEvents: enqueueAbstractEvents,
|
||||
|
||||
Reference in New Issue
Block a user