@samitouri / QOS-React-2 / commits / 5a13dd090d

Standardize prop -> DOM attribute process

Allow more than strings and numbers to be used as attributes for DOM nodes. This removes the special casing for `0` and `false` that was being used in ReactDOMInput and ReactDOMTextarea. Now we will just `toString` any object we try to insert into a DOM. Closes #422, #372, #302

SanderSpies committed Oct 13, 2013 at 08:45 UTC 5a13dd090d63c0c779c0e3b75a3a7bc70459c456
7 files changed +225 -60
src/dom/components/ReactDOMInput.js
+3 -11
@@ -55,7 +55,7 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
55 var defaultValue = this.props.defaultValue;
56 return {
57 checked: this.props.defaultChecked || false,
58 - value: defaultValue != null && defaultValue !== false ? defaultValue : ''
58 + value: defaultValue != null ? defaultValue : ''
59 };
60 },
61
@@ -74,9 +74,7 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
74 this.props.checked != null ? this.props.checked : this.state.checked;
75
76 var value = this.getValue();
77 - props.value = value != null && value !== false ?
78 - '' + value :
79 - this.state.value;
77 + props.value = value != null ? value : this.state.value;
78
79 props.onChange = this._handleChange;
80
@@ -105,13 +103,7 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
103
104 var value = this.getValue();
105 if (value != null) {
108 - // Cast `this.props.value` to a string so falsey values that cast to
109 - // truthy strings are not ignored.
110 - DOMPropertyOperations.setValueForProperty(
111 - rootNode,
112 - 'value',
113 - value !== false ? '' + value : ''
114 - );
106 + DOMPropertyOperations.setValueForProperty(rootNode, 'value', value);
107 }
108 },
109
src/dom/components/ReactDOMTextarea.js
+5 -14
@@ -29,9 +29,6 @@ var merge = require('merge');
29 // Store a reference to the <textarea> `ReactDOMComponent`.
30 var textarea = ReactDOM.textarea;
31
32 -// For quickly matching children type, to test if can be treated as content.
33 -var CONTENT_TYPES = {'string': true, 'number': true};
34 -
32 /**
33 * Implements a <textarea> native component that allows setting `value`, and
34 * `defaultValue`. This differs from the traditional DOM API because value is
@@ -72,11 +69,7 @@ var ReactDOMTextarea = ReactCompositeComponent.createClass({
69 );
70 children = children[0];
71 }
75 - invariant(
76 - CONTENT_TYPES[typeof children],
77 - 'If you specify children to <textarea>, it must be a single string ' +
78 - 'or number., not an array or object.'
79 - );
72 +
73 defaultValue = '' + children;
74 }
75 if (defaultValue == null) {
@@ -86,7 +79,9 @@ var ReactDOMTextarea = ReactCompositeComponent.createClass({
79 return {
80 // We save the initial value so that `ReactDOMComponent` doesn't update
81 // `textContent` (unnecessary since we update value).
89 - initialValue: value != null ? value : defaultValue,
82 + // The initial value can be a boolean or object so that's why it's
83 + // forced to be a string.
84 + initialValue: '' + (value != null ? value : defaultValue),
85 value: defaultValue
86 };
87 },
@@ -118,11 +113,7 @@ var ReactDOMTextarea = ReactCompositeComponent.createClass({
113 componentDidUpdate: function(prevProps, prevState, rootNode) {
114 var value = this.getValue();
115 if (value != null) {
121 - DOMPropertyOperations.setValueForProperty(
122 - rootNode,
123 - 'value',
124 - value !== false ? '' + value : ''
125 - );
116 + DOMPropertyOperations.setValueForProperty(rootNode, 'value', value);
117 }
118 },
119
src/dom/components/__tests__/ReactDOMInput-test.js
+54 -5
@@ -50,11 +50,31 @@ describe('ReactDOMInput', function() {
50 expect(node.value).toBe('0');
51 });
52
53 - it('should display "" for `defaultValue` of `false`', function() {
53 + it('should display "true" for `defaultValue` of `true`', function() {
54 + var stub = <input type="text" defaultValue={true} />;
55 + var node = renderTextInput(stub);
56 +
57 + expect(node.value).toBe('true');
58 + });
59 +
60 + it('should display "false" for `defaultValue` of `false`', function() {
61 var stub = <input type="text" defaultValue={false} />;
62 var node = renderTextInput(stub);
63
57 - expect(node.value).toBe('');
64 + expect(node.value).toBe('false');
65 + });
66 +
67 + it('should display "foobar" for `defaultValue` of `objectToString`', function() {
68 + var objectToString = {
69 + toString: function() {
70 + return "foobar";
71 + }
72 + };
73 +
74 + var stub = <input type="text" defaultValue={objectToString} />;
75 + var node = renderTextInput(stub);
76 +
77 + expect(node.value).toBe('foobar');
78 });
79
80 it('should display `value` of number 0', function() {
@@ -64,11 +84,40 @@ describe('ReactDOMInput', function() {
84 expect(node.value).toBe('0');
85 });
86
67 - it('should display "" for `value` of `false`', function() {
68 - var stub = <input type="text" value={false} />;
87 + it('should allow setting `value` to `true`', function() {
88 + var stub = <input type="text" value="yolo" />;
89 var node = renderTextInput(stub);
90
71 - expect(node.value).toBe('');
91 + expect(node.value).toBe('yolo');
92 +
93 + stub.replaceProps({value: true});
94 + expect(node.value).toEqual('true');
95 + });
96 +
97 + it("should allow setting `value` to `false`", function() {
98 + var stub = <input type="text" value="yolo" />;
99 + var node = renderTextInput(stub);
100 +
101 + expect(node.value).toBe('yolo');
102 +
103 + stub.replaceProps({value: false});
104 + expect(node.value).toEqual('false');
105 + });
106 +
107 + it('should allow setting `value` to `objectToString`', function() {
108 + var stub = <input type="text" value="foo" />;
109 + var node = renderTextInput(stub);
110 +
111 + expect(node.value).toBe('foo');
112 +
113 + var objectToString = {
114 + toString: function() {
115 + return "foobar";
116 + }
117 + };
118 +
119 + stub.replaceProps({value: objectToString});
120 + expect(node.value).toEqual('foobar');
121 });
122
123 it('should properly control a value of number `0`', function() {
src/dom/components/__tests__/ReactDOMSelect-test.js
+29
@@ -116,6 +116,35 @@ describe('ReactDOMSelect', function() {
116 expect(node.options[2].selected).toBe(false); // gorilla
117 });
118
119 + it('should allow setting `value` with `objectToString`', function() {
120 + var objectToString = {
121 + animal: "giraffe",
122 + toString: function() {
123 + return this.animal;
124 + }
125 + };
126 +
127 + var stub =
128 + <select multiple={true} value={[objectToString]}>
129 + <option value="monkey">A monkey!</option>
130 + <option value="giraffe">A giraffe!</option>
131 + <option value="gorilla">A gorilla!</option>
132 + </select>;
133 + var node = renderSelect(stub);
134 +
135 + expect(node.options[0].selected).toBe(false); // monkey
136 + expect(node.options[1].selected).toBe(true); // giraffe
137 + expect(node.options[2].selected).toBe(false); // gorilla
138 +
139 + // Changing the `value` prop should change the selected options.
140 + objectToString.animal = "monkey";
141 + stub.forceUpdate();
142 +
143 + expect(node.options[0].selected).toBe(true); // monkey
144 + expect(node.options[1].selected).toBe(false); // giraffe
145 + expect(node.options[2].selected).toBe(false); // gorilla
146 + });
147 +
148 it('should allow switching to multiple', function() {
149 var stub =
150 <select defaultValue="giraffe">
src/dom/components/__tests__/ReactDOMTextarea-test.js
+74 -13
@@ -62,14 +62,34 @@ describe('ReactDOMTextarea', function() {
62 expect(node.value).toBe('0');
63 });
64
65 - it('should display "" for `defaultValue` of `false`', function() {
65 + it('should display "false" for `defaultValue` of `false`', function() {
66 var stub = <textarea type="text" defaultValue={false} />;
67 var node = renderTextarea(stub);
68
69 - expect(node.value).toBe('');
69 + expect(node.value).toBe('false');
70 });
71
72 - it('should allow setting `value`', function() {
72 + it('should display "foobar" for `defaultValue` of `objectToString`', function() {
73 + var objectToString = {
74 + toString: function() {
75 + return "foobar";
76 + }
77 + };
78 +
79 + var stub = <textarea type="text" defaultValue={objectToString} />;
80 + var node = renderTextarea(stub);
81 +
82 + expect(node.value).toBe('foobar');
83 + });
84 +
85 + it('should display `value` of number 0', function() {
86 + var stub = <textarea value={0} />;
87 + var node = renderTextarea(stub);
88 +
89 + expect(node.value).toBe('0');
90 + });
91 +
92 + it('should allow setting `value` to `giraffe`', function() {
93 var stub = <textarea value="giraffe" />;
94 var node = renderTextarea(stub);
95
@@ -79,18 +99,39 @@ describe('ReactDOMTextarea', function() {
99 expect(node.value).toEqual('gorilla');
100 });
101
82 - it('should display `value` of number 0', function() {
83 - var stub = <textarea value={0} />;
102 + it('should allow setting `value` to `true`', function() {
103 + var stub = <textarea value="giraffe" />;
104 var node = renderTextarea(stub);
105
86 - expect(node.value).toBe('0');
106 + expect(node.value).toBe('giraffe');
107 +
108 + stub.replaceProps({value: true});
109 + expect(node.value).toEqual('true');
110 });
111
89 - it('should display "" for `value` of `false`', function() {
90 - var stub = <textarea type="text" value={false} />;
112 + it('should allow setting `value` to `false`', function() {
113 + var stub = <textarea value="giraffe" />;
114 var node = renderTextarea(stub);
115
93 - expect(node.value).toBe('');
116 + expect(node.value).toBe('giraffe');
117 +
118 + stub.replaceProps({value: false});
119 + expect(node.value).toEqual('false');
120 + });
121 +
122 + it('should allow setting `value` to `objectToString`', function() {
123 + var stub = <textarea value="giraffe" />;
124 + var node = renderTextarea(stub);
125 +
126 + expect(node.value).toBe('giraffe');
127 +
128 + var obj = {
129 + toString: function() {
130 + return "foo";
131 + }
132 + };
133 + stub.replaceProps({value: obj});
134 + expect(node.value).toEqual('foo');
135 });
136
137 it('should properly control a value of number `0`', function() {
@@ -123,6 +164,25 @@ describe('ReactDOMTextarea', function() {
164 expect(node.value).toBe('17');
165 });
166
167 + it('should allow booleans as children', function() {
168 + spyOn(console, 'warn');
169 + var node = renderTextarea(<textarea>{false}</textarea>);
170 + expect(console.warn.argsForCall.length).toBe(1);
171 + expect(node.value).toBe('false');
172 + });
173 +
174 + it('should allow objects as children', function() {
175 + spyOn(console, 'warn');
176 + var obj = {
177 + toString: function() {
178 + return "sharkswithlasers";
179 + }
180 + };
181 + var node = renderTextarea(<textarea>{obj}</textarea>);
182 + expect(console.warn.argsForCall.length).toBe(1);
183 + expect(node.value).toBe('sharkswithlasers');
184 + });
185 +
186 it('should throw with multiple or invalid children', function() {
187 spyOn(console, 'warn');
188
@@ -134,11 +194,12 @@ describe('ReactDOMTextarea', function() {
194
195 expect(console.warn.argsForCall.length).toBe(1);
196
197 + var stub;
198 expect(function() {
138 - ReactTestUtils.renderIntoDocument(
139 - <textarea><strong /></textarea>
140 - );
141 - }).toThrow();
199 + stub = renderTextarea(<textarea><strong /></textarea>);
200 + }).not.toThrow();
201 +
202 + expect(stub.value).toBe('[object Object]');
203
204 expect(console.warn.argsForCall.length).toBe(2);
205 });
src/utils/__tests__/escapeTextForBrowser-test.js new
+56
@@ -0,0 +1,56 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @emails react-core
17 + */
18 +
19 +"use strict";
20 +
21 +describe('escapeTextForBrowser', function() {
22 +
23 + var escapeTextForBrowser = require('escapeTextForBrowser');
24 +
25 + it('should escape boolean to string', function() {
26 + expect(escapeTextForBrowser(true)).toBe('true');
27 + expect(escapeTextForBrowser(false)).toBe('false');
28 + });
29 +
30 + it('should escape object to string', function() {
31 + var escaped = escapeTextForBrowser({
32 + toString: function() {
33 + return 'ponys';
34 + }
35 + });
36 +
37 + expect(escaped).toBe('ponys');
38 + });
39 +
40 + it('should escape number to string', function() {
41 + expect(escapeTextForBrowser(42)).toBe('42');
42 + });
43 +
44 + it('should escape string', function() {
45 + var escaped = escapeTextForBrowser('<script type=\'\' src=""></script>');
46 + expect(escaped).not.toContain('<');
47 + expect(escaped).not.toContain('>');
48 + expect(escaped).not.toContain('\'');
49 + expect(escaped).not.toContain('/');
50 + expect(escaped).not.toContain('\"');
51 +
52 + escaped = escapeTextForBrowser('&');
53 + expect(escaped).toBe('&amp;');
54 + });
55 +
56 +});
src/utils/escapeTextForBrowser.js
+4 -17
@@ -19,8 +19,6 @@
19
20 "use strict";
21
22 -var invariant = require('invariant');
23 -
22 var ESCAPE_LOOKUP = {
23 "&": "&amp;",
24 ">": "&gt;",
@@ -30,6 +28,8 @@ var ESCAPE_LOOKUP = {
28 "/": "&#x2f;"
29 };
30
31 +var ESCAPE_REGEX = /[&><"'\/]/g;
32 +
33 function escaper(match) {
34 return ESCAPE_LOOKUP[match];
35 }
@@ -37,24 +37,11 @@ function escaper(match) {
37 /**
38 * Escapes text to prevent scripting attacks.
39 *
40 - * @param {number|string} text Text value to escape.
40 + * @param {*} text Text value to escape.
41 * @return {string} An escaped string.
42 */
43 function escapeTextForBrowser(text) {
44 - var type = typeof text;
45 - invariant(
46 - type !== 'object',
47 - 'escapeTextForBrowser(...): Attempted to escape an object.'
48 - );
49 - if (text === '') {
50 - return '';
51 - } else {
52 - if (type === 'string') {
53 - return text.replace(/[&><"'\/]/g, escaper);
54 - } else {
55 - return (''+text).replace(/[&><"'\/]/g, escaper);
56 - }
57 - }
44 + return ('' + text).replace(ESCAPE_REGEX, escaper);
45 }
46
47 module.exports = escapeTextForBrowser;