From b1fc5f949d10d75bc566b1be7722227043fe4c44 Mon Sep 17 00:00:00 2001 From: Michael Ridgway Date: Thu, 23 Jul 2015 16:41:47 -0700 Subject: [PATCH] Allow classes created with React.createClass to opt out of autobinding --- src/isomorphic/classic/class/ReactClass.js | 4 +- .../class/__tests__/ReactBindOptout-test.js | 234 ++++++++++++++++++ 2 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js diff --git a/src/isomorphic/classic/class/ReactClass.js b/src/isomorphic/classic/class/ReactClass.js index 74c25242a6..f8ddb2522f 100644 --- a/src/isomorphic/classic/class/ReactClass.js +++ b/src/isomorphic/classic/class/ReactClass.js @@ -395,6 +395,7 @@ var RESERVED_SPEC_KEYS = { statics: function(Constructor, statics) { mixStaticSpecIntoComponent(Constructor, statics); }, + autobind: function() {}, // noop }; function validateTypeDef(Constructor, typeDef, location) { @@ -499,7 +500,8 @@ function mixSpecIntoComponent(Constructor, spec) { var shouldAutoBind = isFunction && !isReactClassMethod && - !isAlreadyDefined; + !isAlreadyDefined && + spec.autobind !== false; if (shouldAutoBind) { if (!proto.__reactAutoBindMap) { diff --git a/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js b/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js new file mode 100644 index 0000000000..cf410eb4e9 --- /dev/null +++ b/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js @@ -0,0 +1,234 @@ +/** + * 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. + * + * @emails react-core + */ +/*global global:true*/ +'use strict'; + +var mocks = require('mocks'); +var React = require('React'); +var ReactTestUtils = require('ReactTestUtils'); +var reactComponentExpect = require('reactComponentExpect'); + +// TODO: Test render and all stock methods. +describe('autobind optout', function() { + + it('should work with manual binding', function() { + + var mouseDidEnter = mocks.getMockFunction(); + var mouseDidLeave = mocks.getMockFunction(); + var mouseDidClick = mocks.getMockFunction(); + + var TestBindComponent = React.createClass({ + autobind: false, + getInitialState: function() { + return {something: 'hi'}; + }, + onMouseEnter: mouseDidEnter, + onMouseLeave: mouseDidLeave, + onClick: mouseDidClick, + + render: function() { + return ( +
+ ); + }, + }); + + var instance1 = ; + var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); + var rendered1 = reactComponentExpect(mountedInstance1) + .expectRenderedChild() + .instance(); + + var instance2 = ; + var mountedInstance2 = ReactTestUtils.renderIntoDocument(instance2); + var rendered2 = reactComponentExpect(mountedInstance2) + .expectRenderedChild() + .instance(); + + ReactTestUtils.Simulate.click(rendered1); + expect(mouseDidClick.mock.instances.length).toBe(1); + expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); + + ReactTestUtils.Simulate.click(rendered2); + expect(mouseDidClick.mock.instances.length).toBe(2); + expect(mouseDidClick.mock.instances[1]).toBe(mountedInstance2); + + ReactTestUtils.Simulate.mouseOver(rendered1); + expect(mouseDidEnter.mock.instances.length).toBe(1); + expect(mouseDidEnter.mock.instances[0]).toBe(mountedInstance1); + + ReactTestUtils.Simulate.mouseOver(rendered2); + expect(mouseDidEnter.mock.instances.length).toBe(2); + expect(mouseDidEnter.mock.instances[1]).toBe(mountedInstance2); + + ReactTestUtils.Simulate.mouseOut(rendered1); + expect(mouseDidLeave.mock.instances.length).toBe(1); + expect(mouseDidLeave.mock.instances[0]).toBe(mountedInstance1); + + ReactTestUtils.Simulate.mouseOut(rendered2); + expect(mouseDidLeave.mock.instances.length).toBe(2); + expect(mouseDidLeave.mock.instances[1]).toBe(mountedInstance2); + }); + + it('should not hold reference to instance', function() { + var mouseDidClick = function () { + void this.state.something; + }; + + var TestBindComponent = React.createClass({ + autobind: false, + getInitialState: function() { + return {something: 'hi'}; + }, + onClick: mouseDidClick, + + // auto binding only occurs on top level functions in class defs. + badIdeas: { + badBind: function() { + void this.state.something; + }, + }, + + render: function() { + return ( +
+ ); + }, + }); + + var instance1 = ; + var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); + var rendered1 = reactComponentExpect(mountedInstance1) + .expectRenderedChild() + .instance(); + + var instance2 = ; + var mountedInstance2 = ReactTestUtils.renderIntoDocument(instance2); + var rendered2 = reactComponentExpect(mountedInstance2) + .expectRenderedChild() + .instance(); + + expect(function() { + var badIdea = instance1.badIdeas.badBind; + badIdea(); + }).toThrow(); + + expect(mountedInstance1.onClick).toBe(mountedInstance2.onClick); + + expect(function () { + ReactTestUtils.Simulate.click(rendered1); + }).toThrow(); + + expect(function () { + ReactTestUtils.Simulate.click(rendered2); + }).toThrow(); + }); + + it('works with mixins that have not opted out of autobinding', function() { + var mouseDidClick = mocks.getMockFunction(); + + var TestMixin = { + onClick: mouseDidClick, + }; + + var TestBindComponent = React.createClass({ + mixins: [TestMixin], + + render: function() { + return
; + }, + }); + + var instance1 = ; + var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); + var rendered1 = reactComponentExpect(mountedInstance1) + .expectRenderedChild() + .instance(); + + ReactTestUtils.Simulate.click(rendered1); + expect(mouseDidClick.mock.instances.length).toBe(1); + expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); + }); + + it('works with mixins that have opted out of autobinding', function() { + var mouseDidClick = mocks.getMockFunction(); + + var TestMixin = { + autobind: false, + onClick: mouseDidClick, + }; + + var TestBindComponent = React.createClass({ + mixins: [TestMixin], + + render: function() { + return
; + }, + }); + + var instance1 = ; + var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); + var rendered1 = reactComponentExpect(mountedInstance1) + .expectRenderedChild() + .instance(); + + ReactTestUtils.Simulate.click(rendered1); + expect(mouseDidClick.mock.instances.length).toBe(1); + expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); + }); + + it('does not warn if you try to bind to this', function() { + spyOn(console, 'error'); + + var TestBindComponent = React.createClass({ + autobind: false, + handleClick: function() { }, + render: function() { + return
; + }, + }); + + ReactTestUtils.renderIntoDocument(); + + expect(console.error.argsForCall.length).toBe(0); + }); + + it('does not warn if you pass an manually bound method to setState', function() { + spyOn(console, 'error'); + + var TestBindComponent = React.createClass({ + autobind: false, + getInitialState: function() { + return {foo: 1}; + }, + componentDidMount: function() { + this.setState({foo: 2}, this.handleUpdate.bind(this)); + }, + handleUpdate: function() { + + }, + render: function() { + return
; + }, + }); + + ReactTestUtils.renderIntoDocument(); + + expect(console.error.argsForCall.length).toBe(0); + }); + +});