diff --git a/src/renderers/dom/shared/DOMPropertyOperations.js b/src/renderers/dom/shared/DOMPropertyOperations.js index 7eb824037761..e674bc1706f0 100644 --- a/src/renderers/dom/shared/DOMPropertyOperations.js +++ b/src/renderers/dom/shared/DOMPropertyOperations.js @@ -60,7 +60,7 @@ if (__DEV__) { }; var warnedProperties = {}; - var warnUnknownProperty = function(name) { + var warnUnknownProperty = function(name, debugPath) { if (reactProps.hasOwnProperty(name) && reactProps[name] || warnedProperties.hasOwnProperty(name) && warnedProperties[name]) { return; @@ -82,8 +82,9 @@ if (__DEV__) { // logging too much when using transferPropsTo. warning( standardName == null, - 'Unknown DOM property %s. Did you mean %s?', + 'Unknown DOM property %s of `%s`. Did you mean %s?', name, + debugPath != null ? debugPath.toString() : 'DOM component', standardName ); @@ -117,7 +118,7 @@ var DOMPropertyOperations = { * @param {*} value * @return {?string} Markup string, or null if the property was invalid. */ - createMarkupForProperty: function(name, value) { + createMarkupForProperty: function(name, value, debugPath) { var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null; if (propertyInfo) { @@ -136,7 +137,7 @@ var DOMPropertyOperations = { } return name + '=' + quoteAttributeValueForBrowser(value); } else if (__DEV__) { - warnUnknownProperty(name); + warnUnknownProperty(name, debugPath); } return null; }, @@ -162,7 +163,7 @@ var DOMPropertyOperations = { * @param {string} name * @param {*} value */ - setValueForProperty: function(node, name, value) { + setValueForProperty: function(node, name, value, debugPath) { var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null; if (propertyInfo) { @@ -198,7 +199,7 @@ var DOMPropertyOperations = { } else if (DOMProperty.isCustomAttribute(name)) { DOMPropertyOperations.setValueForAttribute(node, name, value); } else if (__DEV__) { - warnUnknownProperty(name); + warnUnknownProperty(name, debugPath); } }, @@ -219,7 +220,7 @@ var DOMPropertyOperations = { * @param {DOMElement} node * @param {string} name */ - deleteValueForProperty: function(node, name) { + deleteValueForProperty: function(node, name, debugPath) { var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null; if (propertyInfo) { @@ -242,7 +243,7 @@ var DOMPropertyOperations = { } else if (DOMProperty.isCustomAttribute(name)) { node.removeAttribute(name); } else if (__DEV__) { - warnUnknownProperty(name); + warnUnknownProperty(name, debugPath); } }, diff --git a/src/renderers/dom/shared/ReactDOMComponent.js b/src/renderers/dom/shared/ReactDOMComponent.js index cf7c1587c1bc..f072e2aab990 100644 --- a/src/renderers/dom/shared/ReactDOMComponent.js +++ b/src/renderers/dom/shared/ReactDOMComponent.js @@ -23,6 +23,7 @@ var EventConstants = require('EventConstants'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); var ReactComponentBrowserEnvironment = require('ReactComponentBrowserEnvironment'); +var ReactDebugPath = require('ReactDebugPath'); var ReactDOMButton = require('ReactDOMButton'); var ReactDOMInput = require('ReactDOMInput'); var ReactDOMOption = require('ReactDOMOption'); @@ -513,6 +514,7 @@ function ReactDOMComponent(tag) { this._nativeContainerInfo = null; this._wrapperState = null; this._topLevelWrapper = null; + this._debugPath = null; this._nodeHasLegacyProperties = false; if (__DEV__) { this._ancestorInfo = null; @@ -544,13 +546,18 @@ ReactDOMComponent.Mixin = { transaction, nativeParent, nativeContainerInfo, - context + context, + debugPath ) { this._rootNodeID = rootID; this._nativeContainerInfo = nativeContainerInfo; var props = this._currentElement.props; + if (__DEV__) { + this._debugPath = new ReactDebugPath(debugPath, this._tag); + } + switch (this._tag) { case 'iframe': case 'img': @@ -729,7 +736,7 @@ ReactDOMComponent.Mixin = { if (this._tag != null && isCustomComponent(this._tag, props)) { markup = DOMPropertyOperations.createMarkupForCustomAttribute(propKey, propValue); } else { - markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue); + markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue, this._debugPath); } if (markup) { ret += ' ' + markup; @@ -1008,9 +1015,9 @@ ReactDOMComponent.Mixin = { // from the DOM node instead of inadvertantly setting to a string. This // brings us in line with the same behavior we have on initial render. if (nextProp != null) { - DOMPropertyOperations.setValueForProperty(node, propKey, nextProp); + DOMPropertyOperations.setValueForProperty(node, propKey, nextProp, this._debugPath); } else { - DOMPropertyOperations.deleteValueForProperty(node, propKey); + DOMPropertyOperations.deleteValueForProperty(node, propKey, this._debugPath); } } } diff --git a/src/renderers/dom/shared/ReactDOMTextComponent.js b/src/renderers/dom/shared/ReactDOMTextComponent.js index ded6a67381d9..97c09743bda0 100644 --- a/src/renderers/dom/shared/ReactDOMTextComponent.js +++ b/src/renderers/dom/shared/ReactDOMTextComponent.js @@ -81,7 +81,8 @@ assign(ReactDOMTextComponent.prototype, { transaction, nativeParent, nativeContainerInfo, - context + context, + debugPath ) { if (__DEV__) { var parentInfo; diff --git a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js index 08ee73dbbdce..f10c11231c00 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js @@ -847,6 +847,24 @@ describe('ReactDOMComponent', function() { }); }); + describe('unknown attributes', function() { + var ReactTestUtils; + + beforeEach(function() { + ReactTestUtils = require('ReactTestUtils'); + }); + + it('should warn for incorrect case', () => { + spyOn(console, 'error'); + ReactTestUtils.renderIntoDocument(
link
); + + expect(console.error.calls.length).toBe(1); + expect(console.error.calls[0].args[0]).toBe( + 'Warning: Unknown DOM property HREF of `TopLevelWrapper > div > span > a`. Did you mean href?' + ); + }); + }); + describe('nesting validation', function() { var ReactTestUtils; diff --git a/src/renderers/shared/reconciler/ReactCompositeComponent.js b/src/renderers/shared/reconciler/ReactCompositeComponent.js index 0e30a0128d8e..b6d1af6c6f50 100644 --- a/src/renderers/shared/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/reconciler/ReactCompositeComponent.js @@ -13,6 +13,7 @@ var ReactComponentEnvironment = require('ReactComponentEnvironment'); var ReactCurrentOwner = require('ReactCurrentOwner'); +var ReactDebugPath = require('ReactDebugPath'); var ReactElement = require('ReactElement'); var ReactInstanceMap = require('ReactInstanceMap'); var ReactPerf = require('ReactPerf'); @@ -97,6 +98,7 @@ var ReactCompositeComponentMixin = { this._rootNodeID = null; this._instance = null; this._nativeParent = null; + this._debugPath = null; this._nativeContainerInfo = null; // See ReactUpdateQueue @@ -129,7 +131,8 @@ var ReactCompositeComponentMixin = { transaction, nativeParent, nativeContainerInfo, - context + context, + debugPath ) { this._context = context; this._mountOrder = nextMountID++; @@ -137,6 +140,10 @@ var ReactCompositeComponentMixin = { this._nativeParent = nativeParent; this._nativeContainerInfo = nativeContainerInfo; + if (__DEV__) { + this._debugPath = new ReactDebugPath(debugPath, this.getName() || 'ReactCompositeComponent'); + } + var publicProps = this._processProps(this._currentElement.props); var publicContext = this._processContext(context); @@ -300,7 +307,8 @@ var ReactCompositeComponentMixin = { transaction, nativeParent, nativeContainerInfo, - this._processChildContext(context) + this._processChildContext(context), + this._debugPath ); if (inst.componentDidMount) { transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); @@ -761,7 +769,8 @@ var ReactCompositeComponentMixin = { transaction, this._nativeParent, this._nativeContainerInfo, - this._processChildContext(context) + this._processChildContext(context), + this._debugPath ); this._replaceNodeWithMarkup(oldNativeNode, nextMarkup); } diff --git a/src/renderers/shared/reconciler/ReactDebugPath.js b/src/renderers/shared/reconciler/ReactDebugPath.js new file mode 100644 index 000000000000..d206cc365eca --- /dev/null +++ b/src/renderers/shared/reconciler/ReactDebugPath.js @@ -0,0 +1,29 @@ +/** + * Copyright 2013-2015, 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 ReactDebugPath + */ + +'use strict'; + +class ReactDebugPath +{ + constructor(parent, componentName) { + this.parent = parent; + this.componentName = componentName; + } + + toString() { + if (this.parent != null) { + return this.parent.toString() + ' > ' + this.componentName; + } + return this.componentName; + } +} + +module.exports = ReactDebugPath; diff --git a/src/renderers/shared/reconciler/ReactEmptyComponent.js b/src/renderers/shared/reconciler/ReactEmptyComponent.js index 48430d600972..dd65b67b8051 100644 --- a/src/renderers/shared/reconciler/ReactEmptyComponent.js +++ b/src/renderers/shared/reconciler/ReactEmptyComponent.js @@ -38,7 +38,8 @@ assign(ReactEmptyComponent.prototype, { transaction, nativeParent, nativeContainerInfo, - context + context, + debugPath ) { ReactEmptyComponentRegistry.registerNullComponentID(rootID); this._rootNodeID = rootID; diff --git a/src/renderers/shared/reconciler/ReactMultiChild.js b/src/renderers/shared/reconciler/ReactMultiChild.js index 3dbaa4651f32..77c19f80bdbc 100644 --- a/src/renderers/shared/reconciler/ReactMultiChild.js +++ b/src/renderers/shared/reconciler/ReactMultiChild.js @@ -257,7 +257,8 @@ var ReactMultiChild = { transaction, this, this._nativeContainerInfo, - context + context, + this._debugPath ); child._mountIndex = index++; mountImages.push(mountImage); diff --git a/src/renderers/shared/reconciler/ReactReconciler.js b/src/renderers/shared/reconciler/ReactReconciler.js index 7b3f453e1aab..f603a8faa31d 100644 --- a/src/renderers/shared/reconciler/ReactReconciler.js +++ b/src/renderers/shared/reconciler/ReactReconciler.js @@ -41,14 +41,16 @@ var ReactReconciler = { transaction, nativeParent, nativeContainerInfo, - context + context, + debugPath ) { var markup = internalInstance.mountComponent( rootID, transaction, nativeParent, nativeContainerInfo, - context + context, + debugPath ); if (internalInstance._currentElement && internalInstance._currentElement.ref != null) {