diff --git a/src/addons/transitions/ReactTransitionGroup.js b/src/addons/transitions/ReactTransitionGroup.js index 95dea6b752..623838c49f 100644 --- a/src/addons/transitions/ReactTransitionGroup.js +++ b/src/addons/transitions/ReactTransitionGroup.js @@ -102,7 +102,12 @@ var ReactTransitionGroup = React.createClass({ render: function() { return this.transferPropsTo( this.props.component( - null, + { + transitionName: null, + transitionEnter: null, + transitionLeave: null, + component: null + }, this.renderTransitionableChildren(this.props.children) ) ); diff --git a/src/dom/DOMProperty.js b/src/dom/DOMProperty.js index 1bded212d9..fa031bb87a 100644 --- a/src/dom/DOMProperty.js +++ b/src/dom/DOMProperty.js @@ -82,8 +82,11 @@ var DOMPropertyInjection = { DOMProperty.isStandardName[propName] = true; + var lowerCased = propName.toLowerCase(); + DOMProperty.getPossibleStandardName[lowerCased] = propName; + DOMProperty.getAttributeName[propName] = - DOMAttributeNames[propName] || propName.toLowerCase(); + DOMAttributeNames[propName] || lowerCased; DOMProperty.getPropertyName[propName] = DOMPropertyNames[propName] || propName; @@ -141,6 +144,13 @@ var DOMProperty = { */ isStandardName: {}, + /** + * Mapping from lowercase property names to the properly cased version, used + * to warn in the case of missing properties. + * @type {Object} + */ + getPossibleStandardName: {}, + /** * Mapping from normalized names to attribute names that differ. Attribute * names are used when rendering markup or with `*Attribute()`. diff --git a/src/dom/DOMPropertyOperations.js b/src/dom/DOMPropertyOperations.js index cb5d55ad4c..4d4f6b4385 100644 --- a/src/dom/DOMPropertyOperations.js +++ b/src/dom/DOMPropertyOperations.js @@ -28,6 +28,36 @@ var processAttributeNameAndPrefix = memoizeStringOnly(function(name) { return escapeTextForBrowser(name) + '="'; }); +if (__DEV__) { + var reactProps = { + '{owner}': true, + children: true, + dangerouslySetInnerHTML: true, + key: true, + ref: true + }; + var warnedProperties = {}; + + var warnUnknownProperty = function(name) { + if (reactProps[name] || warnedProperties[name]) { + return; + } + + warnedProperties[name] = true; + var message = 'Unknown DOM property ' + name + '.'; + var lowerCasedName = name.toLowerCase(); + + // data-* attributes should be lowercase; suggest the lowercase version + var standardName = DOMProperty.isCustomAttribute(lowerCasedName) ? + lowerCasedName : DOMProperty.getPossibleStandardName[lowerCasedName]; + if (standardName != null) { + message += ' Did you mean ' + standardName + '?'; + } + + console.warn(message); + }; +} + /** * Operations for dealing with DOM properties. */ @@ -55,6 +85,9 @@ var DOMPropertyOperations = { return processAttributeNameAndPrefix(name) + escapeTextForBrowser(value) + '"'; } else { + if (__DEV__) { + warnUnknownProperty(name); + } return null; } }, @@ -85,6 +118,10 @@ var DOMPropertyOperations = { } } else if (DOMProperty.isCustomAttribute(name)) { node.setAttribute(name, value); + } else { + if (__DEV__) { + warnUnknownProperty(name); + } } }, @@ -110,6 +147,8 @@ var DOMPropertyOperations = { } } else if (DOMProperty.isCustomAttribute(name)) { node.removeAttribute(name); + } else if (__DEV__) { + warnUnknownProperty(name); } } diff --git a/src/dom/__tests__/DOMPropertyOperations-test.js b/src/dom/__tests__/DOMPropertyOperations-test.js index acc10d6074..ec28cfa970 100644 --- a/src/dom/__tests__/DOMPropertyOperations-test.js +++ b/src/dom/__tests__/DOMPropertyOperations-test.js @@ -58,6 +58,16 @@ describe('DOMPropertyOperations', function() { )).toBe('id="simple"'); }); + it('should warn about incorrect casing', function() { + spyOn(console, 'warn'); + expect(DOMPropertyOperations.createMarkupForProperty( + 'tabindex', + '1' + )).toBe(null); + expect(console.warn.argsForCall.length).toBe(1); + expect(console.warn.argsForCall[0][0]).toContain('tabIndex'); + }); + it('should create markup for boolean properties', function() { expect(DOMPropertyOperations.createMarkupForProperty( 'checked', @@ -141,18 +151,24 @@ describe('DOMPropertyOperations', function() { describe('injectDOMPropertyConfig', function() { it('should support custom attributes', function() { + spyOn(console, 'warn'); + // foobar does not exist yet expect(DOMPropertyOperations.createMarkupForProperty( 'foobar', 'simple' )).toBe(null); + expect(console.warn.argsForCall.length).toBe(1); + // foo-* does not exist yet expect(DOMPropertyOperations.createMarkupForProperty( 'foo-xyz', 'simple' )).toBe(null); + expect(console.warn.argsForCall.length).toBe(2); + // inject foobar DOM property DOMProperty.injection.injectDOMPropertyConfig({ isCustomAttribute: function(name) { diff --git a/src/dom/components/ReactDOMInput.js b/src/dom/components/ReactDOMInput.js index 924a5768ae..85b2f33248 100644 --- a/src/dom/components/ReactDOMInput.js +++ b/src/dom/components/ReactDOMInput.js @@ -61,6 +61,8 @@ var ReactDOMInput = ReactCompositeComponent.createClass({ // Clone `this.props` so we don't mutate the input. var props = merge(this.props); + props.defaultChecked = null; + props.defaultValue = null; props.checked = this.props.checked != null ? this.props.checked : this.state.checked; // Cast `this.props.value` to a string so equality checks pass. diff --git a/src/dom/components/ReactDOMTextarea.js b/src/dom/components/ReactDOMTextarea.js index 4682d74355..78db712093 100644 --- a/src/dom/components/ReactDOMTextarea.js +++ b/src/dom/components/ReactDOMTextarea.js @@ -102,6 +102,7 @@ var ReactDOMTextarea = ReactCompositeComponent.createClass({ '`dangerouslySetInnerHTML` does not make sense on