From 0c6f4b3bcc8dc728f4736e53e3d81d6ae4c47cb9 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Wed, 12 Jun 2013 00:14:58 -0700 Subject: [PATCH 01/94] Set textarea value when changing content At http://jsfiddle.net/spicyj/W4QLq/, typing into the textbox would cause clicking the button to do nothing; now it should work. --- src/core/ReactDOM.js | 1 + src/core/ReactDOMTextarea.js | 63 ++++++++++++++++++++ src/core/ReactDefaultInjection.js | 4 +- src/core/__tests__/ReactDOMTextarea-test.js | 66 +++++++++++++++++++++ 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/core/ReactDOMTextarea.js create mode 100644 src/core/__tests__/ReactDOMTextarea-test.js diff --git a/src/core/ReactDOM.js b/src/core/ReactDOM.js index 2fe78e99a7..a3c470c1ce 100644 --- a/src/core/ReactDOM.js +++ b/src/core/ReactDOM.js @@ -115,6 +115,7 @@ var ReactDOM = objMapKeyVal({ table: false, tbody: false, td: false, + // Danger: this gets monkeypatched! See ReactDOMTextarea for more info. textarea: false, tfoot: false, th: false, diff --git a/src/core/ReactDOMTextarea.js b/src/core/ReactDOMTextarea.js new file mode 100644 index 0000000000..d193d7cffe --- /dev/null +++ b/src/core/ReactDOMTextarea.js @@ -0,0 +1,63 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule ReactDOMTextarea + */ + +"use strict"; + +var ReactCompositeComponent = require('ReactCompositeComponent'); +var ReactDOM = require('ReactDOM'); + +var invariant = require('invariant'); + +// Store a reference to the ); + + expect(stub.getDOMNode().value).toEqual('giraffe'); + stub.receiveProps({ children: null }, transaction); + expect(stub.getDOMNode().value).toEqual(''); + }); + + it("should update value with a single string child", function() { + var stub = ReactTestUtils.renderIntoDocument(); + + expect(stub.getDOMNode().value).toEqual('monkey'); + stub.receiveProps({ children: 'gorilla' }, transaction); + expect(stub.getDOMNode().value).toEqual('gorilla'); + }); + + it("should update value with a single numerical child", function() { + var stub = ReactTestUtils.renderIntoDocument(); + + expect(stub.getDOMNode().value).toEqual('17'); + stub.receiveProps({ children: 289 }, transaction); + expect(stub.getDOMNode().value).toEqual('289'); + }); + + it("should update value with the content property", function() { + var stub = ReactTestUtils.renderIntoDocument(); expect(stub.getDOMNode().value).toEqual('giraffe'); - stub.receiveProps({ children: null }, transaction); + stub.replaceProps({ children: null }); expect(stub.getDOMNode().value).toEqual(''); }); @@ -44,7 +41,7 @@ describe('ReactDOMTextarea', function() { var stub = ReactTestUtils.renderIntoDocument(); expect(stub.getDOMNode().value).toEqual('monkey'); - stub.receiveProps({ children: 'gorilla' }, transaction); + stub.replaceProps({ children: 'gorilla' }); expect(stub.getDOMNode().value).toEqual('gorilla'); }); @@ -52,7 +49,7 @@ describe('ReactDOMTextarea', function() { var stub = ReactTestUtils.renderIntoDocument(); expect(stub.getDOMNode().value).toEqual('17'); - stub.receiveProps({ children: 289 }, transaction); + stub.replaceProps({ children: 289 }); expect(stub.getDOMNode().value).toEqual('289'); }); @@ -60,7 +57,7 @@ describe('ReactDOMTextarea', function() { var stub = ReactTestUtils.renderIntoDocument(); + var node = stub.getDOMNode(); - expect(stub.getDOMNode().value).toEqual('giraffe'); - stub.replaceProps({ children: null }); - expect(stub.getDOMNode().value).toEqual(''); - }); + var nodeText = node[textContentAccessor]; + var nodeTextSetter = mocks.getMockFunction(); + Object.defineProperty(node, textContentAccessor, { + get: function() { + return nodeText; + }, + set: nodeTextSetter.mockImplementation(function(newText) { + nodeText = newText; + }) + }); - it("should update value with a single string child", function() { - var stub = ReactTestUtils.renderIntoDocument(); + expect(node.value).toEqual('giraffe'); - expect(stub.getDOMNode().value).toEqual('monkey'); stub.replaceProps({ children: 'gorilla' }); - expect(stub.getDOMNode().value).toEqual('gorilla'); - }); + expect(node.value).toEqual('gorilla'); - it("should update value with a single numerical child", function() { - var stub = ReactTestUtils.renderIntoDocument(); + stub.replaceProps({ children: 17 }); + expect(node.value).toEqual('17'); - expect(stub.getDOMNode().value).toEqual('17'); - stub.replaceProps({ children: 289 }); - expect(stub.getDOMNode().value).toEqual('289'); - }); + stub.replaceProps({ children: [42] }); + expect(node.value).toEqual('42'); - it("should update value with the content property", function() { - var stub = ReactTestUtils.renderIntoDocument( + ); + }).toThrow(); + + expect(function() { + var stub = ReactTestUtils.renderIntoDocument( + + ); + }).toThrow(); }); }); From cb01363260b6e43981c286cac9818a13dc1e6a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Fri, 7 Jun 2013 15:35:42 -0700 Subject: [PATCH 35/94] Cleanup lint warnings --- grunt/tasks/npm.js | 5 ++--- grunt/tasks/phantom.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/grunt/tasks/npm.js b/grunt/tasks/npm.js index becc96a894..8fe6b7ad32 100644 --- a/grunt/tasks/npm.js +++ b/grunt/tasks/npm.js @@ -2,13 +2,11 @@ var assert = require("assert"); var path = require("path"); -var fs = require("fs"); var tmp = require("tmp"); var grunt = require("grunt"); var spawn = grunt.util.spawn; module.exports = function() { - var config = this.data; var done = this.async(); function run(cmd, args, opts, callback) { @@ -25,7 +23,8 @@ module.exports = function() { grunt.log.writeln("> " + cmd + " " + args.join(" ")); - var proc = spawn({ + // var proc = + spawn({ cmd: cmd, args: args, opts: opts diff --git a/grunt/tasks/phantom.js b/grunt/tasks/phantom.js index ff71f683c6..d5c66eb258 100644 --- a/grunt/tasks/phantom.js +++ b/grunt/tasks/phantom.js @@ -38,7 +38,7 @@ function run(config, done) { } args.push("--tests"); - var tests = grunt.file.expand({ + grunt.file.expand({ nonull: true, cwd: "src" }, config.tests || []).forEach(function(file) { From 32030687bacda1da4952ada7710956600960902c Mon Sep 17 00:00:00 2001 From: Vjeux Date: Wed, 19 Jun 2013 19:39:11 +0200 Subject: [PATCH 36/94] Community Roundup #2 --- docs/_posts/2013-06-19-community-roundup-2.md | 71 ++++++++++++++++++ docs/docs/jsx-is-not-html.md | 2 +- docs/img/blog/chatapp.png | Bin 0 -> 27896 bytes docs/img/blog/todomvc.png | Bin 0 -> 52203 bytes docs/img/blog/xreact.png | Bin 0 -> 7913 bytes 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 docs/_posts/2013-06-19-community-roundup-2.md create mode 100644 docs/img/blog/chatapp.png create mode 100644 docs/img/blog/todomvc.png create mode 100644 docs/img/blog/xreact.png diff --git a/docs/_posts/2013-06-19-community-roundup-2.md b/docs/_posts/2013-06-19-community-roundup-2.md new file mode 100644 index 0000000000..57b322e1cc --- /dev/null +++ b/docs/_posts/2013-06-19-community-roundup-2.md @@ -0,0 +1,71 @@ +--- +title: "Community Round-up #2" +layout: post +author: Vjeux +--- + +Since the launch we have received a lot of feedback and are actively working on React 0.4. In the meantime, here are the highlights of this week. + +## Some quick thoughts on React + +[Andrew Greig](http://www.andrewgreig.com/) made a blog post that gives a high level description of what React is. + +> I have been using Facebooks recently released Javascript framework called React.js for the last few days and have managed to obtain a rather high level understanding of how it works and formed a good perspective on how it fits in to the entire javascript framework ecosystem. +> +> Basically, React is not an MVC framework. It is not a replacement for Backbone or Knockout or Angular, instead it is designed to work with existing frameworks and help extend their functionality. +> +> It is designed for building big UIs. The type where you have lots of reusable components that are handling events and presenting and changing some backend data. In a traditional MVC app, React fulfils the role of the View. So you would still need to handle the Model and Controller on your own. +> +> I found the best way to utilise React was to pair it with Backbone, with React replacing the Backbone View, or to write your own Model/Data object and have React communicate with that. +> +> [Read the full post...](http://www.andrewgreig.com/637/) + +## React and Socket.IO Chat Application + +[Danial Khosravi](http://danialk.github.io/) made a real-time chat application that interacts with the back-end using Socket.IO. + +> A week ago I was playing with AngularJS and [this little chat application](https://github.com/btford/angular-socket-io-im) which uses socket.io and nodejs for realtime communication. Yesterday I saw a post about ReactJS in [EchoJS](http://www.echojs.com/) and started playing with this UI library. After playing a bit with React, I decided to write and chat application using React and I used Bran Ford's Backend for server side of this little app. +>
[![](/react/img/blog/chatapp.png)](http://danialk.github.io/blog/2013/06/16/reactjs-and-socket-dot-io-chat-application/)
+> +> [Read the full post...](http://danialk.github.io/blog/2013/06/16/reactjs-and-socket-dot-io-chat-application/) + +## React and Other Frameworks + +[Pete Hunt](http://www.petehunt.net/blog/) wrote an answer on Quora comparing React and Angular directives. At the end, he explains how you can make an Angular directive that is in fact being rendered with React. + +> To set the record straight: React components are far more powerful than Angular templates; they should be compared with Angular's directives instead. So I took the first Google hit for "AngularJS directive tutorial" (AngularJS Directives Tutorial - Fundoo Solutions), rewrote it in React and compared them. [...] +> +> We've designed React from the beginning to work well with other libraries. Angular is no exception. Let's take the original Angular example and use React to implement the fundoo-rating directive. +> +> [Read the full post...](http://www.quora.com/Pete-Hunt/Posts/Facebooks-React-vs-AngularJS-A-Closer-Look) + +In the same vein, [Markov Twain](https://twitter.com/markov_twain/status/345702941845499906) re-implemented the examples on the front-page [with Ember](http://jsbin.com/azihiw/2/edit) and [Vlad Yazhbin](https://twitter.com/vla) re-implemented the tutorial [with Angular](http://jsfiddle.net/vla/Cdrse/). + +## Web Components: React & x-tags + +Mozilla and Google are actively working on Web Components. [Vjeux](http://blog.vjeux.com/) wrote a proof of concept that shows how to implement them using React. + +> Using [x-tags](http://www.x-tags.org/) from Mozilla, we can write custom tags within the DOM. This is a great opportunity to be able to write reusable components without being tied to a particular library. I wrote [x-react](https://github.com/vjeux/react-xtags/) to have them being rendered in React. +>
[![](/react/img/blog/xreact.png)](http://blog.vjeux.com/2013/javascript/custom-components-react-x-tags.html)
+> +> [Read the full post...](http://blog.vjeux.com/2013/javascript/custom-components-react-x-tags.html) + +## React TodoMVC Example + +[TodoMVC.com](http://todomvc.com/) is a website that collects various implementations of the same basic Todo app. [Pete Hunt](http://www.petehunt.net/blog/) wrote an idiomatic React version. + +> Developers these days are spoiled with choice when it comes to selecting an MV* framework for structuring and organizing their JavaScript web apps. +> +> To help solve this problem, we created TodoMVC - a project which offers the same Todo application implemented using MV* concepts in most of the popular JavaScript MV* frameworks of today. +>
[![](/react/img/blog/todomvc.png)](http://todomvc.com/labs/architecture-examples/react/)
+> +> [Read the source code...](https://github.com/tastejs/todomvc/tree/gh-pages/labs/architecture-examples/react) + +## JSX is not HTML + +Many of you pointed out differences between JSX and HTML. In order to clear up some confusion, we have added some documentation that covers the four main differences: + + - [Whitespace removal](http://facebook.github.io/react/docs/jsx-is-not-html.html) + - [HTML Entities](http://facebook.github.io/react/docs/jsx-is-not-html.html) + - [Comments](http://facebook.github.io/react/docs/jsx-is-not-html.html) + - [Custom HTML Attributes](http://facebook.github.io/react/docs/jsx-is-not-html.html) diff --git a/docs/docs/jsx-is-not-html.md b/docs/docs/jsx-is-not-html.md index 084c97197b..e8d0d7bac6 100644 --- a/docs/docs/jsx-is-not-html.md +++ b/docs/docs/jsx-is-not-html.md @@ -80,7 +80,7 @@ As of React 0.3, there is no good way to insert comments within the children sec ``` -## Custom HTML attributes +## Custom HTML Attributes If you pass properties to native HTML elements that do not exist in the HTML specification, React will not render them. If you want to use a custom attribute, you should prefix it with `data-`. diff --git a/docs/img/blog/chatapp.png b/docs/img/blog/chatapp.png new file mode 100644 index 0000000000000000000000000000000000000000..55edf29dd0675f5ce9f0e02ccf744d3a9fa26bc7 GIT binary patch literal 27896 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 literal 0 HcmV?d00001 diff --git a/docs/img/blog/todomvc.png b/docs/img/blog/todomvc.png new file mode 100644 index 0000000000000000000000000000000000000000..9c03bc0f6225ee71a97e4ae34e36e4dbd0229704 GIT binary patch literal 52203 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 JDk+no{{apE4hsMP literal 0 HcmV?d00001 From ac5320e8871e87a01d22a69e2d9dc1ab7f781515 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Wed, 19 Jun 2013 17:26:29 -0700 Subject: [PATCH 37/94] Remove textContent tests; they break in phantomjs --- src/core/__tests__/ReactDOMTextarea-test.js | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/core/__tests__/ReactDOMTextarea-test.js b/src/core/__tests__/ReactDOMTextarea-test.js index 0e600625c6..0ed7417d6a 100644 --- a/src/core/__tests__/ReactDOMTextarea-test.js +++ b/src/core/__tests__/ReactDOMTextarea-test.js @@ -19,8 +19,6 @@ "use strict"; -var mocks = require('mocks'); - describe('ReactDOMTextarea', function() { var React; var ReactTestUtils; @@ -29,25 +27,12 @@ describe('ReactDOMTextarea', function() { beforeEach(function() { React = require('React'); ReactTestUtils = require('ReactTestUtils'); - getTextContentAccessor = require('getTextContentAccessor'); }); - it("should update value and not update textContent", function() { - var textContentAccessor = getTextContentAccessor(); + it("should update value", function() { var stub = ReactTestUtils.renderIntoDocument(); var node = stub.getDOMNode(); - var nodeText = node[textContentAccessor]; - var nodeTextSetter = mocks.getMockFunction(); - Object.defineProperty(node, textContentAccessor, { - get: function() { - return nodeText; - }, - set: nodeTextSetter.mockImplementation(function(newText) { - nodeText = newText; - }) - }); - expect(node.value).toEqual('giraffe'); stub.replaceProps({ children: 'gorilla' }); @@ -64,8 +49,6 @@ describe('ReactDOMTextarea', function() { stub.replaceProps({ content: 'eggplant' }); expect(node.value).toEqual('eggplant'); - - expect(nodeTextSetter.mock.calls.length).toBe(0); }); it("should throw with multiple or invalid children", function() { From 4c97ffee348ada09749a262a7ca22a7853551225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Thu, 20 Jun 2013 16:51:21 -0700 Subject: [PATCH 38/94] Bring in Facebook's requestAnimationFrame module --- src/vendor/core/requestAnimationFrame.js | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/vendor/core/requestAnimationFrame.js diff --git a/src/vendor/core/requestAnimationFrame.js b/src/vendor/core/requestAnimationFrame.js new file mode 100644 index 0000000000..e71052efbf --- /dev/null +++ b/src/vendor/core/requestAnimationFrame.js @@ -0,0 +1,37 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule requestAnimationFrame + */ + +var emptyFunction = require('emptyFunction'); + +var requestAnimationFrame = + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + // Browsers which don't support requestAnimationFrame are likely running on + // older hardware, so it's not reasonable to expect 60FPS animations out of + // them. 30FPS (33.3ms per frame) is more reasonable. + return window.setTimeout(callback, 33); + }; + +// Works around a rare bug in Safari 6 where the first request is never invoked. +requestAnimationFrame(emptyFunction); + +module.exports = requestAnimationFrame; From 18352090e7d334bbba203761d9884ca1a5742022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Thu, 20 Jun 2013 18:30:30 -0700 Subject: [PATCH 39/94] Blogpost & changelog for v0.3.3 --- docs/_posts/2013-06-21-react-v0-3-3.md | 24 ++++++++++++++++++++++++ docs/downloads.md | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 docs/_posts/2013-06-21-react-v0-3-3.md diff --git a/docs/_posts/2013-06-21-react-v0-3-3.md b/docs/_posts/2013-06-21-react-v0-3-3.md new file mode 100644 index 0000000000..668c83de1a --- /dev/null +++ b/docs/_posts/2013-06-21-react-v0-3-3.md @@ -0,0 +1,24 @@ +--- +title: "React v0.3.3" +layout: post +author: Paul O'Shannessy +--- + +We have a ton of great stuff coming in v0.4, but in the meantime we're releasing v0.3.3. This release addresses some small issues people were having and simplifies our tools to make them easier to use. + + +## react-tools + +* Upgrade Commoner so `require` statements are no longer relativized when passing through the transformer. This was a feature needed when building React, but doesn't translate well for other consumers of `bin/jsx`. +* Upgraded our dependencies on Commoner and Recast so they use a different directory for their cache. +* Freeze our esprima dependency. + + +## React + +* Allow reusing the same DOM node to render different components. e.g. `React.renderComponent(
, domNode); React.renderComponent(, domNode);` will work now. + + +## JSXTransformer + +* Improved the in-browser transformer so that transformed scripts will execute in the expected scope. The allows components to be defined and used from separate files. diff --git a/docs/downloads.md b/docs/downloads.md index d35ff4dbd2..2b2a9ad556 100644 --- a/docs/downloads.md +++ b/docs/downloads.md @@ -49,6 +49,8 @@ $ npm install -g react-tools ## Release Notes +**0.3.3** Upgrade `react-tools` dependencies, improve in-browser JSX transformer, make `React.renderComponent` more versatile. [Details...](blog/2013/06/20/react-v0-3-3.html) + **0.3.2** Improve compatibility of JSX Transformer; make `react-tools` compatible with [browserify](https://github.com/substack/node-browserify) **0.3.1** Fix `react-tools` module From 36a724fecaed3c52ce8140683f452524934caceb Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Fri, 21 Jun 2013 13:48:48 -0700 Subject: [PATCH 40/94] Add `reactComponentExpect#toBeComponentOfType` This adds a `toBeComponentOfType` method to `reactComponentExpect`. Now that we are injecting composite native components, `toBeDOMComponentWithTag` will not suffice and should be deprecated. --- src/core/ReactDOM.js | 5 +++-- .../ReactCompositeComponentTransferProps-test.js | 6 +++--- src/test/ReactTestUtils.js | 8 ++++++++ src/test/reactComponentExpect.js | 12 ++++++++++++ 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/core/ReactDOM.js b/src/core/ReactDOM.js index 2fe78e99a7..b1882b3be0 100644 --- a/src/core/ReactDOM.js +++ b/src/core/ReactDOM.js @@ -41,15 +41,16 @@ var objMapKeyVal = require('objMapKeyVal'); */ function createDOMComponentClass(tag, omitClose) { var Constructor = function() {}; - Constructor.prototype = new ReactNativeComponent(tag, omitClose); Constructor.prototype.constructor = Constructor; - return function(props, children) { + var ConvenienceConstructor = function(props, children) { var instance = new Constructor(); instance.construct.apply(instance, arguments); return instance; }; + ConvenienceConstructor.componentConstructor = Constructor; + return ConvenienceConstructor; } /** diff --git a/src/core/__tests__/ReactCompositeComponentTransferProps-test.js b/src/core/__tests__/ReactCompositeComponentTransferProps-test.js index 47cc7455af..70c7e27532 100644 --- a/src/core/__tests__/ReactCompositeComponentTransferProps-test.js +++ b/src/core/__tests__/ReactCompositeComponentTransferProps-test.js @@ -52,7 +52,7 @@ describe('ReactCompositeComponent-transferProps', function() { reactComponentExpect(instance) .expectRenderedChild() - .toBeDOMComponentWithTag('input') + .toBeComponentOfType(React.DOM.input) .scalarPropsEqual({ className: 'textinput', style: {display: 'block'}, @@ -67,7 +67,7 @@ describe('ReactCompositeComponent-transferProps', function() { reactComponentExpect(instance) .expectRenderedChild() - .toBeDOMComponentWithTag('input') + .toBeComponentOfType(React.DOM.input) .scalarPropsEqual({placeholder: 'Type here...'}); }); @@ -81,7 +81,7 @@ describe('ReactCompositeComponent-transferProps', function() { reactComponentExpect(instance) .expectRenderedChild() - .toBeDOMComponentWithTag('input') + .toBeComponentOfType(React.DOM.input) .scalarPropsEqual({ className: 'textinput hidden_elem', style: { diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index 95651d5c90..e703472ea8 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -45,6 +45,14 @@ var ReactTestUtils = { return React.renderComponent(instance, div); }, + isComponentOfType: function(inst, type) { + return !!( + inst && + ReactComponent.isValidComponent(inst) && + inst.constructor === type.componentConstructor + ); + }, + isDOMComponent: function(inst) { return !!(inst && ReactComponent.isValidComponent(inst) && diff --git a/src/test/reactComponentExpect.js b/src/test/reactComponentExpect.js index 0a359fe2d2..dc664075bc 100644 --- a/src/test/reactComponentExpect.js +++ b/src/test/reactComponentExpect.js @@ -104,6 +104,14 @@ mergeInto(reactComponentExpect.prototype, { // Matchers ------------------------------------------------------------------ + toBeComponentOfType: function(convenienceConstructor) { + expect(ReactTestUtils.isComponentOfType( + this.instance(), + convenienceConstructor + )).toBe(true); + return this; + }, + /** * A component that is created with React.createClass. Just duck typing * here. @@ -149,6 +157,10 @@ mergeInto(reactComponentExpect.prototype, { return this; }, + /** + * @deprecated + * @see toBeComponentOfType + */ toBeDOMComponentWithTag: function(tag) { this.toBeDOMComponent(); expect(this.instance().tagName).toBe(tag.toUpperCase()); From 2bc2b52eaa7f541dd3ad30ede41e2a8dba74d9c8 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Fri, 21 Jun 2013 13:53:25 -0700 Subject: [PATCH 41/94] Allow accessing siblings by ref This makes it possible to write a wider range of components without depending on internal implementations. --- src/core/ReactComponent.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index 530a6f9c66..d312881068 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -564,6 +564,22 @@ var ReactComponent = { */ isOwnedBy: function(owner) { return this.props[OWNER] === owner; + }, + + /** + * Gets another component, that shares the same owner as this one, by ref. + * + * @param {string} ref of a sibling Component. + * @return {?ReactComponent} the actual sibling Component. + * @final + * @internal + */ + getSiblingByRef: function(ref) { + var owner = this.props[OWNER]; + if (!owner || !owner.refs) { + return null; + } + return owner.refs[ref]; } } From 191c0dec32ee22ec833c2afc0107058f66982a0f Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Fri, 21 Jun 2013 16:01:16 -0700 Subject: [PATCH 42/94] Consolidate ReactDOMNodeCache and getDOMNodeID into ReactID. When we move away from using the "id" attribute to identify React-generated elements, we will need the cache (formerly ReactDOMNodeCache) to be tied much more closely to the code that looks elements up by ID (getDOMNodeID) and sets element IDs, since the magic of document.getElementById will no longer be available. The priming functions are going to come in handy when we create new DOM fragments in mountComponent. For backwards compatibility, the ReactDOMNodeCache and getDOMNodeID modules still exist, but they are implemented entirely in terms of functions exported from ReactID. --- src/core/ReactDOMNodeCache.js | 70 ++----------------- src/core/ReactID.js | 128 ++++++++++++++++++++++++++++++++++ src/dom/getDOMNodeID.js | 21 ++---- 3 files changed, 136 insertions(+), 83 deletions(-) create mode 100644 src/core/ReactID.js diff --git a/src/core/ReactDOMNodeCache.js b/src/core/ReactDOMNodeCache.js index eaec6501ba..908c2ddbcd 100644 --- a/src/core/ReactDOMNodeCache.js +++ b/src/core/ReactDOMNodeCache.js @@ -19,70 +19,8 @@ "use strict"; -var ExecutionEnvironment = require('ExecutionEnvironment'); -var ReactMount = require('ReactMount'); +var ReactID = require('ReactID'); -var invariant = require('invariant'); - -var nodeCache = {}; - -/** - * DOM node cache only intended for use by React. Placed into a shared module so - * that both read and write utilities may benefit from a shared cache. - * - * @internal - */ -var ReactDOMNodeCache = { - - /** - * Finds the node with the supplied React-generated DOM ID. - * - * @param {string} id A React-generated DOM ID. - * @return {?DOMElement} DOM node with the suppled `id`. - * @internal - */ - getNodeByID: function(id) { - invariant( - ExecutionEnvironment.canUseDOM, - 'getDOMNode(): The DOM is not supported in the current environment.' - ); - if (!nodeCache[id]) { - nodeCache[id] = - document.getElementById(id) || - ReactMount.findReactRenderedDOMNodeSlow(id); - } - return nodeCache[id]; - }, - - /** - * Purges the supplied ID from cache. - * - * @param {string} id A React-generated DOM ID. - * @internal - */ - purgeID: function(id) { - nodeCache[id] = null; - }, - - /** - * Purges the entire node cache used for fast ID lookups. - * - * This implementation is aggressive with purging because the bookkeeping - * associated with doing fine-grained deletes from the cache may outweight the - * benefits of the cache. - * - * The heuristic used to purge is 'any time anything is deleted'. Typically - * this means that a large amount of content is being replaced and several - * elements would need purging regardless. This is also when applications are - * less likely to be in the middle of "smooth operations" such as animations - * or scrolling. - * - * @internal - */ - purgeEntireCache: function() { - nodeCache = {}; - } - -}; - -module.exports = ReactDOMNodeCache; +exports.getNodeByID = ReactID.getNode; +exports.purgeID = ReactID.purgeID; +exports.purgeEntireCache = ReactID.purgeEntireCache; diff --git a/src/core/ReactID.js b/src/core/ReactID.js new file mode 100644 index 0000000000..313ccebefe --- /dev/null +++ b/src/core/ReactID.js @@ -0,0 +1,128 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule ReactID + * @typechecks + */ + +"use strict"; + +var ReactMount = require('ReactMount'); +var ATTR_NAME = 'id'; +var nodeCache = {}; + +/** + * Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form + * element can return its control whose name or ID equals ATTR_NAME. All + * DOM nodes support `getAttributeNode` but this can also get called on + * other objects so just return '' if we're given something other than a + * DOM node (such as window). + * + * @param {DOMElement|DOMWindow|DOMDocument} node DOM node. + * @returns {string} ID of the supplied `domNode`. + */ +function getID(node) { + if (node && node.getAttributeNode) { + var attributeNode = node.getAttributeNode(ATTR_NAME); + return attributeNode && attributeNode.value || ''; + } + + return ''; +} + +/** + * Sets the React-specific ID of the given node. + * + * @param {DOMElement} node The DOM node whose ID will be set. + * @param {string} id The value of the ID attribute. + */ +function setID(node, id) { + var oldID = getID(node); + if (oldID !== id) { + delete nodeCache[oldID]; + } + node.setAttribute(ATTR_NAME, id); + nodeCache[id] = node; +} + +/** + * Finds the node with the supplied React-generated DOM ID. + * + * @param {string} id A React-generated DOM ID. + * @return {?DOMElement} DOM node with the suppled `id`. + * @internal + */ +function getNode(id) { + if (!nodeCache.hasOwnProperty(id)) { + nodeCache[id] = + document.getElementById(id) || // TODO Quit using getElementById. + ReactMount.findReactRenderedDOMNodeSlow(id); + } + + var node = nodeCache[id]; + if (getID(node) === id) { + return node; + } + + return null; +} + +/** + * Efficiently finds all nodes with a React-specific ID and primes the + * cache so that getNode lookups take constant time. + * + * @param {DOMElement} root The root element to scan. + */ +function primeTree(root) { + var nodes = root.querySelectorAll + ? root.querySelectorAll('[' + ATTR_NAME + ']') + : root.getElementsByTagName('*'); + + for (var i = 0; i < nodes.length; ++i) { + prime(nodes.item(i)); + } + + prime(root); +} + +function prime(node) { + var attributeNode = node.getAttributeNode(ATTR_NAME); + if (attributeNode) { + nodeCache[attributeNode.value] = node; + } +} + +/** + * Causes the cache to forget about one React-specific ID. + * + * @param {string} id The ID to forget. + */ +function purgeID(id) { + delete nodeCache[id]; +} + +/** + * Clears the entire cache. + */ +function purgeEntireCache() { + nodeCache = {}; +} + +exports.getID = getID; +exports.setID = setID; +exports.getNode = getNode; +exports.primeTree = primeTree; +exports.purgeID = purgeID; +exports.purgeEntireCache = purgeEntireCache; diff --git a/src/dom/getDOMNodeID.js b/src/dom/getDOMNodeID.js index f9632d8de5..dddd6fadee 100644 --- a/src/dom/getDOMNodeID.js +++ b/src/dom/getDOMNodeID.js @@ -19,22 +19,9 @@ "use strict"; -/** - * Accessing "id" or calling getAttribute('id') on a form element can return its - * control whose name or ID is "id". All DOM nodes support `getAttributeNode` - * but this can also get called on other objects so just return '' if we're - * given something other than a DOM node (such as window). - * - * @param {DOMElement|DOMWindow|DOMDocument} domNode DOM node. - * @returns {string} ID of the supplied `domNode`. - */ -function getDOMNodeID(domNode) { - if (domNode.getAttributeNode) { - var attributeNode = domNode.getAttributeNode('id'); - return attributeNode && attributeNode.value || ''; - } else { - return ''; - } -} +var ReactID = require('ReactID'); +function getDOMNodeID(domNode) { + return ReactID.getID(domNode); +} module.exports = getDOMNodeID; From 1839bcf109a598a45ba45214cde02d25c33f2938 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Fri, 21 Jun 2013 16:01:21 -0700 Subject: [PATCH 43/94] Replace uses of ReactDOMNodeCache and getDOMNodeID with ReactID. Also removed some unnecessary calls to `document.getElementById`, which will eventually cease to work for React-specific IDs. This clears the way for the deprecation of `ReactDOMNodeCache` and `getDOMNodeID`. --- src/core/ReactComponent.js | 6 ++--- src/core/ReactDOMIDOperations.js | 22 +++++++++---------- src/core/ReactEventTopLevelCallback.js | 4 ++-- src/core/ReactInputSelection.js | 6 ++++- src/core/ReactInstanceHandles.js | 4 ++-- .../__tests__/ReactCompositeComponent-test.js | 4 +++- src/core/__tests__/ReactDOM-test.js | 7 +++--- .../__tests__/ReactDOMIDOperations-test.js | 10 ++++----- src/eventPlugins/EnterLeaveEventPlugin.js | 6 ++--- src/test/ReactTestUtils.js | 4 ++-- 10 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index d312881068..f4333af4a6 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -22,7 +22,7 @@ var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); -var ReactDOMNodeCache = require('ReactDOMNodeCache'); +var ReactID = require('ReactID'); var ReactMount = require('ReactMount'); var ReactOwner = require('ReactOwner'); var ReactReconcileTransaction = require('ReactReconcileTransaction'); @@ -285,7 +285,7 @@ var ReactComponent = { this.isMounted(), 'getDOMNode(): A component must be mounted to have a DOM node.' ); - return ReactDOMNodeCache.getNodeByID(this._rootNodeID); + return ReactID.getNode(this._rootNodeID); }, /** @@ -440,7 +440,7 @@ var ReactComponent = { if (props.ref != null) { ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]); } - ReactDOMNodeCache.purgeID(this._rootNodeID); + ReactID.purgeID(this._rootNodeID); this._rootNodeID = null; this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; }, diff --git a/src/core/ReactDOMIDOperations.js b/src/core/ReactDOMIDOperations.js index 531de5338a..f95fc2837a 100644 --- a/src/core/ReactDOMIDOperations.js +++ b/src/core/ReactDOMIDOperations.js @@ -24,7 +24,7 @@ var CSSPropertyOperations = require('CSSPropertyOperations'); var DOMChildrenOperations = require('DOMChildrenOperations'); var DOMPropertyOperations = require('DOMPropertyOperations'); -var ReactDOMNodeCache = require('ReactDOMNodeCache'); +var ReactID = require('ReactID'); var getTextContentAccessor = require('getTextContentAccessor'); var invariant = require('invariant'); @@ -66,7 +66,7 @@ var ReactDOMIDOperations = { * @internal */ updatePropertyByID: function(id, name, value) { - var node = ReactDOMNodeCache.getNodeByID(id); + var node = ReactID.getNode(id); invariant( !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), 'updatePropertyByID(...): %s', @@ -84,7 +84,7 @@ var ReactDOMIDOperations = { * @internal */ deletePropertyByID: function(id, name, value) { - var node = ReactDOMNodeCache.getNodeByID(id); + var node = ReactID.getNode(id); invariant( !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), 'updatePropertyByID(...): %s', @@ -121,7 +121,7 @@ var ReactDOMIDOperations = { * @internal */ updateStylesByID: function(id, styles) { - var node = ReactDOMNodeCache.getNodeByID(id); + var node = ReactID.getNode(id); CSSPropertyOperations.setValueForStyles(node, styles); }, @@ -133,7 +133,7 @@ var ReactDOMIDOperations = { * @internal */ updateInnerHTMLByID: function(id, html) { - var node = ReactDOMNodeCache.getNodeByID(id); + var node = ReactID.getNode(id); // HACK: IE8- normalize whitespace in innerHTML, removing leading spaces. // @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html node.innerHTML = (html && html.__html || '').replace(/^ /g, ' '); @@ -147,7 +147,7 @@ var ReactDOMIDOperations = { * @internal */ updateTextContentByID: function(id, content) { - var node = ReactDOMNodeCache.getNodeByID(id); + var node = ReactID.getNode(id); node[textContentAccessor] = content; }, @@ -160,9 +160,9 @@ var ReactDOMIDOperations = { * @see {Danger.dangerouslyReplaceNodeWithMarkup} */ dangerouslyReplaceNodeWithMarkupByID: function(id, markup) { - var node = ReactDOMNodeCache.getNodeByID(id); + var node = ReactID.getNode(id); DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup); - ReactDOMNodeCache.purgeEntireCache(); + ReactID.purgeEntireCache(); }, /** @@ -170,13 +170,13 @@ var ReactDOMIDOperations = { * Detect if any elements were removed instead of blindly purging. */ manageChildrenByParentID: function(parentID, domOperations) { - var parent = ReactDOMNodeCache.getNodeByID(parentID); + var parent = ReactID.getNode(parentID); DOMChildrenOperations.manageChildren(parent, domOperations); - ReactDOMNodeCache.purgeEntireCache(); + ReactID.purgeEntireCache(); }, setTextNodeValueAtIndexByParentID: function(parentID, index, value) { - var parent = ReactDOMNodeCache.getNodeByID(parentID); + var parent = ReactID.getNode(parentID); DOMChildrenOperations.setTextNodeValueAtIndex(parent, index, value); } diff --git a/src/core/ReactEventTopLevelCallback.js b/src/core/ReactEventTopLevelCallback.js index fde97acc15..be70785073 100644 --- a/src/core/ReactEventTopLevelCallback.js +++ b/src/core/ReactEventTopLevelCallback.js @@ -23,7 +23,7 @@ var ExecutionEnvironment = require('ExecutionEnvironment'); var ReactEventEmitter = require('ReactEventEmitter'); var ReactInstanceHandles = require('ReactInstanceHandles'); -var getDOMNodeID = require('getDOMNodeID'); +var ReactID = require('ReactID'); var getEventTarget = require('getEventTarget'); /** @@ -76,7 +76,7 @@ var ReactEventTopLevelCallback = { var topLevelTarget = ReactInstanceHandles.getFirstReactDOM( getEventTarget(nativeEvent) ) || ExecutionEnvironment.global; - var topLevelTargetID = getDOMNodeID(topLevelTarget) || ''; + var topLevelTargetID = ReactID.getID(topLevelTarget) || ''; ReactEventEmitter.handleTopLevel( topLevelType, topLevelTarget, diff --git a/src/core/ReactInputSelection.js b/src/core/ReactInputSelection.js index 2abd7ab6f0..056db55119 100644 --- a/src/core/ReactInputSelection.js +++ b/src/core/ReactInputSelection.js @@ -27,6 +27,10 @@ function getActiveElement() { } } +function isInDocument(node) { + return document.documentElement.contains(node); +} + /** * @ReactInputSelection: React input selection module. Based on Selection.js, * but modified to be suitable for react and has a couple of bug fixes (doesn't @@ -64,7 +68,7 @@ var ReactInputSelection = { var priorFocusedElem = priorSelectionInformation.focusedElem; var priorSelectionRange = priorSelectionInformation.selectionRange; if (curFocusedElem !== priorFocusedElem && - document.getElementById(priorFocusedElem.id)) { + isInDocument(priorFocusedElem)) { if (ReactInputSelection.hasSelectionCapabilities(priorFocusedElem)) { ReactInputSelection.setSelection( priorFocusedElem, diff --git a/src/core/ReactInstanceHandles.js b/src/core/ReactInstanceHandles.js index 8f26212864..d6a8df0941 100644 --- a/src/core/ReactInstanceHandles.js +++ b/src/core/ReactInstanceHandles.js @@ -19,7 +19,7 @@ "use strict"; -var getDOMNodeID = require('getDOMNodeID'); +var ReactID = require('ReactID'); var invariant = require('invariant'); var SEPARATOR = '.'; @@ -250,7 +250,7 @@ var ReactInstanceHandles = { // Not a DOMElement, therefore not a React component return false; } - var id = getDOMNodeID(node); + var id = ReactID.getID(node); return id ? id.charAt(0) === SEPARATOR : false; }, diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index 178edaee1d..5a6e9602bf 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -26,6 +26,7 @@ var React; var ReactCurrentOwner; var ReactProps; var ReactTestUtils; +var ReactID; var cx; var reactComponentExpect; @@ -39,6 +40,7 @@ describe('ReactCompositeComponent', function() { ReactCurrentOwner = require('ReactCurrentOwner'); ReactProps = require('ReactProps'); ReactTestUtils = require('ReactTestUtils'); + ReactID = require('ReactID'); MorphingComponent = React.createClass({ getInitialState: function() { @@ -160,7 +162,7 @@ describe('ReactCompositeComponent', function() { // rerender instance.setProps({renderAnchor: true, anchorClassOn: false}); var anchorID = instance.getAnchorID(); - var actualDOMAnchorNode = document.getElementById(anchorID); + var actualDOMAnchorNode = ReactID.getNode(anchorID); expect(actualDOMAnchorNode.className).toBe(''); }); diff --git a/src/core/__tests__/ReactDOM-test.js b/src/core/__tests__/ReactDOM-test.js index 3d20fd6e16..6068a81327 100644 --- a/src/core/__tests__/ReactDOM-test.js +++ b/src/core/__tests__/ReactDOM-test.js @@ -25,6 +25,7 @@ var React = require('React'); var ReactDOM = require('ReactDOM'); var ReactTestUtils = require('ReactTestUtils'); var React = require('React'); +var ReactID = require('ReactID'); describe('ref swapping', function() { // TODO: uncomment this test once we can run in phantom, which @@ -60,7 +61,7 @@ describe('ref swapping', function() { var argDiv = ReactTestUtils.renderIntoDocument( ReactDOM.div(null, 'child') ); - var argNode = document.getElementById(argDiv._rootNodeID); + var argNode = ReactID.getNode(argDiv._rootNodeID); expect(argNode.innerHTML).toBe('child'); }); @@ -68,7 +69,7 @@ describe('ref swapping', function() { var conflictDiv = ReactTestUtils.renderIntoDocument( ReactDOM.div({children: 'fakechild'}, 'child') ); - var conflictNode = document.getElementById(conflictDiv._rootNodeID); + var conflictNode = ReactID.getNode(conflictDiv._rootNodeID); expect(conflictNode.innerHTML).toBe('child'); }); @@ -110,7 +111,7 @@ describe('ref swapping', function() { theBird:
} }); - var root = document.getElementById(myDiv._rootNodeID); + var root = ReactID.getNode(myDiv._rootNodeID); var dog = root.childNodes[0]; expect(dog.className).toBe('bigdog'); }); diff --git a/src/core/__tests__/ReactDOMIDOperations-test.js b/src/core/__tests__/ReactDOMIDOperations-test.js index aa717aafd0..6353993cf5 100644 --- a/src/core/__tests__/ReactDOMIDOperations-test.js +++ b/src/core/__tests__/ReactDOMIDOperations-test.js @@ -23,11 +23,11 @@ describe('ReactDOMIDOperations', function() { var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); - var ReactDOMNodeCache = require('ReactDOMNodeCache'); + var ReactID = require('ReactID'); var keyOf = require('keyOf'); it('should disallow updating special properties', function() { - spyOn(ReactDOMNodeCache, "getNodeByID"); + spyOn(ReactID, "getNode"); spyOn(DOMPropertyOperations, "setValueForProperty"); expect(function() { @@ -39,7 +39,7 @@ describe('ReactDOMIDOperations', function() { }).toThrow(); expect( - ReactDOMNodeCache.getNodeByID.argsForCall[0][0] + ReactID.getNode.argsForCall[0][0] ).toBe('testID'); expect( @@ -49,7 +49,7 @@ describe('ReactDOMIDOperations', function() { it('should update innerHTML and special-case whitespace', function() { var stubNode = document.createElement('div'); - spyOn(ReactDOMNodeCache, "getNodeByID").andReturn(stubNode); + spyOn(ReactID, "getNode").andReturn(stubNode); ReactDOMIDOperations.updateInnerHTMLByID( 'testID', @@ -57,7 +57,7 @@ describe('ReactDOMIDOperations', function() { ); expect( - ReactDOMNodeCache.getNodeByID.argsForCall[0][0] + ReactID.getNode.argsForCall[0][0] ).toBe('testID'); expect(stubNode.innerHTML).toBe(' testContent'); diff --git a/src/eventPlugins/EnterLeaveEventPlugin.js b/src/eventPlugins/EnterLeaveEventPlugin.js index c3fe3a8fe7..d01e032b99 100644 --- a/src/eventPlugins/EnterLeaveEventPlugin.js +++ b/src/eventPlugins/EnterLeaveEventPlugin.js @@ -25,7 +25,7 @@ var ExecutionEnvironment = require('ExecutionEnvironment'); var ReactInstanceHandles = require('ReactInstanceHandles'); var SyntheticMouseEvent = require('SyntheticMouseEvent'); -var getDOMNodeID = require('getDOMNodeID'); +var ReactID = require('ReactID'); var keyOf = require('keyOf'); var topLevelTypes = EventConstants.topLevelTypes; @@ -85,8 +85,8 @@ var EnterLeaveEventPlugin = { return null; } - var fromID = from ? getDOMNodeID(from) : ''; - var toID = to ? getDOMNodeID(to) : ''; + var fromID = from ? ReactID.getID(from) : ''; + var toID = to ? ReactID.getID(to) : ''; var leave = SyntheticMouseEvent.getPooled( eventTypes.mouseLeave, diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index e703472ea8..56c43e0bce 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -21,8 +21,8 @@ var React = require('React'); var ReactComponent = require('ReactComponent'); var ReactEventEmitter = require('ReactEventEmitter'); var ReactTextComponent = require('ReactTextComponent'); +var ReactID = require('ReactID'); -var ge = require('ge'); var mergeInto = require('mergeInto'); var topLevelTypes = EventConstants.topLevelTypes; @@ -222,7 +222,7 @@ var ReactTestUtils = { ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( topLevelType ); - var node = ge(reactRootID); + var node = ReactID.getNode(reactRootID); fakeNativeEvent.target = node; /* jsdom is returning nodes without id's - fixing that issue here. */ node.id = reactRootID; From 3373572e15c00b2dab9f005a85bb420fae107b2b Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Fri, 21 Jun 2013 16:01:26 -0700 Subject: [PATCH 44/94] Use ReactID.{get,set}ID instead of manipulating .id property directly. Another step in the plan to centralize control of React-specific identifers. --- src/core/ReactInstanceHandles.js | 5 +- src/core/ReactMount.js | 3 +- src/core/__tests__/ReactEventEmitter-test.js | 141 ++++++++++-------- src/core/__tests__/ReactIdentity-test.js | 4 +- .../__tests__/ReactInstanceHandles-test.js | 12 +- .../ReactMultiChildReconcile-test.js | 3 +- .../__tests__/ReactNativeComponent-test.js | 3 +- src/test/ReactTestUtils.js | 2 +- 8 files changed, 99 insertions(+), 74 deletions(-) diff --git a/src/core/ReactInstanceHandles.js b/src/core/ReactInstanceHandles.js index d6a8df0941..50564ac00c 100644 --- a/src/core/ReactInstanceHandles.js +++ b/src/core/ReactInstanceHandles.js @@ -285,9 +285,10 @@ var ReactInstanceHandles = { findComponentRoot: function(ancestorNode, id) { var child = ancestorNode.firstChild; while (child) { - if (id === child.id) { + var childId = ReactID.getID(child); + if (id === childId) { return child; - } else if (isAncestorIDOf(child.id, id)) { + } else if (isAncestorIDOf(childId, id)) { return ReactInstanceHandles.findComponentRoot(child, id); } child = child.nextSibling; diff --git a/src/core/ReactMount.js b/src/core/ReactMount.js index 6eaf42cfd6..2d9ddfcdb5 100644 --- a/src/core/ReactMount.js +++ b/src/core/ReactMount.js @@ -21,6 +21,7 @@ var ReactEventEmitter = require('ReactEventEmitter'); var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); +var ReactID = require('ReactID'); var $ = require('$'); @@ -44,7 +45,7 @@ function getReactRootElementInContainer(container) { */ function getReactRootID(container) { var rootElement = getReactRootElementInContainer(container); - return rootElement && rootElement.id; + return rootElement && ReactID.getID(rootElement); } /** diff --git a/src/core/__tests__/ReactEventEmitter-test.js b/src/core/__tests__/ReactEventEmitter-test.js index c2646d3332..79bf8f2390 100644 --- a/src/core/__tests__/ReactEventEmitter-test.js +++ b/src/core/__tests__/ReactEventEmitter-test.js @@ -22,6 +22,7 @@ require('mock-modules') .dontMock('BrowserScroll') .dontMock('CallbackRegistry') .dontMock('EventPluginHub') + .dontMock('ReactID') .dontMock('ReactEventEmitter') .dontMock('ReactInstanceHandles') .dontMock('EventPluginHub') @@ -34,6 +35,9 @@ var keyOf = require('keyOf'); var mocks = require('mocks'); var EventPluginHub; +var ReactID = require('ReactID'); +var getID = ReactID.getID; +var setID = ReactID.setID; var ReactEventEmitter; var ReactEventTopLevelCallback; var ReactTestUtils; @@ -68,15 +72,15 @@ var ON_TOUCH_TAP_KEY = keyOf({onTouchTap: null}); var CHILD = document.createElement('div'); var PARENT = document.createElement('div'); var GRANDPARENT = document.createElement('div'); -CHILD.id = '.reactRoot.[0].[0].[0]'; -PARENT.id = '.reactRoot.[0].[0]'; -GRANDPARENT.id = '.reactRoot.[0]'; +setID(CHILD, '.reactRoot.[0].[0].[0]'); +setID(PARENT, '.reactRoot.[0].[0]'); +setID(GRANDPARENT, '.reactRoot.[0]'); function registerSimpleTestHandler() { - ReactEventEmitter.putListener(CHILD.id, ON_CLICK_KEY, LISTENER); - var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); + ReactEventEmitter.putListener(getID(CHILD), ON_CLICK_KEY, LISTENER); + var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY); expect(listener).toEqual(LISTENER); - return ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); + return ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY); } @@ -85,6 +89,9 @@ describe('ReactEventEmitter', function() { require('mock-modules').dumpCache(); EventPluginHub = require('EventPluginHub'); TapEventPlugin = require('TapEventPlugin'); + ReactID = require('ReactID'); + getID = ReactID.getID; + setID = ReactID.setID; ReactEventEmitter = require('ReactEventEmitter'); ReactTestUtils = require('ReactTestUtils'); ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); @@ -98,20 +105,20 @@ describe('ReactEventEmitter', function() { it('should store a listener correctly', function() { registerSimpleTestHandler(); - var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); + var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY); expect(listener).toBe(LISTENER); }); it('should retrieve a listener correctly', function() { registerSimpleTestHandler(); - var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); + var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY); expect(listener).toEqual(LISTENER); }); it('should clear all handlers when asked to', function() { registerSimpleTestHandler(); - ReactEventEmitter.deleteAllListeners(CHILD.id); - var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); + ReactEventEmitter.deleteAllListeners(getID(CHILD)); + var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY); expect(listener).toBe(undefined); }); @@ -133,89 +140,89 @@ describe('ReactEventEmitter', function() { it('should bubble simply', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_CLICK_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactEventEmitter.putListener( - PARENT.id, + getID(PARENT), ON_CLICK_KEY, - recordID.bind(null, PARENT.id) + recordID.bind(null, getID(PARENT)) ); ReactEventEmitter.putListener( - GRANDPARENT.id, + getID(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, GRANDPARENT.id) + recordID.bind(null, getID(GRANDPARENT)) ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(CHILD.id); - expect(idCallOrder[1]).toBe(PARENT.id); - expect(idCallOrder[2]).toBe(GRANDPARENT.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); + expect(idCallOrder[1]).toBe(getID(PARENT)); + expect(idCallOrder[2]).toBe(getID(GRANDPARENT)); }); it('should support stopPropagation()', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_CLICK_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactEventEmitter.putListener( - PARENT.id, + getID(PARENT), ON_CLICK_KEY, - recordIDAndStopPropagation.bind(null, PARENT.id) + recordIDAndStopPropagation.bind(null, getID(PARENT)) ); ReactEventEmitter.putListener( - GRANDPARENT.id, + getID(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, GRANDPARENT.id) + recordID.bind(null, getID(GRANDPARENT)) ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(2); - expect(idCallOrder[0]).toBe(CHILD.id); - expect(idCallOrder[1]).toBe(PARENT.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); + expect(idCallOrder[1]).toBe(getID(PARENT)); }); it('should stop after first dispatch if stopPropagation', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_CLICK_KEY, - recordIDAndStopPropagation.bind(null, CHILD.id) + recordIDAndStopPropagation.bind(null, getID(CHILD)) ); ReactEventEmitter.putListener( - PARENT.id, + getID(PARENT), ON_CLICK_KEY, - recordID.bind(null, PARENT.id) + recordID.bind(null, getID(PARENT)) ); ReactEventEmitter.putListener( - GRANDPARENT.id, + getID(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, GRANDPARENT.id) + recordID.bind(null, getID(GRANDPARENT)) ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(CHILD.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); }); it('should stopPropagation if false is returned', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_CLICK_KEY, - recordIDAndReturnFalse.bind(null, CHILD.id) + recordIDAndReturnFalse.bind(null, getID(CHILD)) ); ReactEventEmitter.putListener( - PARENT.id, + getID(PARENT), ON_CLICK_KEY, - recordID.bind(null, PARENT.id) + recordID.bind(null, getID(PARENT)) ); ReactEventEmitter.putListener( - GRANDPARENT.id, + getID(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, GRANDPARENT.id) + recordID.bind(null, getID(GRANDPARENT)) ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(CHILD.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); }); /** @@ -230,10 +237,14 @@ describe('ReactEventEmitter', function() { it('should invoke handlers that were removed while bubbling', function() { var handleParentClick = mocks.getMockFunction(); var handleChildClick = function(event) { - ReactEventEmitter.deleteAllListeners(PARENT.id); + ReactEventEmitter.deleteAllListeners(getID(PARENT)); }; - ReactEventEmitter.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick); - ReactEventEmitter.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick); + ReactEventEmitter.putListener(getID(CHILD), ON_CLICK_KEY, handleChildClick); + ReactEventEmitter.putListener( + getID(PARENT), + ON_CLICK_KEY, + handleParentClick + ); ReactTestUtils.Simulate.click(CHILD); expect(handleParentClick.mock.calls.length).toBe(1); }); @@ -241,18 +252,22 @@ describe('ReactEventEmitter', function() { it('should not invoke newly inserted handlers while bubbling', function() { var handleParentClick = mocks.getMockFunction(); var handleChildClick = function(event) { - ReactEventEmitter.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick); + ReactEventEmitter.putListener( + getID(PARENT), + ON_CLICK_KEY, + handleParentClick + ); }; - ReactEventEmitter.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick); + ReactEventEmitter.putListener(getID(CHILD), ON_CLICK_KEY, handleChildClick); ReactTestUtils.Simulate.click(CHILD); expect(handleParentClick.mock.calls.length).toBe(0); }); it('should infer onTouchTap from a touchStart/End', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactTestUtils.Simulate.touchStart( CHILD, @@ -263,14 +278,14 @@ describe('ReactEventEmitter', function() { ReactTestUtils.nativeTouchData(0, 0) ); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(CHILD.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); }); it('should infer onTouchTap from when dragging below threshold', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactTestUtils.Simulate.touchStart( CHILD, @@ -281,14 +296,14 @@ describe('ReactEventEmitter', function() { ReactTestUtils.nativeTouchData(0, tapMoveThreshold - 1) ); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(CHILD.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); }); it('should not onTouchTap from when dragging beyond threshold', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactTestUtils.Simulate.touchStart( CHILD, @@ -304,19 +319,19 @@ describe('ReactEventEmitter', function() { it('should bubble onTouchTap', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactEventEmitter.putListener( - PARENT.id, + getID(PARENT), ON_TOUCH_TAP_KEY, - recordID.bind(null, PARENT.id) + recordID.bind(null, getID(PARENT)) ); ReactEventEmitter.putListener( - GRANDPARENT.id, + getID(GRANDPARENT), ON_TOUCH_TAP_KEY, - recordID.bind(null, GRANDPARENT.id) + recordID.bind(null, getID(GRANDPARENT)) ); ReactTestUtils.Simulate.touchStart( CHILD, @@ -327,9 +342,9 @@ describe('ReactEventEmitter', function() { ReactTestUtils.nativeTouchData(0, 0) ); expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(CHILD.id); - expect(idCallOrder[1]).toBe(PARENT.id); - expect(idCallOrder[2]).toBe(GRANDPARENT.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); + expect(idCallOrder[1]).toBe(getID(PARENT)); + expect(idCallOrder[2]).toBe(getID(GRANDPARENT)); }); }); diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js index f92a4c3e0b..9edf0f73ef 100644 --- a/src/core/__tests__/ReactIdentity-test.js +++ b/src/core/__tests__/ReactIdentity-test.js @@ -22,6 +22,7 @@ var React; var ReactTestUtils; var reactComponentExpect; +var ReactID; describe('ReactIdentity', function() { @@ -30,11 +31,12 @@ describe('ReactIdentity', function() { React = require('React'); ReactTestUtils = require('ReactTestUtils'); reactComponentExpect = require('reactComponentExpect'); + ReactID = require('ReactID'); }); var idExp = /^\.reactRoot\[\d+\](.*)$/; function checkId(child, expectedId) { - var actual = idExp.exec(child.id); + var actual = idExp.exec(ReactID.getID(child)); var expected = idExp.exec(expectedId); expect(actual).toBeTruthy(); expect(expected).toBeTruthy(); diff --git a/src/core/__tests__/ReactInstanceHandles-test.js b/src/core/__tests__/ReactInstanceHandles-test.js index 95e03d8250..34531223fa 100644 --- a/src/core/__tests__/ReactInstanceHandles-test.js +++ b/src/core/__tests__/ReactInstanceHandles-test.js @@ -21,6 +21,7 @@ var React = require('React'); var ReactTestUtils = require('ReactTestUtils'); +var ReactID = require('ReactID'); /** * Ensure that all callbacks are invoked, passing this unique argument. @@ -90,12 +91,15 @@ describe('ReactInstanceHandles', function() { parentNode.appendChild(childNodeA); parentNode.appendChild(childNodeB); - parentNode.id = '.react[0]'; - childNodeA.id = '.react[0].0'; - childNodeB.id = '.react[0].0:1'; + ReactID.setID(parentNode, '.react[0]'); + ReactID.setID(childNodeA, '.react[0].0'); + ReactID.setID(childNodeB, '.react[0].0:1'); expect( - ReactInstanceHandles.findComponentRoot(parentNode, childNodeB.id) + ReactInstanceHandles.findComponentRoot( + parentNode, + ReactID.getID(childNodeB) + ) ).toBe(childNodeB); }); }); diff --git a/src/core/__tests__/ReactMultiChildReconcile-test.js b/src/core/__tests__/ReactMultiChildReconcile-test.js index 597ca8f182..81b680c35b 100644 --- a/src/core/__tests__/ReactMultiChildReconcile-test.js +++ b/src/core/__tests__/ReactMultiChildReconcile-test.js @@ -23,6 +23,7 @@ require('mock-modules'); var React = require('React'); var ReactTestUtils = require('ReactTestUtils'); +var ReactID = require('ReactID'); var objMapKeyVal = require('objMapKeyVal'); @@ -191,7 +192,7 @@ function verifyDomOrderingAccurate(parentInstance, statusDisplays) { var i; var orderedDomIds = []; for (i=0; i < statusDisplayNodes.length; i++) { - orderedDomIds.push(statusDisplayNodes[i].id); + orderedDomIds.push(ReactID.getID(statusDisplayNodes[i])); } var orderedLogicalIds = []; diff --git a/src/core/__tests__/ReactNativeComponent-test.js b/src/core/__tests__/ReactNativeComponent-test.js index 55e5f13ec6..2611f1d442 100644 --- a/src/core/__tests__/ReactNativeComponent-test.js +++ b/src/core/__tests__/ReactNativeComponent-test.js @@ -328,6 +328,7 @@ describe('ReactNativeComponent', function() { it("should clean up listeners", function() { var React = require('React'); var ReactEventEmitter = require('ReactEventEmitter'); + var ReactID = require('ReactID'); var container = document.createElement('div'); document.documentElement.appendChild(container); @@ -337,7 +338,7 @@ describe('ReactNativeComponent', function() { React.renderComponent(instance, container); var rootNode = instance.getDOMNode(); - var rootNodeID = rootNode.id; + var rootNodeID = ReactID.getID(rootNode); expect( ReactEventEmitter.getListener(rootNodeID, 'onClick') ).toBe(callback); diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index 56c43e0bce..b8784183d6 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -225,7 +225,7 @@ var ReactTestUtils = { var node = ReactID.getNode(reactRootID); fakeNativeEvent.target = node; /* jsdom is returning nodes without id's - fixing that issue here. */ - node.id = reactRootID; + ReactID.setID(node, reactRootID); virtualHandler(fakeNativeEvent); }, From 7053f59ad11c3483da1c163f80c4b2de7f901f77 Mon Sep 17 00:00:00 2001 From: petehunt Date: Sun, 16 Jun 2013 19:35:39 -0700 Subject: [PATCH 45/94] Remove second argument to invariant() --- vendor/constants.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/vendor/constants.js b/vendor/constants.js index b0530b4d69..339398c2a8 100644 --- a/vendor/constants.js +++ b/vendor/constants.js @@ -34,6 +34,15 @@ var ConstantVisitor = recast.Visitor.extend({ } }, + visitCallExpression: function(call) { + if (!this.constants.__DEV__) { + if (call.callee.type === 'Identifier' && call.callee.name === 'invariant') { + call.arguments.length = 1; + } + } + this.genericVisit(call); + }, + visitIfStatement: function(stmt) { // Replaces all identifiers in this.constants with literal values. this.genericVisit(stmt); From c9ecbaccb365ba39bd8839f61ae245cdc295317b Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 16:09:09 -0700 Subject: [PATCH 46/94] Use React.autoBind by default. Per our discussion - this is the general approach we'd like to take for the public facing API. var MyComponent = React.createClass({ render: function() { return
; }, myCallback: function() { } }); --- src/core/ReactCompositeComponent.js | 217 +++++++++--------- src/core/ReactDoNotBindDeprecated.js | 48 ++++ src/core/ReactID.js | 2 +- src/core/__tests__/ReactBind-test.js | 19 +- .../__tests__/ReactComponentLifeCycle-test.js | 76 +----- .../__tests__/ReactCompositeComponent-test.js | 50 ++-- 6 files changed, 217 insertions(+), 195 deletions(-) create mode 100644 src/core/ReactDoNotBindDeprecated.js diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index 6e79b9ca97..6d4a6ee0d9 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -28,6 +28,17 @@ var keyMirror = require('keyMirror'); var merge = require('merge'); var mixInto = require('mixInto'); +var invokedBeforeMount = function() { + invariant( + false, + 'You have invoked a method that is automatically bound, before the ' + + 'instance has been mounted. There is nothing conceptually wrong with ' + + 'this, but since this method will be replaced with a new version once ' + + 'the component is mounted - you should be aware of the fact that this ' + + 'method will soon be replaced.' + ); +}; + /** * Policies that describe methods in `ReactCompositeComponentInterface`. */ @@ -283,6 +294,47 @@ var RESERVED_SPEC_KEYS = { } }; +function validateMethodOverride(proto, name) { + var specPolicy = ReactCompositeComponentInterface[name]; + + // Disallow overriding of base class methods unless explicitly allowed. + if (ReactCompositeComponentMixin.hasOwnProperty(name)) { + invariant( + specPolicy === SpecPolicy.OVERRIDE_BASE, + 'ReactCompositeComponentInterface: You are attempting to override ' + + '`%s` from your class specification. Ensure that your method names ' + + 'do not overlap with React methods.', + name + ); + } + + // Disallow defining methods more than once unless explicitly allowed. + if (proto.hasOwnProperty(name)) { + invariant( + specPolicy === SpecPolicy.DEFINE_MANY, + 'ReactCompositeComponentInterface: You are attempting to define ' + + '`%s` on your component more than once. This conflict may be due ' + + 'to a mixin.', + name + ); + } +} + + +function validateLifeCycleOnReplaceState(instance) { + var compositeLifeCycleState = instance._compositeLifeCycleState; + invariant( + instance.isMounted(), + 'replaceState(...): Can only update a mounted component.' + ); + invariant( + compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE && + compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING, + 'replaceState(...): Cannot update while unmounting component or during ' + + 'an existing state transition (such as within `render`).' + ); +} + /** * Custom version of `mixInto` which handles policy validation and reserved * specification keys when building `ReactCompositeComponent` classses. @@ -290,58 +342,44 @@ var RESERVED_SPEC_KEYS = { function mixSpecIntoComponent(Constructor, spec) { var proto = Constructor.prototype; for (var name in spec) { - if (!spec.hasOwnProperty(name)) { + var property = spec[name]; + if (!spec.hasOwnProperty(name) || !property) { continue; } - var property = spec[name]; - var specPolicy = ReactCompositeComponentInterface[name]; - - // Disallow overriding of base class methods unless explicitly allowed. - if (ReactCompositeComponentMixin.hasOwnProperty(name)) { - invariant( - specPolicy === SpecPolicy.OVERRIDE_BASE, - 'ReactCompositeComponentInterface: You are attempting to override ' + - '`%s` from your class specification. Ensure that your method names ' + - 'do not overlap with React methods.', - name - ); - } - - // Disallow using `React.autoBind` on internal methods. - if (specPolicy != null) { - invariant( - !property || !property.__reactAutoBind, - 'ReactCompositeComponentInterface: You are attempting to use ' + - '`React.autoBind` on `%s`, a method that is internal to React.' + - 'Internal methods are called with the component as the context.', - name - ); - } - - // Disallow defining methods more than once unless explicitly allowed. - if (proto.hasOwnProperty(name)) { - invariant( - specPolicy === SpecPolicy.DEFINE_MANY, - 'ReactCompositeComponentInterface: You are attempting to define ' + - '`%s` on your component more than once. This conflict may be due ' + - 'to a mixin.', - name - ); - } + validateMethodOverride(proto, name); if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) { RESERVED_SPEC_KEYS[name](Constructor, property); - } else if (property && property.__reactAutoBind) { - if (!proto.__reactAutoBindMap) { - proto.__reactAutoBindMap = {}; - } - proto.__reactAutoBindMap[name] = property.__reactAutoBind; - } else if (proto.hasOwnProperty(name)) { - // For methods which are defined more than once, call the existing methods - // before calling the new property. - proto[name] = createChainedFunction(proto[name], property); } else { - proto[name] = property; + // Setup methods on prototype: + // The following member methods should not be automatically bound: + // 1. Expected ReactCompositeComponent methods (in the "interface"). + // 2. Overridden methods (that were mixed in). + var isCompositeComponentMethod = name in ReactCompositeComponentInterface; + var isInherited = name in proto; + var markedDontBind = property.__reactDontBind; + var isFunction = typeof property === 'function'; + var shouldAutoBind = + isFunction && + !isCompositeComponentMethod && + !isInherited && + !markedDontBind; + + if (shouldAutoBind) { + if (!proto.__reactAutoBindMap) { + proto.__reactAutoBindMap = {}; + } + proto.__reactAutoBindMap[name] = property; + proto[name] = invokedBeforeMount; + } else { + if (isInherited) { + // For methods which are defined more than once, call the existing + // methods before calling the new property. + proto[name] = createChainedFunction(proto[name], property); + } else { + proto[name] = property; + } + } } } } @@ -370,7 +408,26 @@ function createChainedFunction(one, two) { * `this._compositeLifeCycleState` (which can be null). * * This is different from the life cycle state maintained by `ReactComponent` in - * `this._lifeCycleState`. + * `this._lifeCycleState`. The following diagram shows how the states overlap in + * time. There are times when the CompositeLifeCycle is null - at those times it + * is only meaningful to look at ComponentLifeCycle alone. + * + * Top Row: ReactComponent.ComponentLifeCycle + * Low Row: ReactComponent.CompositeLifeCycle + * + * +-------+------------------------------------------------------+--------+ + * | UN | MOUNTED | UN | + * |MOUNTED| | MOUNTED| + * +-------+------------------------------------------------------+--------+ + * | ^--------+ +------+ +------+ +------+ +--------^ | + * | | | | | | | | | | | | + * | 0--|MOUNTING|-0-|RECEIV|-0-|RECEIV|-0-|RECEIV|-0-| UN |--->0 | + * | | | |PROPS | | PROPS| | STATE| |MOUNTING| | + * | | | | | | | | | | | | + * | | | | | | | | | | | | + * | +--------+ +------+ +------+ +------+ +--------+ | + * | | | | + * +-------+------------------------------------------------------+--------+ */ var CompositeLifeCycle = keyMirror({ /** @@ -429,11 +486,7 @@ var ReactCompositeComponentMixin = { */ mountComponent: function(rootID, transaction) { ReactComponent.Mixin.mountComponent.call(this, rootID, transaction); - - // Unset `this._lifeCycleState` until after this method is finished. - this._lifeCycleState = ReactComponent.LifeCycle.UNMOUNTED; this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING; - this._processProps(this.props); if (this.__reactAutoBindMap) { @@ -459,15 +512,11 @@ var ReactCompositeComponentMixin = { // Done with mounting, `setState` will now trigger UI changes. this._compositeLifeCycleState = null; - this._lifeCycleState = ReactComponent.LifeCycle.MOUNTED; - - var html = this._renderedComponent.mountComponent(rootID, transaction); - + var markup = this._renderedComponent.mountComponent(rootID, transaction); if (this.componentDidMount) { transaction.getReactOnDOMReady().enqueue(this, this.componentDidMount); } - - return html; + return markup; }, /** @@ -551,18 +600,7 @@ var ReactCompositeComponentMixin = { */ replaceState: function(completeState) { var compositeLifeCycleState = this._compositeLifeCycleState; - invariant( - this.isMounted() || - compositeLifeCycleState === CompositeLifeCycle.MOUNTING, - 'replaceState(...): Can only update a mounted (or mounting) component.' - ); - invariant( - compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE && - compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING, - 'replaceState(...): Cannot update while unmounting component or during ' + - 'an existing state transition (such as within `render`).' - ); - + validateLifeCycleOnReplaceState.call(null, this); this._pendingState = completeState; // Do not trigger a state transition if we are in the middle of mounting or @@ -583,7 +621,6 @@ var ReactCompositeComponentMixin = { transaction ); ReactComponent.ReactReconcileTransaction.release(transaction); - this._compositeLifeCycleState = null; } }, @@ -777,25 +814,12 @@ var ReactCompositeComponentMixin = { */ _bindAutoBindMethod: function(method) { var component = this; - var hasWarned = false; function autoBound(a, b, c, d, e, tooMany) { invariant( typeof tooMany === 'undefined', 'React.autoBind(...): Methods can only take a maximum of 5 arguments.' ); - if (component._lifeCycleState === ReactComponent.LifeCycle.MOUNTED) { - return method.call(component, a, b, c, d, e); - } else if (!hasWarned) { - hasWarned = true; - if (__DEV__) { - console.warn( - 'React.autoBind(...): Attempted to invoke an auto-bound method ' + - 'on an unmounted instance of `%s`. You either have a memory leak ' + - 'or an event handler that is being run after unmounting.', - component.constructor.displayName || 'ReactCompositeComponent' - ); - } - } + return method.call(component, a, b, c, d, e); } return autoBound; } @@ -850,33 +874,16 @@ var ReactCompositeComponent = { }, /** - * Marks the provided method to be automatically bound to the component. - * This means the method's context will always be the component. - * - * React.createClass({ - * handleClick: React.autoBind(function() { - * this.setState({jumping: true}); - * }), - * render: function() { - * return Jump; - * } - * }); + * TODO: Delete this when all callers have been updated to rely on this + * behavior being the default. * + * Backwards compatible stub for what is now the default behavior. * @param {function} method Method to be bound. * @public */ autoBind: function(method) { - function unbound() { - invariant( - false, - 'React.autoBind(...): Attempted to invoke an auto-bound method that ' + - 'was not correctly defined on the class specification.' - ); - } - unbound.__reactAutoBind = method; - return unbound; + return method; } - }; module.exports = ReactCompositeComponent; diff --git a/src/core/ReactDoNotBindDeprecated.js b/src/core/ReactDoNotBindDeprecated.js new file mode 100644 index 0000000000..11b6c00166 --- /dev/null +++ b/src/core/ReactDoNotBindDeprecated.js @@ -0,0 +1,48 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule ReactDoNotBindDeprecated + */ + +var ReactDoNotBindDeprecated = { + /** + * Marks the method for not being automatically bound on component mounting. A + * couple of reasons you might want to use this: + * + * - Automatically supporting the previous behavior in components that were + * built with previous versions of React. + * - Tuning performance, by avoiding binding on initial render for methods + * that are always invoked while being preceded by `this.`. Such binds are + * unnecessary. + * + * React.createClass({ + * handleClick: ReactDoNotBindDeprecated.doNotBind(function() { + * alert(this.setState); // undefined! + * }), + * render: function() { + * return Jump; + * } + * }); + * + * @param {function} method Method to avoid automatically binding. + * @public + */ + doNotBind: function(method) { + method.__reactDontBind = true; // Mutating + return method; + } +}; + +module.exports = ReactDoNotBindDeprecated; diff --git a/src/core/ReactID.js b/src/core/ReactID.js index 313ccebefe..edc5a10e0e 100644 --- a/src/core/ReactID.js +++ b/src/core/ReactID.js @@ -30,7 +30,7 @@ var nodeCache = {}; * other objects so just return '' if we're given something other than a * DOM node (such as window). * - * @param {DOMElement|DOMWindow|DOMDocument} node DOM node. + * @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node. * @returns {string} ID of the supplied `domNode`. */ function getID(node) { diff --git a/src/core/__tests__/ReactBind-test.js b/src/core/__tests__/ReactBind-test.js index 2fa5f77c59..4d31f38ab0 100644 --- a/src/core/__tests__/ReactBind-test.js +++ b/src/core/__tests__/ReactBind-test.js @@ -21,9 +21,11 @@ var mocks = require('mocks'); var React = require('React'); +var ReactDoNotBindDeprecated = require('ReactDoNotBindDeprecated'); var ReactTestUtils = require('ReactTestUtils'); var reactComponentExpect = require('reactComponentExpect'); +// TODO: Test render and all stock methods. describe('React.autoBind', function() { it('Holds reference to instance', function() { @@ -31,16 +33,20 @@ describe('React.autoBind', function() { var mouseDidEnter = mocks.getMockFunction(); var mouseDidLeave = mocks.getMockFunction(); var mouseDidClick = mocks.getMockFunction(); - var didBadIdea = mocks.getMockFunction(); var TestBindComponent = React.createClass({ - onMouseEnter: mouseDidEnter, - onMouseLeave: mouseDidLeave, + getInitialState: function() { + return {something: 'hi'}; + }, + onMouseEnter: ReactDoNotBindDeprecated.doNotBind(mouseDidEnter), + onMouseLeave: ReactDoNotBindDeprecated.doNotBind(mouseDidLeave), onClick: React.autoBind(mouseDidClick), - // autoBind needs to be on the top-level spec. + // auto binding only occurs on top level functions in class defs. badIdeas: { - badBind: React.autoBind(didBadIdea) + badBind: function() { + this.state.something; + } }, render: function() { @@ -48,7 +54,8 @@ describe('React.autoBind', function() {
+ onClick={this.onClick} + /> ); } }); diff --git a/src/core/__tests__/ReactComponentLifeCycle-test.js b/src/core/__tests__/ReactComponentLifeCycle-test.js index 4f66eb10e2..a3c18be304 100644 --- a/src/core/__tests__/ReactComponentLifeCycle-test.js +++ b/src/core/__tests__/ReactComponentLifeCycle-test.js @@ -286,6 +286,8 @@ describe('ReactComponentLifeCycle', function() { if (isInitialRender) { this._testJournal.stateInInitialRender = clone(this.state); this._testJournal.lifeCycleInInitialRender = this._lifeCycleState; + this._testJournal.compositeLifeCycleInInitialRender = + this._compositeLifeCycleState; } else { this._testJournal.stateInLaterRender = clone(this.state); this._testJournal.lifeCycleInLaterRender = this._lifeCycleState; @@ -319,7 +321,7 @@ describe('ReactComponentLifeCycle', function() { GET_INIT_STATE_RETURN_VAL ); expect(instance._testJournal.lifeCycleAtStartOfGetInitialState) - .toBe(ComponentLifeCycle.UNMOUNTED); + .toBe(ComponentLifeCycle.MOUNTED); expect(instance._testJournal.compositeLifeCycleAtStartOfGetInitialState) .toBe(CompositeComponentLifeCycle.MOUNTING); @@ -328,7 +330,7 @@ describe('ReactComponentLifeCycle', function() { instance._testJournal.returnedFromGetInitialState ); expect(instance._testJournal.lifeCycleAtStartOfWillMount) - .toBe(ComponentLifeCycle.UNMOUNTED); + .toBe(ComponentLifeCycle.MOUNTED); expect(instance._testJournal.compositeLifeCycleAtStartOfWillMount) .toBe(CompositeComponentLifeCycle.MOUNTING); @@ -343,7 +345,10 @@ describe('ReactComponentLifeCycle', function() { expect(instance._testJournal.stateInInitialRender) .toEqual(INIT_RENDER_STATE); expect(instance._testJournal.lifeCycleInInitialRender).toBe( - ComponentLifeCycle.UNMOUNTED + ComponentLifeCycle.MOUNTED + ); + expect(instance._testJournal.compositeLifeCycleInInitialRender).toBe( + CompositeComponentLifeCycle.MOUNTING ); expect(instance._lifeCycleState).toBe(ComponentLifeCycle.MOUNTED); @@ -429,70 +434,5 @@ describe('ReactComponentLifeCycle', function() { expect(instance.state.stateField).toBe('goodbye'); }); - it('should call nested lifecycle methods in the right order', function() { - var log; - var logger = function(msg) { - return function() { - // return true for shouldComponentUpdate - log.push(msg); - return true; - }; - }; - var Outer = React.createClass({ - render: function() { - return
; - }, - componentWillMount: logger('outer componentWillMount'), - componentDidMount: logger('outer componentDidMount'), - componentWillReceiveProps: logger('outer componentWillReceiveProps'), - shouldComponentUpdate: logger('outer shouldComponentUpdate'), - componentWillUpdate: logger('outer componentWillUpdate'), - componentDidUpdate: logger('outer componentDidUpdate'), - componentWillUnmount: logger('outer componentWillUnmount') - }); - var Inner = React.createClass({ - render: function() { - return {this.props.x}; - }, - componentWillMount: logger('inner componentWillMount'), - componentDidMount: logger('inner componentDidMount'), - componentWillReceiveProps: logger('inner componentWillReceiveProps'), - shouldComponentUpdate: logger('inner shouldComponentUpdate'), - componentWillUpdate: logger('inner componentWillUpdate'), - componentDidUpdate: logger('inner componentDidUpdate'), - componentWillUnmount: logger('inner componentWillUnmount') - }); - var instance; - - log = []; - instance = ReactTestUtils.renderIntoDocument(); - expect(log).toEqual([ - 'outer componentWillMount', - 'inner componentWillMount', - 'inner componentDidMount', - 'outer componentDidMount' - ]); - - log = []; - instance.setProps({x: 42}); - expect(log).toEqual([ - 'outer componentWillReceiveProps', - 'outer shouldComponentUpdate', - 'outer componentWillUpdate', - 'inner componentWillReceiveProps', - 'inner shouldComponentUpdate', - 'inner componentWillUpdate', - 'inner componentDidUpdate', - 'outer componentDidUpdate' - ]); - - log = []; - instance.unmountComponent(); - expect(log).toEqual([ - 'outer componentWillUnmount', - 'inner componentWillUnmount' - ]); - }); - }); diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index 5a6e9602bf..d6623e9daa 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -27,6 +27,7 @@ var ReactCurrentOwner; var ReactProps; var ReactTestUtils; var ReactID; +var ReactDoNotBindDeprecated; var cx; var reactComponentExpect; @@ -38,6 +39,7 @@ describe('ReactCompositeComponent', function() { reactComponentExpect = require('reactComponentExpect'); React = require('React'); ReactCurrentOwner = require('ReactCurrentOwner'); + ReactDoNotBindDeprecated = require('ReactDoNotBindDeprecated'); ReactProps = require('ReactProps'); ReactTestUtils = require('ReactTestUtils'); ReactID = require('ReactID'); @@ -52,7 +54,7 @@ describe('ReactCompositeComponent', function() { }, render: function() { - var toggleActivatedState = this._toggleActivatedState.bind(this); + var toggleActivatedState = this._toggleActivatedState; return !this.state.activated ? : ; @@ -64,9 +66,9 @@ describe('ReactCompositeComponent', function() { return {activated: false}; }, - _toggleActivatedState: React.autoBind(function() { + _toggleActivatedState:function() { this.setState({activated: !this.state.activated}); - }), + }, render: function() { return !this.state.activated ? @@ -167,15 +169,18 @@ describe('ReactCompositeComponent', function() { }); it('should auto bind methods and values correctly', function() { - var RETURN_VALUE_AFTER_MOUNT = 'returnValue'; var ComponentClass = React.createClass({ getInitialState: function() { - return { - valueToReturn: RETURN_VALUE_AFTER_MOUNT - }; + return {valueToReturn: 'hi'}; }, - methodBoundOnMount: React.autoBind(function() { - return this.state.valueToReturn; + methodToBeExplicitlyBound: function() { + return this; + }, + methodAutoBound: function() { + return this; + }, + methodExplicitlyNotBound: ReactDoNotBindDeprecated.doNotBind(function() { + return this; }), render: function() { return
; @@ -184,18 +189,33 @@ describe('ReactCompositeComponent', function() { var instance = ; // Autobound methods will throw before mounting. + // TODO: We should actually allow component instance methods to be invoked + // before mounting for read-only operations. We would then update this test. expect(function() { - instance.methodBoundOnMount(); + instance.methodToBeExplicitlyBound.bind(instance)(); }).toThrow(); + expect(function() { + instance.methodAutoBound(); + }).toThrow(); + expect(function() { + instance.methodExplicitlyNotBound(); + }).not.toThrow(); // Next, prove that once mounted, the scope is bound correctly to the actual // component. ReactTestUtils.renderIntoDocument(instance); - var retValAfterMount = instance.methodBoundOnMount(); - expect(retValAfterMount).toBe(RETURN_VALUE_AFTER_MOUNT); - var retValAfterMountWithCrazyScope = - instance.methodBoundOnMount.call({thisIsACrazyScope:null}); - expect(retValAfterMountWithCrazyScope).toBe(RETURN_VALUE_AFTER_MOUNT); + var explicitlyBound = instance.methodToBeExplicitlyBound.bind(instance); + var autoBound = instance.methodAutoBound; + var explicitlyNotBound = instance.methodExplicitlyNotBound; + + expect(explicitlyBound.call(null)).toBe(instance); + expect(autoBound.call(null)).toBe(instance); + expect(explicitlyNotBound.call(null)).toBe(null); + + expect(explicitlyBound.call(instance)).toBe(instance); + expect(autoBound.call(instance)).toBe(instance); + expect(explicitlyNotBound.call(instance)).toBe(instance); + }); it('should normalize props with default values', function() { From 3266818b42fbb035ea8d9b48c8a97da5f3e9d756 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 16:09:18 -0700 Subject: [PATCH 47/94] fix bad caching in ReactID.getID --- src/core/ReactID.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ReactID.js b/src/core/ReactID.js index edc5a10e0e..9bfba32bfa 100644 --- a/src/core/ReactID.js +++ b/src/core/ReactID.js @@ -65,7 +65,7 @@ function setID(node, id) { * @internal */ function getNode(id) { - if (!nodeCache.hasOwnProperty(id)) { + if (!nodeCache[id]) { nodeCache[id] = document.getElementById(id) || // TODO Quit using getElementById. ReactMount.findReactRenderedDOMNodeSlow(id); From a61f4df0b9b23250a5a59285aad82cf7d82a19b8 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 16:09:26 -0700 Subject: [PATCH 48/94] Fix a bunch of problems with implicit autobinding --- src/core/ReactCompositeComponent.js | 41 +++++++++++-------- .../__tests__/ReactCompositeComponent-test.js | 9 ---- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index 6d4a6ee0d9..e97b5439fc 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -28,16 +28,18 @@ var keyMirror = require('keyMirror'); var merge = require('merge'); var mixInto = require('mixInto'); -var invokedBeforeMount = function() { - invariant( - false, - 'You have invoked a method that is automatically bound, before the ' + - 'instance has been mounted. There is nothing conceptually wrong with ' + - 'this, but since this method will be replaced with a new version once ' + - 'the component is mounted - you should be aware of the fact that this ' + - 'method will soon be replaced.' - ); -}; +function invokeWithWarning(func) { + return function() { + console && console.warn && console.warn( + 'You have invoked a method that is automatically bound, before the ' + + 'instance has been mounted. There is nothing conceptually wrong with ' + + 'this, but since this method will be replaced with a new version once' + + ' the component is mounted - you should be aware of the fact that ' + + 'this method will soon be replaced.' + ); + return func.apply(this, arguments); + }; +} /** * Policies that describe methods in `ReactCompositeComponentInterface`. @@ -370,7 +372,10 @@ function mixSpecIntoComponent(Constructor, spec) { proto.__reactAutoBindMap = {}; } proto.__reactAutoBindMap[name] = property; - proto[name] = invokedBeforeMount; + proto[name] = property; + if (__DEV__) { + proto[name] = invokeWithWarning(property); + } } else { if (isInherited) { // For methods which are defined more than once, call the existing @@ -393,13 +398,13 @@ function mixSpecIntoComponent(Constructor, spec) { * @private */ function createChainedFunction(one, two) { - return function chainedFunction(a, b, c, d, e, tooMany) { + return function chainedFunction(a, b, c, d, e, f, g, tooMany) { invariant( typeof tooMany === 'undefined', - 'Chained function can only take a maximum of 5 arguments.' + 'Chained function can only take a maximum of 7 arguments.' ); - one.call(this, a, b, c, d, e); - two.call(this, a, b, c, d, e); + one.call(this, a, b, c, d, e, f, g); + two.call(this, a, b, c, d, e, f, g); }; } @@ -814,12 +819,12 @@ var ReactCompositeComponentMixin = { */ _bindAutoBindMethod: function(method) { var component = this; - function autoBound(a, b, c, d, e, tooMany) { + function autoBound(a, b, c, d, e, f, g, tooMany) { invariant( typeof tooMany === 'undefined', - 'React.autoBind(...): Methods can only take a maximum of 5 arguments.' + 'React.autoBind(...): Methods can only take a maximum of 7 arguments.' ); - return method.call(component, a, b, c, d, e); + return method.call(component, a, b, c, d, e, f, g); } return autoBound; } diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index d6623e9daa..ee48a4bcb1 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -188,15 +188,6 @@ describe('ReactCompositeComponent', function() { }); var instance = ; - // Autobound methods will throw before mounting. - // TODO: We should actually allow component instance methods to be invoked - // before mounting for read-only operations. We would then update this test. - expect(function() { - instance.methodToBeExplicitlyBound.bind(instance)(); - }).toThrow(); - expect(function() { - instance.methodAutoBound(); - }).toThrow(); expect(function() { instance.methodExplicitlyNotBound(); }).not.toThrow(); From a62686622b4fbd2acd903d1879f77623373ff3bd Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 16:09:35 -0700 Subject: [PATCH 49/94] Clean up naming Fix a small style nit --- src/core/ReactComponent.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index f4333af4a6..5db60d5fc5 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -63,7 +63,7 @@ var CHILD_HAS_NO_IDENTITY = var CHILD_CAME_FROM_ANOTHER_OWNER = '. It was passed a child from '; -var owner_has_warned = {}; +var ownerHasWarned = {}; /** * Helpers for flattening child arguments onto a new array or use an existing @@ -120,8 +120,8 @@ function assignKey(groupingIndex, child, index) { var childOwnerName = child.props[OWNER] && child.props[OWNER].constructor.displayName; - if (currentName && !owner_has_warned.hasOwnProperty(currentName)) { - owner_has_warned[currentName] = true; + if (currentName && !ownerHasWarned.hasOwnProperty(currentName)) { + ownerHasWarned[currentName] = true; var message = CHILD_HAS_NO_IDENTITY + currentName; if (childOwnerName && currentName !== childOwnerName) { // Usually the current owner is the offender, but if it accepts From 7c60bb3e548980f3636190ecf08392d9f26aebb5 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 16:09:38 -0700 Subject: [PATCH 50/94] Add unit test to prevent regression The original autobinding diff made some assumptions about how methods were called on components that had to be reverted. This diff enforces those assumptions in a test --- src/core/__tests__/ReactCompositeComponent-test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index ee48a4bcb1..6bfd0b03d5 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -188,6 +188,14 @@ describe('ReactCompositeComponent', function() { }); var instance = ; + // These are controversial assertions for now, they just exist + // because existing code depends on these assumptions. + expect(function() { + instance.methodToBeExplicitlyBound.bind(instance)(); + }).not.toThrow(); + expect(function() { + instance.methodAutoBound(); + }).not.toThrow(); expect(function() { instance.methodExplicitlyNotBound(); }).not.toThrow(); From 4f2d8dfe720aa5061ada4b027848b180a662b068 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 16:09:43 -0700 Subject: [PATCH 51/94] Dynamic Input Component Summary: This is a proposal based loosely on the discussions I've had with @paulshen and @jwalke. It implements a shim for `React.DOM.input` (used as ``) that supports two different use cases depending on whether `value` is provided or not. If a `value` is //not// provided, the input will be initialized with the empty string (or `defaultValue`) and anytime the user changes the input, the `onChange` (or `onTextChange`) handler will be fired and the DOM will reflect the new changes. React.renderComponent( , container ); If a `value` is provided, the input will be initialized to that value. Anytime the user changes the input, the `onChange` (or `onTextChange`) handler will be fired. However, the DOM will //not// reflect the new changes. If a `value` is provided, it is the responsibility of the owner to update the `value` prop passed in. var value = "Untitled"; var input = React.renderComponent( , container ); function handleChange(event) { // Do something cool like strip out non-numbers. var value = event.target.value.replace(/\D/g, ''); input.setProps({value: value}); } This is just a start and we should build similar components for `textarea` and `select`. Also, this does not inject the new components because the changes are not backward compatible. Once we change all `` uses to use `ReactDOMInput`, then we can inject. --- src/core/ReactDefaultInjection.js | 11 +- src/{core => dom/components}/ReactDOMForm.js | 0 src/dom/components/ReactDOMInput.js | 123 +++++++++++++++++++ 3 files changed, 129 insertions(+), 5 deletions(-) rename src/{core => dom/components}/ReactDOMForm.js (100%) create mode 100644 src/dom/components/ReactDOMInput.js diff --git a/src/core/ReactDefaultInjection.js b/src/core/ReactDefaultInjection.js index 27fb2afba0..d67203aef8 100644 --- a/src/core/ReactDefaultInjection.js +++ b/src/core/ReactDefaultInjection.js @@ -45,14 +45,15 @@ function inject() { 'TextChangeEventPlugin': TextChangeEventPlugin }); - /* - * This is a bit of a hack. We need to override the
element - * to be a composite component because IE8 does not bubble or capture - * submit to the top level. In order to make this work with our - * dependency graph we need to inject it here. + /** + * This is a bit of a hack. We need to override the element to be a + * composite component because IE8 does not bubble or capture submit to the + * top level. In order to make this work with our dependency graph we need to + * inject it here. */ ReactDOM.injection.injectComponentClasses({ form: ReactDOMForm + // TODO: Inject `ReactDOMInput`. }); } diff --git a/src/core/ReactDOMForm.js b/src/dom/components/ReactDOMForm.js similarity index 100% rename from src/core/ReactDOMForm.js rename to src/dom/components/ReactDOMForm.js diff --git a/src/dom/components/ReactDOMInput.js b/src/dom/components/ReactDOMInput.js new file mode 100644 index 0000000000..9a0ca10160 --- /dev/null +++ b/src/dom/components/ReactDOMInput.js @@ -0,0 +1,123 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule ReactDOMInput + */ + +"use strict"; + +var DOMPropertyOperations = require('DOMPropertyOperations'); +var ReactCompositeComponent = require('ReactCompositeComponent'); +var ReactDOM = require('ReactDOM'); + +var merge = require('merge'); + +// Store a reference to the `ReactNativeComponent`. +var input = ReactDOM.input; + +/** + * Implements an native 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 = ReactCompositeComponent.createClass({ + + getInitialState: function() { + return { + checked: this.props.defaultChecked || false, + value: this.props.defaultValue || '' + }; + }, + + getChecked: function() { + return this.props.checked != null ? this.props.checked : this.state.checked; + }, + + getValue: function() { + return this.props.value != null ? this.props.value : this.state.value; + }, + + render: function() { + // Clone `this.props` so we don't mutate the input. + var props = merge(this.props); + + props.checked = this.getChecked(); + props.value = this.getValue(); + + // TODO: Re-implement `onChange` with the implementation of `onTextChange`. + if (props.type === 'text' || props.type === 'password') { + props.onTextChange = this.handleTextChange; + } else { + props.onChange = this.handleChange; + } + + return input(props, this.props.children); + }, + + componentDidUpdate: function(prevProps, prevState, rootNode) { + if (this.props.checked != null) { + DOMPropertyOperations.setValueForProperty( + rootNode, + 'checked', + this.props.checked || false + ); + } + if (this.props.value != null) { + DOMPropertyOperations.setValueForProperty( + rootNode, + 'value', + this.props.value || '' + ); + } + }, + + handleTextChange: ReactCompositeComponent.autoBind(function(event) { + var returnValue; + if (this.props.onTextChange) { + returnValue = this.props.onTextChange(event); + } + this.setState({ + checked: event.target.checked, + value: event.target.value + }); + return returnValue; + }), + + handleChange: ReactCompositeComponent.autoBind(function(event) { + var returnValue; + if (this.props.onChange) { + returnValue = this.props.onChange(event); + } + this.setState({ + checked: event.target.checked, + value: event.target.value + }); + return returnValue; + }) + +}); + +module.exports = ReactDOMInput; From 14102e8a4851119afe66866735d63d7901ab470f Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 16:09:46 -0700 Subject: [PATCH 52/94] Fix isMounted() for composite components This behavior seemed incorrect for composite components. - isComponentMounted() represents ReactComponent's lifecycle of mounting - isMounted() represents ReactCompositeComponent's lifecycle of isMounted() Therefore, ReactComponents no longer have isMounted(). I think this is fine since it was not supposed to be public anyway. --- src/core/ReactCompositeComponent.js | 22 +++++++++++++++---- src/core/__tests__/ReactComponent-test.js | 19 ++++++++++++++++ .../__tests__/ReactCompositeComponent-test.js | 4 ++-- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index e97b5439fc..df9e5bbd78 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -326,8 +326,9 @@ function validateMethodOverride(proto, name) { function validateLifeCycleOnReplaceState(instance) { var compositeLifeCycleState = instance._compositeLifeCycleState; invariant( - instance.isMounted(), - 'replaceState(...): Can only update a mounted component.' + instance.isMounted() || + compositeLifeCycleState === CompositeLifeCycle.MOUNTING, + 'replaceState(...): Can only update a mounted or mounting component.' ); invariant( compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE && @@ -480,6 +481,17 @@ var ReactCompositeComponentMixin = { ReactCurrentOwner.getDepth() + 1 : 0; }, + /** + * Checks whether or not this composite component is mounted. + * @return {boolean} True if mounted, false otherwise. + * @protected + * @final + */ + isMounted: function() { + return ReactComponent.Mixin.isMounted.call(this) && + this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING; + }, + /** * Initializes the component, renders markup, and registers event listeners. * @@ -756,8 +768,10 @@ var ReactCompositeComponentMixin = { forceUpdate: function() { var compositeLifeCycleState = this._compositeLifeCycleState; invariant( - this.isMounted(), - 'forceUpdate(...): Can only force an update on mounted components.' + this.isMounted() || + compositeLifeCycleState === CompositeLifeCycle.MOUNTING, + 'forceUpdate(...): Can only force an update on mounted or mounting ' + + 'components.' ); invariant( compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE && diff --git a/src/core/__tests__/ReactComponent-test.js b/src/core/__tests__/ReactComponent-test.js index a753ce0c89..5b1d96a9e2 100644 --- a/src/core/__tests__/ReactComponent-test.js +++ b/src/core/__tests__/ReactComponent-test.js @@ -95,4 +95,23 @@ describe('ReactComponent', function() { ReactTestUtils.renderIntoDocument(instance); }); + it('should correctly determine if a component is mounted', function() { + var Component = React.createClass({ + componentWillMount: function() { + expect(this.isMounted()).toBeFalsy(); + }, + componentDidMount: function() { + expect(this.isMounted()).toBeTruthy(); + }, + render: function() { + return
; + } + }); + + var instance = ; + + expect(instance.isMounted()).toBeFalsy(); + ReactTestUtils.renderIntoDocument(instance); + expect(instance.isMounted()).toBeTruthy(); + }); }); diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index 6bfd0b03d5..c9aad6498a 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -308,7 +308,7 @@ describe('ReactCompositeComponent', function() { instance.forceUpdate(); }).toThrow( 'Invariant Violation: forceUpdate(...): Can only force an update on ' + - 'mounted components.' + 'mounted or mounting components.' ); React.renderComponent(instance, container); @@ -321,7 +321,7 @@ describe('ReactCompositeComponent', function() { instance.forceUpdate(); }).toThrow( 'Invariant Violation: forceUpdate(...): Can only force an update on ' + - 'mounted components.' + 'mounted or mounting components.' ); }); From 76ec746341b2ce3a98f8ca2183cfdd23fcd2d783 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 16:09:50 -0700 Subject: [PATCH 53/94] Require Statement Nits --- src/core/ReactEventTopLevelCallback.js | 2 +- src/core/ReactInstanceHandles.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/ReactEventTopLevelCallback.js b/src/core/ReactEventTopLevelCallback.js index be70785073..25719e812c 100644 --- a/src/core/ReactEventTopLevelCallback.js +++ b/src/core/ReactEventTopLevelCallback.js @@ -21,9 +21,9 @@ var ExecutionEnvironment = require('ExecutionEnvironment'); var ReactEventEmitter = require('ReactEventEmitter'); +var ReactID = require('ReactID'); var ReactInstanceHandles = require('ReactInstanceHandles'); -var ReactID = require('ReactID'); var getEventTarget = require('getEventTarget'); /** diff --git a/src/core/ReactInstanceHandles.js b/src/core/ReactInstanceHandles.js index 50564ac00c..dd8e68c219 100644 --- a/src/core/ReactInstanceHandles.js +++ b/src/core/ReactInstanceHandles.js @@ -20,6 +20,7 @@ "use strict"; var ReactID = require('ReactID'); + var invariant = require('invariant'); var SEPARATOR = '.'; From de4084259759e7c70aad9583b2b2aadc82e41911 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 16:09:54 -0700 Subject: [PATCH 54/94] Use `@return`, not `@returns` --- src/core/ReactID.js | 2 +- src/dom/DOMChildrenOperations.js | 2 +- src/dom/Danger.js | 2 +- src/event/EventPluginUtils.js | 6 +++--- src/utils/OrderedMap.js | 4 ++-- src/utils/Transaction.js | 2 +- src/utils/accumulate.js | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/ReactID.js b/src/core/ReactID.js index 9bfba32bfa..3ce1d42469 100644 --- a/src/core/ReactID.js +++ b/src/core/ReactID.js @@ -31,7 +31,7 @@ var nodeCache = {}; * DOM node (such as window). * * @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node. - * @returns {string} ID of the supplied `domNode`. + * @return {string} ID of the supplied `domNode`. */ function getID(node) { if (node && node.getAttributeNode) { diff --git a/src/dom/DOMChildrenOperations.js b/src/dom/DOMChildrenOperations.js index ce0f762a39..b1562e8e79 100644 --- a/src/dom/DOMChildrenOperations.js +++ b/src/dom/DOMChildrenOperations.js @@ -47,7 +47,7 @@ var REMOVE_AT = keyOf({removeAt: null}); * * @param {Element} parent Parent DOM node. * @param {Object} childOperations Description of child operations. - * @returns {Array?} Sparse array containing elements by their current index in + * @return {Array?} Sparse array containing elements by their current index in * the DOM. */ var _getNodesByOriginalIndex = function(parent, childOperations) { diff --git a/src/dom/Danger.js b/src/dom/Danger.js index 1181739679..21fd331fa6 100644 --- a/src/dom/Danger.js +++ b/src/dom/Danger.js @@ -65,7 +65,7 @@ function getParentDummy(parent) { * @param {Element} elem Parent element. * @param {Element} insert Element to insert. * @param {Element} after Element to insert after. - * @returns {Element} Element that was inserted. + * @return {Element} Element that was inserted. */ function insertNodeAfterNode(elem, insert, after) { if (__DEV__) { diff --git a/src/event/EventPluginUtils.js b/src/event/EventPluginUtils.js index 686f7073eb..c9d063f620 100644 --- a/src/event/EventPluginUtils.js +++ b/src/event/EventPluginUtils.js @@ -106,7 +106,7 @@ function executeDispatchesInOrder(event, executeDispatch) { * Standard/simple iteration through an event's collected dispatches, but stops * at the first dispatch execution returning true, and returns that id. * - * @returns id of the first dispatch execution who's listener returns true, or + * @return id of the first dispatch execution who's listener returns true, or * null if no listener returned true. */ function executeDispatchesInOrderStopAtTrue(event) { @@ -140,7 +140,7 @@ function executeDispatchesInOrderStopAtTrue(event) { * return values at each dispatch execution, but it does tend to make sense when * dealing with "direct" dispatches. * - * @returns The return value of executing the single dispatch. + * @return The return value of executing the single dispatch. */ function executeDirectDispatch(event) { if (__DEV__) { @@ -162,7 +162,7 @@ function executeDirectDispatch(event) { /** * @param {SyntheticEvent} event - * @returns {bool} True iff number of dispatches accumulated is greater than 0. + * @return {bool} True iff number of dispatches accumulated is greater than 0. */ function hasDispatches(event) { return !!event._dispatchListeners; diff --git a/src/utils/OrderedMap.js b/src/utils/OrderedMap.js index 429ddf8307..b4ea88216f 100644 --- a/src/utils/OrderedMap.js +++ b/src/utils/OrderedMap.js @@ -246,7 +246,7 @@ var OrderedMapMethods = { * * @param {Function} cb Callback to invoke for each item. * @param {Object?=} context Context to invoke callback from. - * @returns {OrderedMap} OrderedMap that results from mapping. + * @return {OrderedMap} OrderedMap that results from mapping. */ map: function(cb, context) { return this.mapRange(cb, 0, this.length, context); @@ -288,7 +288,7 @@ var OrderedMapMethods = { * * @param {Function} cb Callback to invoke for each item. * @param {Object?=} context Context to invoke callback from. - * @returns {OrderedMap} OrderedMap that results from filtering. + * @return {OrderedMap} OrderedMap that results from filtering. */ filter: function(cb, context) { return this.filterRange(cb, 0, this.length, context); diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index a851e81f32..8e630d88ab 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -153,7 +153,7 @@ var Mixin = { * @param {Object} scope Scope to invoke from. * @param {Object?=} args... Arguments to pass to the method (optional). * Helps prevent need to bind in many cases. - * @returns Return value from `method`. + * @return Return value from `method`. */ perform: function(method, scope, a, b, c, d, e, f) { throwIf(this.isInTransaction(), DUAL_TRANSACTION); diff --git a/src/utils/accumulate.js b/src/utils/accumulate.js index e87a8a3b6b..86f58b966a 100644 --- a/src/utils/accumulate.js +++ b/src/utils/accumulate.js @@ -35,7 +35,7 @@ if (__DEV__) { * depending on the total count (if greater than one, an array is allocated). * Handles most common case first (starting with an empty current value and * acquiring one). - * @returns {Accumulation} An accumulation which is either a single item or an + * @return {Accumulation} An accumulation which is either a single item or an * Array of items. */ function accumulate(cur, next) { From f0a4ca5f69a4c0bffe40aa26335a7171ab6e7883 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 16:49:12 -0700 Subject: [PATCH 55/94] Remove React.autoBind entirely --- src/core/__tests__/ReactBind-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/__tests__/ReactBind-test.js b/src/core/__tests__/ReactBind-test.js index 4d31f38ab0..47e77bb9d8 100644 --- a/src/core/__tests__/ReactBind-test.js +++ b/src/core/__tests__/ReactBind-test.js @@ -40,7 +40,7 @@ describe('React.autoBind', function() { }, onMouseEnter: ReactDoNotBindDeprecated.doNotBind(mouseDidEnter), onMouseLeave: ReactDoNotBindDeprecated.doNotBind(mouseDidLeave), - onClick: React.autoBind(mouseDidClick), + onClick: mouseDidClick, // auto binding only occurs on top level functions in class defs. badIdeas: { @@ -110,7 +110,7 @@ describe('React.autoBind', function() { var mouseDidClick = mocks.getMockFunction(); var TestMixin = { - onClick: React.autoBind(mouseDidClick) + onClick: mouseDidClick }; var TestBindComponent = React.createClass({ From 6bbcbc08cf5e2af9e9c0d0cde75148880df69fd8 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 16:49:16 -0700 Subject: [PATCH 56/94] Rename `BrowserEnv` to `ViewportMetrics` Becuase that's what it is. --- src/core/ReactEventEmitter.js | 4 ++-- .../BrowserEnv.js => dom/ViewportMetrics.js} | 20 +++++++++---------- src/event/synthetic/SyntheticMouseEvent.js | 6 +++--- src/eventPlugins/TapEventPlugin.js | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) rename src/{core/BrowserEnv.js => dom/ViewportMetrics.js} (73%) diff --git a/src/core/ReactEventEmitter.js b/src/core/ReactEventEmitter.js index ed552fc4d9..65f5072bbf 100644 --- a/src/core/ReactEventEmitter.js +++ b/src/core/ReactEventEmitter.js @@ -19,11 +19,11 @@ "use strict"; -var BrowserEnv = require('BrowserEnv'); var EventConstants = require('EventConstants'); var EventListener = require('EventListener'); var EventPluginHub = require('EventPluginHub'); var ExecutionEnvironment = require('ExecutionEnvironment'); +var ViewportMetrics = require('ViewportMetrics'); var invariant = require('invariant'); var isEventSupported = require('isEventSupported'); @@ -126,7 +126,7 @@ function trapCapturedEvent(topLevelType, handlerBaseName, element) { * @see http://www.quirksmode.org/dom/events/scroll.html */ function registerScrollValueMonitoring() { - var refresh = BrowserEnv.refreshAuthoritativeScrollValues; + var refresh = ViewportMetrics.refreshScrollValues; EventListener.listen(window, 'scroll', refresh); EventListener.listen(window, 'resize', refresh); } diff --git a/src/core/BrowserEnv.js b/src/dom/ViewportMetrics.js similarity index 73% rename from src/core/BrowserEnv.js rename to src/dom/ViewportMetrics.js index 53937a4ada..e9f2d3d1e0 100644 --- a/src/core/BrowserEnv.js +++ b/src/dom/ViewportMetrics.js @@ -13,24 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @providesModule BrowserEnv + * @providesModule ViewportMetrics */ "use strict"; -/** - * A place to share/cache browser/chrome level computations. - */ -var BrowserEnv = { +var ViewportMetrics = { + currentScrollLeft: 0, + currentScrollTop: 0, - browserInfo: null, - refreshAuthoritativeScrollValues: function() { - BrowserEnv.currentScrollLeft = + + refreshScrollValues: function() { + ViewportMetrics.currentScrollLeft = document.body.scrollLeft + document.documentElement.scrollLeft; - BrowserEnv.currentScrollTop = + ViewportMetrics.currentScrollTop = document.body.scrollTop + document.documentElement.scrollTop; } + }; -module.exports = BrowserEnv; +module.exports = ViewportMetrics; diff --git a/src/event/synthetic/SyntheticMouseEvent.js b/src/event/synthetic/SyntheticMouseEvent.js index 6272fbb83a..5e5dd8ded8 100644 --- a/src/event/synthetic/SyntheticMouseEvent.js +++ b/src/event/synthetic/SyntheticMouseEvent.js @@ -19,8 +19,8 @@ "use strict"; -var BrowserEnv = require('BrowserEnv'); var SyntheticUIEvent = require('SyntheticUIEvent'); +var ViewportMetrics = require('ViewportMetrics'); /** * @interface MouseEvent @@ -61,12 +61,12 @@ var MouseEventInterface = { pageX: function(event) { return 'pageX' in event ? event.pageX : - event.clientX + BrowserEnv.currentPageScrollLeft; + event.clientX + ViewportMetrics.currentPageScrollLeft; }, pageY: function(event) { return 'pageY' in event ? event.pageY : - event.clientY + BrowserEnv.currentPageScrollTop; + event.clientY + ViewportMetrics.currentPageScrollTop; } }; diff --git a/src/eventPlugins/TapEventPlugin.js b/src/eventPlugins/TapEventPlugin.js index 03293eaf16..75409ab4b3 100644 --- a/src/eventPlugins/TapEventPlugin.js +++ b/src/eventPlugins/TapEventPlugin.js @@ -19,11 +19,11 @@ "use strict"; -var BrowserEnv = require('BrowserEnv'); var EventPluginUtils = require('EventPluginUtils'); var EventPropagators = require('EventPropagators'); var SyntheticUIEvent = require('SyntheticUIEvent'); var TouchEventUtils = require('TouchEventUtils'); +var ViewportMetrics = require('ViewportMetrics'); var keyOf = require('keyOf'); @@ -49,7 +49,7 @@ function getAxisCoordOfEvent(axis, nativeEvent) { } return axis.page in nativeEvent ? nativeEvent[axis.page] : - nativeEvent[axis.client] + BrowserEnv[axis.envScroll]; + nativeEvent[axis.client] + ViewportMetrics[axis.envScroll]; } function getDistance(coords, nativeEvent) { From bd8ecc1caa59dba79d79a126343a666ef4513582 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 18:27:32 -0700 Subject: [PATCH 57/94] Restore @spicyj's lifecycle tests These got lost in a rebase. --- .../__tests__/ReactComponentLifeCycle-test.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/core/__tests__/ReactComponentLifeCycle-test.js b/src/core/__tests__/ReactComponentLifeCycle-test.js index a3c18be304..3f2df5e82e 100644 --- a/src/core/__tests__/ReactComponentLifeCycle-test.js +++ b/src/core/__tests__/ReactComponentLifeCycle-test.js @@ -434,5 +434,69 @@ describe('ReactComponentLifeCycle', function() { expect(instance.state.stateField).toBe('goodbye'); }); + it('should call nested lifecycle methods in the right order', function() { + var log; + var logger = function(msg) { + return function() { + // return true for shouldComponentUpdate + log.push(msg); + return true; + }; + }; + var Outer = React.createClass({ + render: function() { + return
; + }, + componentWillMount: logger('outer componentWillMount'), + componentDidMount: logger('outer componentDidMount'), + componentWillReceiveProps: logger('outer componentWillReceiveProps'), + shouldComponentUpdate: logger('outer shouldComponentUpdate'), + componentWillUpdate: logger('outer componentWillUpdate'), + componentDidUpdate: logger('outer componentDidUpdate'), + componentWillUnmount: logger('outer componentWillUnmount') + }); + var Inner = React.createClass({ + render: function() { + return {this.props.x}; + }, + componentWillMount: logger('inner componentWillMount'), + componentDidMount: logger('inner componentDidMount'), + componentWillReceiveProps: logger('inner componentWillReceiveProps'), + shouldComponentUpdate: logger('inner shouldComponentUpdate'), + componentWillUpdate: logger('inner componentWillUpdate'), + componentDidUpdate: logger('inner componentDidUpdate'), + componentWillUnmount: logger('inner componentWillUnmount') + }); + var instance; + + log = []; + instance = ReactTestUtils.renderIntoDocument(); + expect(log).toEqual([ + 'outer componentWillMount', + 'inner componentWillMount', + 'inner componentDidMount', + 'outer componentDidMount' + ]); + + log = []; + instance.setProps({x: 42}); + expect(log).toEqual([ + 'outer componentWillReceiveProps', + 'outer shouldComponentUpdate', + 'outer componentWillUpdate', + 'inner componentWillReceiveProps', + 'inner shouldComponentUpdate', + 'inner componentWillUpdate', + 'inner componentDidUpdate', + 'outer componentDidUpdate' + ]); + + log = []; + instance.unmountComponent(); + expect(log).toEqual([ + 'outer componentWillUnmount', + 'inner componentWillUnmount' + ]); + }); }); From c1886c651365e1db415a9efd2d4e621eea20cf32 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Mon, 24 Jun 2013 18:27:35 -0700 Subject: [PATCH 58/94] Use ReactID.ATTR_NAME as the React-specific ID attribute name. Another step in the plan towards making `ReactID.ATTR_NAME` the central source of truth regarding the React-specific ID attribute name. --- src/core/ReactID.js | 1 + src/core/ReactMount.js | 1 + src/core/ReactNativeComponent.js | 3 ++- src/core/ReactTextComponent.js | 3 ++- .../__tests__/ReactServerRendering-test.js | 19 ++++++++++++++----- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/core/ReactID.js b/src/core/ReactID.js index 3ce1d42469..ea59e0b09b 100644 --- a/src/core/ReactID.js +++ b/src/core/ReactID.js @@ -120,6 +120,7 @@ function purgeEntireCache() { nodeCache = {}; } +exports.ATTR_NAME = ATTR_NAME; exports.getID = getID; exports.setID = setID; exports.getNode = getNode; diff --git a/src/core/ReactMount.js b/src/core/ReactMount.js index 2d9ddfcdb5..ae6f263f19 100644 --- a/src/core/ReactMount.js +++ b/src/core/ReactMount.js @@ -55,6 +55,7 @@ function getReactRootID(container) { * * ReactMount.renderComponent(component, $('container')); * + * TODO Update this comment when ReactID.ATTR_NAME changes. *
<-- Supplied `container`. *
<-- Rendered reactRoot of React component. * // ... diff --git a/src/core/ReactNativeComponent.js b/src/core/ReactNativeComponent.js index 07371b2c8f..eddb27ec5f 100644 --- a/src/core/ReactNativeComponent.js +++ b/src/core/ReactNativeComponent.js @@ -24,6 +24,7 @@ var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactComponent = require('ReactComponent'); var ReactEventEmitter = require('ReactEventEmitter'); var ReactMultiChild = require('ReactMultiChild'); +var ReactID = require('ReactID'); var escapeTextForBrowser = require('escapeTextForBrowser'); var flattenChildren = require('flattenChildren'); @@ -138,7 +139,7 @@ ReactNativeComponent.Mixin = { } } - return ret + ' id="' + this._rootNodeID + '">'; + return ret + ' ' + ReactID.ATTR_NAME + '="' + this._rootNodeID + '">'; }, /** diff --git a/src/core/ReactTextComponent.js b/src/core/ReactTextComponent.js index 4b7aa495ac..57e56bbce2 100644 --- a/src/core/ReactTextComponent.js +++ b/src/core/ReactTextComponent.js @@ -20,6 +20,7 @@ "use strict"; var ReactComponent = require('ReactComponent'); +var ReactID = require('ReactID'); var escapeTextForBrowser = require('escapeTextForBrowser'); var mixInto = require('mixInto'); @@ -57,7 +58,7 @@ mixInto(ReactTextComponent, { mountComponent: function(rootID) { ReactComponent.Mixin.mountComponent.call(this, rootID); return ( - '' + + '' + escapeTextForBrowser(this.props.text) + '' ); diff --git a/src/environment/__tests__/ReactServerRendering-test.js b/src/environment/__tests__/ReactServerRendering-test.js index ebbad6b4b0..c8cc58ebe5 100644 --- a/src/environment/__tests__/ReactServerRendering-test.js +++ b/src/environment/__tests__/ReactServerRendering-test.js @@ -24,10 +24,12 @@ require('mock-modules') .dontMock('ExecutionEnvironment') .dontMock('React') + .dontMock('ReactID') .dontMock('ReactServerRendering') .dontMock('ReactTestUtils'); var React; +var ReactID; var ReactTestUtils; var ReactServerRendering; var ExecutionEnvironment; @@ -36,6 +38,7 @@ describe('ReactServerRendering', function() { beforeEach(function() { require('mock-modules').dumpCache(); React = require('React'); + ReactID = require('ReactID'); ReactTestUtils = require('ReactTestUtils'); ExecutionEnvironment = require('ExecutionEnvironment'); ExecutionEnvironment.canUseDOM = false; @@ -51,7 +54,7 @@ describe('ReactServerRendering', function() { } ); expect(response).toMatch( - 'hello world' + 'hello world' ); }); @@ -74,8 +77,12 @@ describe('ReactServerRendering', function() { } ); expect(response).toMatch( - '
My name is ' + - 'child
' + '
' + + '' + + 'My name is ' + + 'child' + + '' + + '
' ); }); @@ -123,8 +130,10 @@ describe('ReactServerRendering', function() { ); expect(response).toMatch( - 'Component name: ' + - 'TestComponent' + '' + + 'Component name: ' + + 'TestComponent' + + '' ); expect(lifecycle).toEqual( ['getInitialState', 'componentWillMount', 'render'] From b282a0f4f16bc86ef821f82dcf720385116f0989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Mon, 24 Jun 2013 16:38:21 -0700 Subject: [PATCH 59/94] The compare_size cache gets wiped out with `grunt clean` `.grunt/` is the directory that is suggested for storing task related files, and this file is not useful if it gets wiped out often. So I'm moving the compare_size cache into `.grunt/` so we keep it around longer. --- grunt/config/compare_size.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grunt/config/compare_size.js b/grunt/config/compare_size.js index e785fe5281..fb59d94cdb 100644 --- a/grunt/config/compare_size.js +++ b/grunt/config/compare_size.js @@ -12,6 +12,6 @@ module.exports = { return gzip.zip(contents, {}).length; } }, - cache: "build/.sizecache.json" + cache: ".grunt/sizecache.json" } }; From f6c4d2d16110e45bc9969cf6ad42290dd4d09c60 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Wed, 19 Jun 2013 00:23:33 -0700 Subject: [PATCH 60/94] Check that `container` is a valid DOM element --- src/core/ReactComponent.js | 4 ++++ src/core/ReactMount.js | 2 +- src/core/__tests__/ReactComponent-test.js | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index 530a6f9c66..94507b6968 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -511,6 +511,10 @@ var ReactComponent = { container, transaction, shouldReuseMarkup) { + invariant( + container && container.nodeType === 1, + 'mountComponentIntoNode(...): Target container is not a DOM element.' + ); var renderStart = Date.now(); var markup = this.mountComponent(rootID, transaction); ReactMount.totalInstantiationTime += (Date.now() - renderStart); diff --git a/src/core/ReactMount.js b/src/core/ReactMount.js index 6eaf42cfd6..f11c509142 100644 --- a/src/core/ReactMount.js +++ b/src/core/ReactMount.js @@ -35,7 +35,7 @@ var containersByReactRootID = {}; * @return {?*} DOM element that may have the reactRoot ID, or null. */ function getReactRootElementInContainer(container) { - return container.firstChild; + return container && container.firstChild; } /** diff --git a/src/core/__tests__/ReactComponent-test.js b/src/core/__tests__/ReactComponent-test.js index a753ce0c89..2dbe49aedb 100644 --- a/src/core/__tests__/ReactComponent-test.js +++ b/src/core/__tests__/ReactComponent-test.js @@ -31,6 +31,24 @@ describe('ReactComponent', function() { reactComponentExpect = require('reactComponentExpect'); }); + it('should throw on invalid render targets', function() { + var container = document.createElement('div'); + // jQuery objects are basically arrays; people often pass them in by mistake + expect(function() { + React.renderComponent(
, [container]); + }).toThrow( + 'Invariant Violation: mountComponentIntoNode(...): Target container is ' + + 'not a DOM element.' + ); + + expect(function() { + React.renderComponent(
, null); + }).toThrow( + 'Invariant Violation: mountComponentIntoNode(...): Target container is ' + + 'not a DOM element.' + ); + }); + it('should throw when supplying a ref outside of render method', function() { var instance =
; expect(function() { From 1c40dde7825e7d8863d69357dbd7013f926e35f6 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 25 Jun 2013 13:56:41 -0700 Subject: [PATCH 61/94] Remove ReactID.primeTree in favor of priming in ReactID.getID. Although it would have been nice to prime the entire tree and achieve a cache hit rate of 100%, that cost would have to be paid up front, during page rendering. This patch avoids priming up front in favor of making the most of the work done by `ReactMount.findReactRenderedDOMNodeSlow`, which calls `ReactID.getID` while traversing the rendered DOM. The insight is this: if `getID` simply primes the cache whenever it finds a new ID, then `findReactRenderedDOMNodeSlow` will end up priming quite a few more nodes that are actually involved in `ReactID.getNode` lookups, and we won't need `primeTree` at all. --- src/core/ReactID.js | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/src/core/ReactID.js b/src/core/ReactID.js index ea59e0b09b..f791285557 100644 --- a/src/core/ReactID.js +++ b/src/core/ReactID.js @@ -36,7 +36,16 @@ var nodeCache = {}; function getID(node) { if (node && node.getAttributeNode) { var attributeNode = node.getAttributeNode(ATTR_NAME); - return attributeNode && attributeNode.value || ''; + if (attributeNode) { + var id = attributeNode.value; + if (id) { + // Assume that any node previously cached with this ID has since + // become invalid and should be replaced. TODO Enforce this. + nodeCache[id] = node; + + return id; + } + } } return ''; @@ -79,31 +88,6 @@ function getNode(id) { return null; } -/** - * Efficiently finds all nodes with a React-specific ID and primes the - * cache so that getNode lookups take constant time. - * - * @param {DOMElement} root The root element to scan. - */ -function primeTree(root) { - var nodes = root.querySelectorAll - ? root.querySelectorAll('[' + ATTR_NAME + ']') - : root.getElementsByTagName('*'); - - for (var i = 0; i < nodes.length; ++i) { - prime(nodes.item(i)); - } - - prime(root); -} - -function prime(node) { - var attributeNode = node.getAttributeNode(ATTR_NAME); - if (attributeNode) { - nodeCache[attributeNode.value] = node; - } -} - /** * Causes the cache to forget about one React-specific ID. * @@ -124,6 +108,5 @@ exports.ATTR_NAME = ATTR_NAME; exports.getID = getID; exports.setID = setID; exports.getNode = getNode; -exports.primeTree = primeTree; exports.purgeID = purgeID; exports.purgeEntireCache = purgeEntireCache; From 1d65f81b1617e339f36fad285bc45f805bb899f2 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 25 Jun 2013 13:56:48 -0700 Subject: [PATCH 62/94] Remove ReactDOMNodeCache and getDOMNodeID. These modules have been superseded by `ReactID`. Since they were only used internally, and I have updated all client code that previously assumed their existence, I believe they can be dropped for good. --- src/core/ReactDOMNodeCache.js | 26 -------------------------- src/dom/getDOMNodeID.js | 27 --------------------------- 2 files changed, 53 deletions(-) delete mode 100644 src/core/ReactDOMNodeCache.js delete mode 100644 src/dom/getDOMNodeID.js diff --git a/src/core/ReactDOMNodeCache.js b/src/core/ReactDOMNodeCache.js deleted file mode 100644 index 908c2ddbcd..0000000000 --- a/src/core/ReactDOMNodeCache.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule ReactDOMNodeCache - * @typechecks - */ - -"use strict"; - -var ReactID = require('ReactID'); - -exports.getNodeByID = ReactID.getNode; -exports.purgeID = ReactID.purgeID; -exports.purgeEntireCache = ReactID.purgeEntireCache; diff --git a/src/dom/getDOMNodeID.js b/src/dom/getDOMNodeID.js deleted file mode 100644 index dddd6fadee..0000000000 --- a/src/dom/getDOMNodeID.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule getDOMNodeID - * @typechecks - */ - -"use strict"; - -var ReactID = require('ReactID'); - -function getDOMNodeID(domNode) { - return ReactID.getID(domNode); -} -module.exports = getDOMNodeID; From fb6381fb35c8af868b5134c3d7a91699a7d01090 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 25 Jun 2013 13:56:51 -0700 Subject: [PATCH 63/94] Upgrade TextChangeEventPlugin to ChangeEventPlugin and support more form elements Upgrade `TextChangeEventPlugin` to be the `onChange` event that React fires. In React, `onChange` will now fire when `input` fires for form elements in modern browsers. Handle this for: input[type=text] input[type=password] input[type=checkbox] input[type=radio] textarea select Support: - OSX Chrome - OSX Safari - OSX Firefox - Win 7 / IE8 - Win 7 / IE9 - Win 7 / IE10 Everything works but caret selection / placement differs from browser to browser. For ); }).toThrow(); expect(function() { - var stub = ReactTestUtils.renderIntoDocument( + ReactTestUtils.renderIntoDocument( ); }).toThrow(); From 738de8cfa846936ae754ccfda607e4fb41c2170b Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Fri, 28 Jun 2013 16:33:45 -0700 Subject: [PATCH 86/94] Improve `findComponentRoot` Error Message Instead of simply logging the React ID of the `ancestorNode` when `findComponentRoot` fails, use a `try ... finally` to `console.error` the `ancestorNode`. This allows modern web inspectors to actually log a reference to the node (which may not have a React ID). This means when people run into the problem, they will not have to execute: require('ReactID').getNode(); NOTE: Admittedly, this will not log anything in IE8. That's fine, since IE8 has shitty console logging anyway. --- src/core/ReactInstanceHandles.js | 11 +++++++---- src/core/__tests__/ReactInstanceHandles-test.js | 7 +++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/core/ReactInstanceHandles.js b/src/core/ReactInstanceHandles.js index 000856604b..02ea765e0a 100644 --- a/src/core/ReactInstanceHandles.js +++ b/src/core/ReactInstanceHandles.js @@ -296,12 +296,15 @@ var ReactInstanceHandles = { } child = child.nextSibling; } + global.console && console.error && console.error( + 'Error while invoking `findComponentRoot` with the following ' + + 'ancestor node:', + ancestorNode + ); invariant( false, - 'findComponentRoot: Unable to find element by React ID, `%s`. This ' + - 'indicates that someone (or the browser) has mutated the DOM tree in ' + - 'an unexpected way. Try inspecting the child nodes of the element with ' + - 'React ID, `%s`.', + 'findComponentRoot(..., %s): Unable to find element. This probably ' + + 'means the DOM was unexpectedly mutated (e.g. by the browser).', id, ReactID.getID(ancestorNode) ); diff --git a/src/core/__tests__/ReactInstanceHandles-test.js b/src/core/__tests__/ReactInstanceHandles-test.js index 4cde75ab5d..110612c1c8 100644 --- a/src/core/__tests__/ReactInstanceHandles-test.js +++ b/src/core/__tests__/ReactInstanceHandles-test.js @@ -139,10 +139,9 @@ describe('ReactInstanceHandles', function() { ReactID.getID(childNodeB) ); }).toThrow( - 'Invariant Violation: findComponentRoot: Unable to find element by ' + - 'React ID, `.react[0].1:0`. This indicates that someone (or the ' + - 'browser) has mutated the DOM tree in an unexpected way. Try ' + - 'inspecting the child nodes of the element with React ID, `.react[0]`.' + 'Invariant Violation: findComponentRoot(..., .react[0].1:0): Unable ' + + 'to find element. This probably means the DOM was unexpectedly ' + + 'mutated (e.g. by the browser).' ); }); }); From 55176116a225c7664d082580faa6dde12a3bb00e Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Fri, 28 Jun 2013 16:33:49 -0700 Subject: [PATCH 87/94] Implement `ReactDOMTextarea` This changes `ReactDOMTextarea` to accept `defaultValue` and `value`. It will warn people about using children (but allow it and treat it as `defaultValue`, which is the current behavior). --- src/core/ReactDOM.js | 6 +- src/core/ReactDOMTextarea.js | 92 ------------- src/core/__tests__/ReactDOMTextarea-test.js | 66 --------- src/dom/components/ReactDOMTextarea.js | 129 ++++++++++++++++++ .../__tests__/ReactDOMTextarea-test.js | 93 +++++++++++++ 5 files changed, 224 insertions(+), 162 deletions(-) delete mode 100644 src/core/ReactDOMTextarea.js delete mode 100644 src/core/__tests__/ReactDOMTextarea-test.js create mode 100644 src/dom/components/ReactDOMTextarea.js create mode 100644 src/dom/components/__tests__/ReactDOMTextarea-test.js diff --git a/src/core/ReactDOM.js b/src/core/ReactDOM.js index 0b7c4e3907..2a9749c9be 100644 --- a/src/core/ReactDOM.js +++ b/src/core/ReactDOM.js @@ -80,8 +80,7 @@ var ReactDOM = objMapKeyVal({ embed: true, fieldset: false, footer: false, - // Danger: this gets monkeypatched! See ReactDOMForm for more info. - form: false, + form: false, // NOTE: Injected, see `ReactDOMForm`. h1: false, h2: false, h3: false, @@ -116,8 +115,7 @@ var ReactDOM = objMapKeyVal({ table: false, tbody: false, td: false, - // Danger: this gets monkeypatched! See ReactDOMTextarea for more info. - textarea: false, + textarea: false, // NOTE: Injected, see `ReactDOMTextarea`. tfoot: false, th: false, thead: false, diff --git a/src/core/ReactDOMTextarea.js b/src/core/ReactDOMTextarea.js deleted file mode 100644 index a9f22e0b65..0000000000 --- a/src/core/ReactDOMTextarea.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule ReactDOMTextarea - */ - -"use strict"; - -var ReactCompositeComponent = require('ReactCompositeComponent'); -var ReactDOM = require('ReactDOM'); - -var invariant = require('invariant'); - -// Store a reference to the ); - var node = stub.getDOMNode(); - - expect(node.value).toEqual('giraffe'); - - stub.replaceProps({ children: 'gorilla' }); - expect(node.value).toEqual('gorilla'); - - stub.replaceProps({ children: 17 }); - expect(node.value).toEqual('17'); - - stub.replaceProps({ children: [42] }); - expect(node.value).toEqual('42'); - - stub.replaceProps({ children: null }); - expect(node.value).toEqual(''); - - stub.replaceProps({ content: 'eggplant' }); - expect(node.value).toEqual('eggplant'); - }); - - it("should throw with multiple or invalid children", function() { - expect(function() { - ReactTestUtils.renderIntoDocument( - - ); - }).toThrow(); - - expect(function() { - ReactTestUtils.renderIntoDocument( - - ); - }).toThrow(); - }); -}); diff --git a/src/dom/components/ReactDOMTextarea.js b/src/dom/components/ReactDOMTextarea.js new file mode 100644 index 0000000000..ddba826a23 --- /dev/null +++ b/src/dom/components/ReactDOMTextarea.js @@ -0,0 +1,129 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule ReactDOMTextarea + */ + +"use strict"; + +var DOMPropertyOperations = require('DOMPropertyOperations'); +var ReactCompositeComponent = require('ReactCompositeComponent'); +var ReactDOM = require('ReactDOM'); + +var invariant = require('invariant'); +var merge = require('merge'); + +// Store a reference to the ; + var node = renderTextarea(stub); + + expect(node.value).toBe('giraffe'); + + // Changing children should do nothing, it functions like `defaultValue`. + stub.replaceProps({children: 'gorilla'}); + expect(node.value).toEqual('giraffe'); + }); + + it('should allow numbers as children', function() { + var node = renderTextarea(); + expect(node.value).toBe('17'); + }); + + it("should throw with multiple or invalid children", function() { + expect(function() { + ReactTestUtils.renderIntoDocument( + + ); + }).toThrow(); + + expect(function() { + ReactTestUtils.renderIntoDocument( + + ); + }).toThrow(); + }); +}); From 15272f30f426dc02349856f4a4dc0f0dad8afb9a Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Fri, 28 Jun 2013 16:33:56 -0700 Subject: [PATCH 88/94] Don't keep the HTML escaped ID internally, only in HTML generation A dynamic value can be provided as a key to a child. Either as part of an object or key property. This becomes part of the component's ID. We have to be careful to escape this key before inserting it into the DOM since it could become a vulnerability. We fixed this by escaping just the keys. However, the current implementation breaks when you used escaped keys. The internal value is escaped and the value used by getAttributeNode and getElementById are both unescaped. This fixes that by keeping the unescaped value internally but escaping it right before the HTML is generated (like any other attribute). This is important since business logic IDs (that should be used as keys) contains characters that need to be escaped. --- src/core/ReactNativeComponent.js | 3 +- src/core/__tests__/ReactIdentity-test.js | 46 ++++++++++++++++++++++++ src/utils/__tests__/mapChildren-test.js | 4 +-- src/utils/flattenChildren.js | 10 ++---- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/core/ReactNativeComponent.js b/src/core/ReactNativeComponent.js index c6a9783e90..17f205f278 100644 --- a/src/core/ReactNativeComponent.js +++ b/src/core/ReactNativeComponent.js @@ -139,7 +139,8 @@ ReactNativeComponent.Mixin = { } } - return ret + ' ' + ReactID.ATTR_NAME + '="' + this._rootNodeID + '">'; + var escapedID = escapeTextForBrowser(this._rootNodeID); + return ret + ' ' + ReactID.ATTR_NAME + '="' + escapedID + '">'; }, /** diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js index 36998e9f62..9d638d5579 100644 --- a/src/core/__tests__/ReactIdentity-test.js +++ b/src/core/__tests__/ReactIdentity-test.js @@ -106,6 +106,52 @@ describe('ReactIdentity', function() { checkId(node.childNodes[2].firstChild, '.r[0].[0:2].[0:chipmunk]'); }); + function renderAComponentWithKeyIntoContainer(key, container) { + var span1 = ; + var span2 = ; + + var map = {}; + map[key] = span2; + + React.renderComponent(
{[span1, map]}
, container); + + expect(span1.getDOMNode()).not.toBe(null); + expect(span2.getDOMNode()).not.toBe(null); + + checkId(span1.getDOMNode(), '.r[0].[0:0:' + key + ']'); + checkId(span2.getDOMNode(), '.r[0].[1]{' + key + '}'); + } + + it('should allow any character as a key, in a detached parent', function() { + var detachedContainer = document.createElement('div'); + renderAComponentWithKeyIntoContainer("<'WEIRD/&\\key'>", detachedContainer); + }); + + it('should allow any character as a key, in an attached parent', function() { + // This test exists to protect against implementation details that + // incorrectly query escaped IDs using DOM tools like getElementById. + var attachedContainer = document.createElement('div'); + document.body.appendChild(attachedContainer); + + renderAComponentWithKeyIntoContainer("<'WEIRD/&\\key'>", attachedContainer); + + document.body.removeChild(attachedContainer); + }); + + it('should not allow scripts in keys to execute', function() { + var h4x0rKey = '">
1) { + var childArray = Array(childrenLength); + for (var i = 0; i < childrenLength; i++) { + if (__DEV__) { + validateChildKeys(arguments[i + 1]); + } + childArray[i] = arguments[i + 1]; } - + this.props.children = childArray; } - - // Subsequent arguments are rolled into one child array. Array arguments - // are flattened onto it. This is inlined to avoid extra heap allocation. - var targetArray = null; - for (var i = 1; i < arguments.length; i++) { - var child = arguments[i]; - if (Array.isArray(child)) { - if (child.length === 0) { - continue; - } - - if (targetArray === null) { - targetArray = []; - } - appendNestedChildren(i - 1, child, targetArray); - - } else if (!isEmptyChild(child)) { - - // Only truthy internal keys are valid. If it's not, we assign one. - if (ReactComponent.isValidComponent(child) && !child._key) { - // This is a static node and therefore safe to key by index. - // No warning necessary. - child._key = createKey(child.props.key, i - 1); - } - - if (targetArray === null) { - targetArray = []; - } - targetArray.push(child); - - } - } - this.props.children = targetArray; }, /** diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index be911dc6f8..e7fef2733b 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -472,7 +472,6 @@ var ReactCompositeComponentMixin = { this.state = null; this._pendingState = null; this._compositeLifeCycleState = null; - this._compositionLevel = ReactCurrentOwner.getDepth() + 1; }, /** @@ -521,8 +520,6 @@ var ReactCompositeComponentMixin = { this._renderedComponent = this._renderValidatedComponent(); - this._renderedComponent._compositionLevel = this._compositionLevel + 1; - // Done with mounting, `setState` will now trigger UI changes. this._compositeLifeCycleState = null; var markup = this._renderedComponent.mountComponent(rootID, transaction); diff --git a/src/core/ReactCurrentOwner.js b/src/core/ReactCurrentOwner.js index 9ac0ab6ede..ad137cdf2e 100644 --- a/src/core/ReactCurrentOwner.js +++ b/src/core/ReactCurrentOwner.js @@ -32,19 +32,7 @@ var ReactCurrentOwner = { * @internal * @type {ReactComponent} */ - current: null, - - /** - * @internal - * @return {number} - */ - getDepth: function() { - var owner = ReactCurrentOwner.current; - if (!owner) { - return 0; - } - return owner._compositionLevel; - } + current: null }; diff --git a/src/core/__tests__/ReactComponentFlattenChildren-test.js b/src/core/__tests__/ReactComponentFlattenChildren-test.js deleted file mode 100644 index 9ebe7baa2a..0000000000 --- a/src/core/__tests__/ReactComponentFlattenChildren-test.js +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @jsx React.DOM - * @emails react-core - */ - -"use strict"; - -var React; - -var Fake; - -var singleChild = function(children) { - return ({children}).props.children; -}; - -var multiChild = function(child1, child2, child3, child4) { - return ({child1} {child2} {child3}{child4}).props.children; -}; - - -describe('ReactComponentFlatten', function() { - beforeEach(function() { - require('mock-modules').dumpCache(); - React = require('React'); - var ReactComponent = require('ReactComponent'); - var copyProperties = require('copyProperties'); - var Constructor = function() {}; - copyProperties(Constructor.prototype, ReactComponent.Mixin); - Fake = function() { - var instance = new Constructor(); - instance.construct.apply(instance, arguments); - return instance; - }; - }); - - it("should reuse a flat array", function() { - var children = [ -
, -
- ]; - - var flat = singleChild(children); - - expect(flat).toBe(children); - expect(flat[0]._key).toEqual('0:0:foo'); - expect(flat[1]._key).toEqual('0:0:bar'); - }); - - it("should collapse a sparse array", function() { - var children = [ - null, -
, -
- ]; - - var flat = singleChild(children); - - expect(flat).not.toBe(children); - expect(flat.length).toEqual(2); - expect(flat[0]._key).toEqual('0:0:foo'); - expect(flat[1]._key).toEqual('0:0:bar'); - }); - - it("should collapse a nested array into a flat array", function() { - var flat = multiChild( - null, -
, - [
, null] - ); - - expect(flat.length).toEqual(2); - expect(flat[0]._key).toEqual('0:foo'); - expect(flat[1]._key).toEqual('0:2:bar'); - }); - - it("should retain key index despite static empty values", function() { - var before = multiChild( -
, -
, - [
], - [
] - ); - - var after = multiChild( - null, -
, - null, - [
] - ); - - expect(before.length).toEqual(4); - expect(after.length).toEqual(2); - expect(before[1]._key).toEqual(after[0]._key); - expect(before[3]._key).toEqual(after[1]._key); - }); - - it("should retain key index when wrapped", function() { - var before = singleChild(multiChild( -
, -
, - [
], - [
] - )); - - var after = singleChild(multiChild( - null, -
, - null, - [
] - )); - - expect(before.length).toEqual(4); - expect(after.length).toEqual(2); - expect(before[1]._key).toEqual(after[0]._key); - expect(before[3]._key).toEqual(after[1]._key); - }); - - it("should assign idempotent strings through flattening", function() { - var children = [ - 'FOO', - 'BAR' - ]; - var flat = singleChild(children); - var wrappedFlat = multiChild(null, flat); - expect(flat).toBe(children); - expect(wrappedFlat.length).toBe(2); - expect(wrappedFlat[0]).toBe(flat[0]); - expect(wrappedFlat[1]).toBe(flat[1]); - }); - - it("cannot keep keys unique when children are unboxed", function() { - // This is a case that difficult to solve and should not actually be solved. - var children1 = [
]; - var children2 = [
]; - var flat1 = singleChild(children1); - var flat2 = singleChild(children2); - // There's no way to tell flat1[0] and flat2[0] apart. - var mergedChildren = [flat1[0], flat2[0]]; - var newChildren = multiChild(mergedChildren); - expect(newChildren.length).toBe(2); - expect(newChildren[0]._key).toBe(newChildren[1]._key); - }); - -}); - - diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js index 9d638d5579..fac5cde23d 100644 --- a/src/core/__tests__/ReactIdentity-test.js +++ b/src/core/__tests__/ReactIdentity-test.js @@ -55,8 +55,8 @@ describe('ReactIdentity', function() { React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); reactComponentExpect(instance).toBeDOMComponentWithChildCount(2); - checkId(node.childNodes[0], '.r[0].[0]{first}'); - checkId(node.childNodes[1], '.r[0].[0]{second}'); + checkId(node.childNodes[0], '.r[0].{first}'); + checkId(node.childNodes[1], '.r[0].{second}'); }); it('should allow key property to express identity', function() { @@ -71,10 +71,10 @@ describe('ReactIdentity', function() { React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); reactComponentExpect(instance).toBeDOMComponentWithChildCount(4); - checkId(node.childNodes[0], '.r[0].[0:apple]'); - checkId(node.childNodes[1], '.r[0].[0:banana]'); - checkId(node.childNodes[2], '.r[0].[0:0]'); - checkId(node.childNodes[3], '.r[0].[0:123]'); + checkId(node.childNodes[0], '.r[0].[apple]'); + checkId(node.childNodes[1], '.r[0].[banana]'); + checkId(node.childNodes[2], '.r[0].[0]'); + checkId(node.childNodes[3], '.r[0].[123]'); }); it('should use instance identity', function() { @@ -95,15 +95,13 @@ describe('ReactIdentity', function() { React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); reactComponentExpect(instance).toBeDOMComponentWithChildCount(3); - checkId(node.childNodes[0], '.r[0].[0:wrap1]'); - checkId( - node.childNodes[0].firstChild, - '.r[0].[0:wrap1].[0:squirrel]' - ); - checkId(node.childNodes[1], '.r[0].[0:wrap2]'); - checkId(node.childNodes[1].firstChild, '.r[0].[0:wrap2].[0:bunny]'); - checkId(node.childNodes[2], '.r[0].[0:2]'); - checkId(node.childNodes[2].firstChild, '.r[0].[0:2].[0:chipmunk]'); + + checkId(node.childNodes[0], '.r[0].[wrap1]'); + checkId(node.childNodes[0].firstChild, '.r[0].[wrap1].[squirrel]'); + checkId(node.childNodes[1], '.r[0].[wrap2]'); + checkId(node.childNodes[1].firstChild, '.r[0].[wrap2].[bunny]'); + checkId(node.childNodes[2], '.r[0].[2]'); + checkId(node.childNodes[2].firstChild, '.r[0].[2].[chipmunk]'); }); function renderAComponentWithKeyIntoContainer(key, container) { @@ -118,7 +116,7 @@ describe('ReactIdentity', function() { expect(span1.getDOMNode()).not.toBe(null); expect(span2.getDOMNode()).not.toBe(null); - checkId(span1.getDOMNode(), '.r[0].[0:0:' + key + ']'); + checkId(span1.getDOMNode(), '.r[0].[' + key + ']'); checkId(span2.getDOMNode(), '.r[0].[1]{' + key + '}'); } @@ -247,7 +245,7 @@ describe('ReactIdentity', function() { }).not.toThrow(); }); - it('should retain keys during updates in composite components', function() { + it('should retain key during updates in composite components', function() { var TestComponent = React.createClass({ render: function() { @@ -283,19 +281,13 @@ describe('ReactIdentity', function() { React.renderComponent(wrapped, document.createElement('div')); - var beforeKey = wrapped - ._renderedComponent - ._renderedComponent - .props.children[0]._key; + var beforeID = ReactID.getID(wrapped.getDOMNode().firstChild); wrapped.swap(); - var afterKey = wrapped - ._renderedComponent - ._renderedComponent - .props.children[0]._key; + var afterID = ReactID.getID(wrapped.getDOMNode().firstChild); - expect(beforeKey).not.toEqual(afterKey); + expect(beforeID).not.toEqual(afterID); }); diff --git a/src/core/__tests__/ReactMultiChildReconcile-test.js b/src/core/__tests__/ReactMultiChildReconcile-test.js index 81b680c35b..0ff3cf2eab 100644 --- a/src/core/__tests__/ReactMultiChildReconcile-test.js +++ b/src/core/__tests__/ReactMultiChildReconcile-test.js @@ -42,12 +42,11 @@ var stripEmptyValues = function(obj) { }; /** - * These children are wrapped in an array and therefore their keys are prefixed. - * Their name is also wrapped in an prefix and suffix character. We strip those + * Child names is are wrapped in an prefix and suffix character. We strip those * out. This relies on a tiny implementation detail of the rendering system. */ var getOriginalKey = function(childName) { - return childName.substr(4, childName.length - 5); + return childName.substr(1, childName.length - 2); }; /** diff --git a/src/utils/__tests__/mapChildren-test.js b/src/utils/__tests__/mapChildren-test.js index 3e1d3ca236..3f34482b09 100644 --- a/src/utils/__tests__/mapChildren-test.js +++ b/src/utils/__tests__/mapChildren-test.js @@ -48,11 +48,17 @@ describe('mapChildren', function() { this.addMatchers({ toHaveKeys: function(expected) { + if (!Array.isArray(this.actual)) { + if (expected.length !== 1) { + return false; + } + return this.actual.props.key === expected[0]; + } if (this.actual.length != expected.length) { return false; } return this.actual.every(function(component, index) { - return component._key === expected[index]; + return component.props.key === expected[index]; }, this); } }); @@ -73,9 +79,9 @@ describe('mapChildren', function() { .expectRenderedChild() .instance(); - expect(mapFn).toHaveBeenCalledWith(simpleKid, '0:simple', 0); + expect(mapFn).toHaveBeenCalledWith(simpleKid, '[simple]', 0); expect(rendered.props.children[0]).toBe(simpleKid); - expect(rendered.props.children).toHaveKeys(['0:simple']); + expect(rendered.props.children).toHaveKeys(['[simple]']); }); it('should pass key to returned component', function() { @@ -93,10 +99,10 @@ describe('mapChildren', function() { .instance(); expect(rendered.props.children[0]).not.toBe(simpleKid); - expect(rendered.props.children[0].props.children[0]).toBe(simpleKid); - expect(rendered.props.children).toHaveKeys(['1:0:0:simple']); + expect(rendered.props.children[0].props.children).toBe(simpleKid); + expect(rendered.props.children).toHaveKeys(['[simple]']); expect(rendered.props.children[0].props.children) - .toHaveKeys(['0:simple']); + .toHaveKeys(['simple']); }); it('should be called for each child', function() { @@ -123,12 +129,12 @@ describe('mapChildren', function() { .instance(); expect(mapFn.calls.length).toBe(3); - expect(mapFn).toHaveBeenCalledWith(kidOne, '0:one', 0); - expect(mapFn).toHaveBeenCalledWith(kidTwo, '0:two', 1); - expect(mapFn).toHaveBeenCalledWith(kidThree, '0:three', 2); + expect(mapFn).toHaveBeenCalledWith(kidOne, '[one]', 0); + expect(mapFn).toHaveBeenCalledWith(kidTwo, '[two]', 1); + expect(mapFn).toHaveBeenCalledWith(kidThree, '[three]', 2); expect(rendered.props.children).not.toEqual(instance.props.children); expect(rendered.props.children).toHaveKeys([ - '1:0:0:one', '1:0:0:two', '1:0:0:three' + '[one]', '[two]', '[three]' ]); }); }); diff --git a/src/utils/flattenChildren.js b/src/utils/flattenChildren.js index 7f18ab2fbd..a0e65d2137 100644 --- a/src/utils/flattenChildren.js +++ b/src/utils/flattenChildren.js @@ -18,6 +18,7 @@ "use strict"; +var ReactComponent = require('ReactComponent'); var ReactTextComponent = require('ReactTextComponent'); var throwIf = require('throwIf'); @@ -39,27 +40,24 @@ var DUPLICATE_KEY_ERROR = 'You have two children with identical keys. Make sure that you set the ' + '"key" property to a unique value such as a row ID.'; -/** - * If there is only a single child, it still needs a name. - */ -var ONLY_CHILD_NAME = '0'; - var flattenChildrenImpl = function(res, children, nameSoFar) { - var key; if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { var child = children[i]; - key = child && (child._key || (child.props && child.props.key)); flattenChildrenImpl( res, child, - nameSoFar + '[' + (key ? key : ('' + i)) + ']' + nameSoFar + '[' + ReactComponent.getKey(child, i) + ']' ); } } else { var type = typeof children; var isOnlyChild = nameSoFar === ''; - var storageName = isOnlyChild ? ONLY_CHILD_NAME : nameSoFar; + // If it's the only child, treat the name as if it was wrapped in an array + // so that it's consistent if the number of children grows + var storageName = isOnlyChild ? + '[' + ReactComponent.getKey(children, 0) + ']' : + nameSoFar; if (children === null || children === undefined || type === 'boolean') { res[storageName] = null; } else if (children.mountComponentIntoNode) { @@ -71,7 +69,7 @@ var flattenChildrenImpl = function(res, children, nameSoFar) { } else { if (type === 'object') { throwIf(children && children.nodeType === 1, INVALID_CHILD); - for (key in children) { + for (var key in children) { if (children.hasOwnProperty(key)) { flattenChildrenImpl( res, diff --git a/src/utils/mapChildren.js b/src/utils/mapChildren.js index a357eefa06..fbdfd0cb11 100644 --- a/src/utils/mapChildren.js +++ b/src/utils/mapChildren.js @@ -18,14 +18,14 @@ "use strict"; +var flattenChildren = require('flattenChildren'); + /** * Maps children that are typically specified as `props.children`. * * The provided mapFunction(child, key, index) will be called for each * leaf child. * - * Note: mapChildren assumes children have already been flattened. - * * @param {array} children * @param {function(*, string, int)} mapFunction * @param {*} context @@ -36,12 +36,20 @@ function mapChildren(children, mapFunction, context) { return children; } var mappedChildren = []; - for (var ii = 0; ii < children.length; ii++) { - var child = children[ii]; - var key = child._key; - var mappedChild = mapFunction.call(context, child, key, ii); - mappedChild.props.key = key; - mappedChildren.push(mappedChild); + var flattenedMap = flattenChildren(children); + var ii = 0; + for (var key in flattenedMap) { + if (!flattenedMap.hasOwnProperty(key)) { + continue; + } + var child = flattenedMap[key]; + // In this version of map children we ignore empty children. + if (child !== null) { + var mappedChild = mapFunction.call(context, child, key, ii); + mappedChild.props.key = key; + mappedChildren.push(mappedChild); + ii++; + } } return mappedChildren; } diff --git a/src/utils/onlyChild.js b/src/utils/onlyChild.js index 2a78dcbfda..ccc5137668 100644 --- a/src/utils/onlyChild.js +++ b/src/utils/onlyChild.js @@ -24,24 +24,20 @@ var invariant = require('invariant'); /** * Returns the first child in a collection of children and verifies that there * is only one child in the collection. The current implementation of this - * function assumes that children have been flattened, but the purpose of this - * helper function is to abstract away the particular structure of children. + * function assumes that a single child gets passed without a wrapper, but the + * purpose of this helper function is to abstract away the particular structure + * of children. * * @param {?object} children Child collection structure. * @return {ReactComponent} The first and only `ReactComponent` contained in the * structure. */ function onlyChild(children) { - invariant(Array.isArray(children), 'onlyChild must be passed a valid Array.'); invariant( - children.length === 1, - 'onlyChild must be passed an Array with exactly one child.' + ReactComponent.isValidComponent(children), + 'onlyChild must be passed a children with exactly one child.' ); - invariant( - ReactComponent.isValidComponent(children[0]), - 'onlyChild must be passed an Array with exactly one child.' - ); - return children[0]; + return children; } module.exports = onlyChild; From e748be32da843211e68e60e3ec247bbfeef1d33a Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Mon, 1 Jul 2013 16:29:56 -0400 Subject: [PATCH 93/94] Bump Commoner version to fix EMFILE errors. Finally found a more robust solution for the "too many open files" problem: https://github.com/benjamn/commoner/commit/ad72ba42db. Closes #137. Closes #138. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e531b0e7c3..d85eb0a0d7 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "base62": "~0.1.1", - "commoner": "~0.7.0", + "commoner": "~0.7.1", "esprima": "https://github.com/facebook/esprima/tarball/a3e0ea3979eb8d54d8bfade220c272903f928b1e", "recast": "~0.4.8", "source-map": "~0.1.22" From 18ef8962f1d1c8cf4d776a101e127a1ce8966c21 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Mon, 1 Jul 2013 16:50:35 -0400 Subject: [PATCH 94/94] Bump Commoner version to disable dependency scanning by default. If you are using bin/jsx independently, you may need to pass --follow-requires to it if you rely on its dependency scanning. Dependency scanning is still a good idea, but it's difficult to make it work perfectly for everyone the first time they try bin/jsx. Closes #131. --- grunt/tasks/jsx.js | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/grunt/tasks/jsx.js b/grunt/tasks/jsx.js index 7aa8407596..290816711f 100644 --- a/grunt/tasks/jsx.js +++ b/grunt/tasks/jsx.js @@ -11,6 +11,7 @@ module.exports = function() { var args = [ "--cache-dir", ".module-cache", "--relativize", + "--follow-requires", config.sourceDir, config.outputDir ]; diff --git a/package.json b/package.json index d85eb0a0d7..7eb7e57801 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "base62": "~0.1.1", - "commoner": "~0.7.1", + "commoner": "~0.8.0", "esprima": "https://github.com/facebook/esprima/tarball/a3e0ea3979eb8d54d8bfade220c272903f928b1e", "recast": "~0.4.8", "source-map": "~0.1.22"