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('&');
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
"&": "&",
24
">": ">",
@@ -30,6 +28,8 @@ var ESCAPE_LOOKUP = {
28
"/": "/"
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;