@samitouri / QOS-React / commits / 9254ce27a8

Split markup generation from DOM property management (#10197)

* Replace SSR unit test with integration test * Remove unit test that is already covered by integration suite * Replace unit tests for boolean attrs with integration tests * Replace unit test for aria attrs with integration test * Replace unit tests for numeric 0 with integration tests * Remove unit test covered by integration tests * Replace unit test for injection with integration test It still touches internals but it tests both renderers. * Fork DOMPropertyOperations into DOMMarkupOperations * Trim down DOMPropertyOperations and DOMMarkupOperations * Record SSR sizes * Record them tests * Fix false positive warning for overloaded booleans when passing numbers * Remove stray import * Replace CSS markup tests with public API tests Some of these are handy as integration tests so I moved them there. But some test markup specifically so I changed them to use DOMServer. * Make CSSPropertyOperations client-only I forked createMarkupForStyles() into ReactDOMComponent and ReactPartialRenderer. Duplication is fine because one of them will soon be gone (guess which one!) The warnInvalidStyle helper is used by both server and client, unlike other client-only stuff in CSSPropertyOperations, so I moved it to a separately module used in both. * Record server bundle size * Add an early exit to validation * Clarify what is being duplicated

Dan Abramov committed Jul 19, 2017 at 14:06 UTC 9254ce27a820c64fef51aeb4d39bdeeba5cb116d
11 files changed +742 -563
scripts/fiber/tests-passing.txt
+130 -28
@@ -683,15 +683,10 @@ src/renderers/dom/shared/__tests__/CSSProperty-test.js
683 * should generate browser prefixes for its `isUnitlessNumber`
684
685 src/renderers/dom/shared/__tests__/CSSPropertyOperations-test.js
686 -* should create markup for simple styles
687 -* should ignore undefined styles
688 -* should ignore null styles
689 -* should return null for no styles
686 * should automatically append `px` to relevant styles
687 * should trim values
688 * should not append `px` to styles that might need a number
689 * should create vendor-prefixed markup correctly
694 -* should create markup with unitless css custom property
690 * should set style attribute when styles exist
691 * should not set style attribute when no styles exist
692 * should warn when using hyphenated style names
@@ -704,13 +699,6 @@ src/renderers/dom/shared/__tests__/CSSPropertyOperations-test.js
699 * should not add units to CSS custom properties
700
701 src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js
707 -* should create markup for simple properties
708 -* should work with the id attribute
709 -* should create markup for boolean properties
710 -* should create markup for booleanish properties
711 -* should create markup for custom attributes
712 -* should create markup for numeric properties
713 -* should allow custom properties on web components
702 * should set values as properties by default
703 * should set values as attributes if necessary
704 * should set values as namespace attributes if necessary
@@ -728,7 +716,6 @@ src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js
716 * should remove attributes for normal properties
717 * should not remove attributes for special properties
718 * should not leave all options selected when deleting multiple
731 -* should support custom attributes
719
720 src/renderers/dom/shared/__tests__/ReactBrowserEventEmitter-test.js
721 * should store a listener correctly
@@ -895,11 +882,6 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
882 * renders a blank div with clean client render
883 * renders a blank div with client render on top of good server markup
884 * renders a blank div with client render on top of bad server markup
898 -* renders a div with inline styles with server string render
899 -* renders a div with inline styles with server stream render
900 -* renders a div with inline styles with clean client render
901 -* renders a div with inline styles with client render on top of good server markup
902 -* renders a div with inline styles with client render on top of bad server markup
885 * renders a self-closing tag with server string render
886 * renders a self-closing tag with server stream render
887 * renders a self-closing tag with clean client render
@@ -930,6 +912,11 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
912 * renders string prop with false value with clean client render
913 * renders string prop with false value with client render on top of good server markup
914 * renders string prop with false value with client render on top of bad server markup
915 +* renders no string prop with null value with server string render
916 +* renders no string prop with null value with server stream render
917 +* renders no string prop with null value with clean client render
918 +* renders no string prop with null value with client render on top of good server markup
919 +* renders no string prop with null value with client render on top of bad server markup
920 * renders boolean prop with true value with server string render
921 * renders boolean prop with true value with server stream render
922 * renders boolean prop with true value with clean client render
@@ -975,6 +962,11 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
962 * renders boolean prop with zero value with clean client render
963 * renders boolean prop with zero value with client render on top of good server markup
964 * renders boolean prop with zero value with client render on top of bad server markup
965 +* renders no boolean prop with null value with server string render
966 +* renders no boolean prop with null value with server stream render
967 +* renders no boolean prop with null value with clean client render
968 +* renders no boolean prop with null value with client render on top of good server markup
969 +* renders no boolean prop with null value with client render on top of bad server markup
970 * renders download prop with true value with server string render
971 * renders download prop with true value with server stream render
972 * renders download prop with true value with clean client render
@@ -990,11 +982,31 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
982 * renders download prop with string value with clean client render
983 * renders download prop with string value with client render on top of good server markup
984 * renders download prop with string value with client render on top of bad server markup
985 +* renders download prop with string "false" value with server string render
986 +* renders download prop with string "false" value with server stream render
987 +* renders download prop with string "false" value with clean client render
988 +* renders download prop with string "false" value with client render on top of good server markup
989 +* renders download prop with string "false" value with client render on top of bad server markup
990 * renders download prop with string "true" value with server string render
991 * renders download prop with string "true" value with server stream render
992 * renders download prop with string "true" value with clean client render
993 * renders download prop with string "true" value with client render on top of good server markup
994 * renders download prop with string "true" value with client render on top of bad server markup
995 +* renders download prop with number 0 value with server string render
996 +* renders download prop with number 0 value with server stream render
997 +* renders download prop with number 0 value with clean client render
998 +* renders download prop with number 0 value with client render on top of good server markup
999 +* renders download prop with number 0 value with client render on top of bad server markup
1000 +* renders no download prop with null value with server string render
1001 +* renders no download prop with null value with server stream render
1002 +* renders no download prop with null value with clean client render
1003 +* renders no download prop with null value with client render on top of good server markup
1004 +* renders no download prop with null value with client render on top of bad server markup
1005 +* renders no download prop with undefined value with server string render
1006 +* renders no download prop with undefined value with server stream render
1007 +* renders no download prop with undefined value with clean client render
1008 +* renders no download prop with undefined value with client render on top of good server markup
1009 +* renders no download prop with undefined value with client render on top of bad server markup
1010 * renders className prop with string value with server string render
1011 * renders className prop with string value with server stream render
1012 * renders className prop with string value with clean client render
@@ -1015,6 +1027,11 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
1027 * renders className prop with false value with clean client render
1028 * renders className prop with false value with client render on top of good server markup
1029 * renders className prop with false value with client render on top of bad server markup
1030 +* renders no className prop with null value with server string render
1031 +* renders no className prop with null value with server stream render
1032 +* renders no className prop with null value with clean client render
1033 +* renders no className prop with null value with client render on top of good server markup
1034 +* renders no className prop with null value with client render on top of bad server markup
1035 * renders htmlFor with string value with server string render
1036 * renders htmlFor with string value with server stream render
1037 * renders htmlFor with string value with clean client render
@@ -1025,16 +1042,36 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
1042 * renders htmlFor with an empty string with clean client render
1043 * renders htmlFor with an empty string with client render on top of good server markup
1044 * renders htmlFor with an empty string with client render on top of bad server markup
1028 -* renders className prop with true value with server string render
1029 -* renders className prop with true value with server stream render
1030 -* renders className prop with true value with clean client render
1031 -* renders className prop with true value with client render on top of good server markup
1032 -* renders className prop with true value with client render on top of bad server markup
1033 -* renders className prop with false value with server string render
1034 -* renders className prop with false value with server stream render
1035 -* renders className prop with false value with clean client render
1036 -* renders className prop with false value with client render on top of good server markup
1037 -* renders className prop with false value with client render on top of bad server markup
1045 +* renders htmlFor prop with true value with server string render
1046 +* renders htmlFor prop with true value with server stream render
1047 +* renders htmlFor prop with true value with clean client render
1048 +* renders htmlFor prop with true value with client render on top of good server markup
1049 +* renders htmlFor prop with true value with client render on top of bad server markup
1050 +* renders htmlFor prop with false value with server string render
1051 +* renders htmlFor prop with false value with server stream render
1052 +* renders htmlFor prop with false value with clean client render
1053 +* renders htmlFor prop with false value with client render on top of good server markup
1054 +* renders htmlFor prop with false value with client render on top of bad server markup
1055 +* renders no htmlFor prop with null value with server string render
1056 +* renders no htmlFor prop with null value with server stream render
1057 +* renders no htmlFor prop with null value with clean client render
1058 +* renders no htmlFor prop with null value with client render on top of good server markup
1059 +* renders no htmlFor prop with null value with client render on top of bad server markup
1060 +* renders positive numeric property with positive value with server string render
1061 +* renders positive numeric property with positive value with server stream render
1062 +* renders positive numeric property with positive value with clean client render
1063 +* renders positive numeric property with positive value with client render on top of good server markup
1064 +* renders positive numeric property with positive value with client render on top of bad server markup
1065 +* renders no positive numeric property with zero value with server string render
1066 +* renders no positive numeric property with zero value with server stream render
1067 +* renders no positive numeric property with zero value with clean client render
1068 +* renders no positive numeric property with zero value with client render on top of good server markup
1069 +* renders no positive numeric property with zero value with client render on top of bad server markup
1070 +* renders numeric property with zero value with server string render
1071 +* renders numeric property with zero value with server stream render
1072 +* renders numeric property with zero value with clean client render
1073 +* renders numeric property with zero value with client render on top of good server markup
1074 +* renders numeric property with zero value with client render on top of bad server markup
1075 * renders no ref attribute with server string render
1076 * renders no ref attribute with server stream render
1077 * renders no ref attribute with clean client render
@@ -1055,6 +1092,51 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
1092 * renders no dangerouslySetInnerHTML attribute with clean client render
1093 * renders no dangerouslySetInnerHTML attribute with client render on top of good server markup
1094 * renders no dangerouslySetInnerHTML attribute with client render on top of bad server markup
1095 +* renders simple styles with server string render
1096 +* renders simple styles with server stream render
1097 +* renders simple styles with clean client render
1098 +* renders simple styles with client render on top of good server markup
1099 +* renders simple styles with client render on top of bad server markup
1100 +* renders relevant styles with px with server string render
1101 +* renders relevant styles with px with server stream render
1102 +* renders relevant styles with px with clean client render
1103 +* renders relevant styles with px with client render on top of good server markup
1104 +* renders relevant styles with px with client render on top of bad server markup
1105 +* renders custom properties with server string render
1106 +* renders custom properties with server stream render
1107 +* renders custom properties with clean client render
1108 +* renders custom properties with client render on top of good server markup
1109 +* renders custom properties with client render on top of bad server markup
1110 +* renders no undefined styles with server string render
1111 +* renders no undefined styles with server stream render
1112 +* renders no undefined styles with clean client render
1113 +* renders no undefined styles with client render on top of good server markup
1114 +* renders no undefined styles with client render on top of bad server markup
1115 +* renders no null styles with server string render
1116 +* renders no null styles with server stream render
1117 +* renders no null styles with clean client render
1118 +* renders no null styles with client render on top of good server markup
1119 +* renders no null styles with client render on top of bad server markup
1120 +* renders no empty styles with server string render
1121 +* renders no empty styles with server stream render
1122 +* renders no empty styles with clean client render
1123 +* renders no empty styles with client render on top of good server markup
1124 +* renders no empty styles with client render on top of bad server markup
1125 +* renders simple strings with server string render
1126 +* renders simple strings with server stream render
1127 +* renders simple strings with clean client render
1128 +* renders simple strings with client render on top of good server markup
1129 +* renders simple strings with client render on top of bad server markup
1130 +* renders aria string prop with false value with server string render
1131 +* renders aria string prop with false value with server stream render
1132 +* renders aria string prop with false value with clean client render
1133 +* renders aria string prop with false value with client render on top of good server markup
1134 +* renders aria string prop with false value with client render on top of bad server markup
1135 +* renders no aria prop with null value with server string render
1136 +* renders no aria prop with null value with server stream render
1137 +* renders no aria prop with null value with clean client render
1138 +* renders no aria prop with null value with client render on top of good server markup
1139 +* renders no aria prop with null value with client render on top of bad server markup
1140 * renders no unknown attributes with server string render
1141 * renders no unknown attributes with server stream render
1142 * renders no unknown attributes with clean client render
@@ -1065,6 +1147,11 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
1147 * renders unknown data- attributes with clean client render
1148 * renders unknown data- attributes with client render on top of good server markup
1149 * renders unknown data- attributes with client render on top of bad server markup
1150 +* renders no unknown data- attributes with null value with server string render
1151 +* renders no unknown data- attributes with null value with server stream render
1152 +* renders no unknown data- attributes with null value with clean client render
1153 +* renders no unknown data- attributes with null value with client render on top of good server markup
1154 +* renders no unknown data- attributes with null value with client render on top of bad server markup
1155 * renders no unknown attributes for non-standard elements with server string render
1156 * renders no unknown attributes for non-standard elements with server stream render
1157 * renders no unknown attributes for non-standard elements with clean client render
@@ -1075,11 +1162,21 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
1162 * renders unknown attributes for custom elements with clean client render
1163 * renders unknown attributes for custom elements with client render on top of good server markup
1164 * renders unknown attributes for custom elements with client render on top of bad server markup
1165 +* renders no unknown attributes for custom elements with null value with server string render
1166 +* renders no unknown attributes for custom elements with null value with server stream render
1167 +* renders no unknown attributes for custom elements with null value with clean client render
1168 +* renders no unknown attributes for custom elements with null value with client render on top of good server markup
1169 +* renders no unknown attributes for custom elements with null value with client render on top of bad server markup
1170 * renders unknown attributes for custom elements using is with server string render
1171 * renders unknown attributes for custom elements using is with server stream render
1172 * renders unknown attributes for custom elements using is with clean client render
1173 * renders unknown attributes for custom elements using is with client render on top of good server markup
1174 * renders unknown attributes for custom elements using is with client render on top of bad server markup
1175 +* renders no unknown attributes for custom elements using is with null value with server string render
1176 +* renders no unknown attributes for custom elements using is with null value with server stream render
1177 +* renders no unknown attributes for custom elements using is with null value with clean client render
1178 +* renders no unknown attributes for custom elements using is with null value with client render on top of good server markup
1179 +* renders no unknown attributes for custom elements using is with null value with client render on top of bad server markup
1180 * renders no HTML events with server string render
1181 * renders no HTML events with server stream render
1182 * renders no HTML events with clean client render
@@ -1563,6 +1660,11 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
1660 * can distinguish an empty component from a dom node
1661 * can distinguish an empty component from an empty text component
1662 * should error reconnecting a div with different dangerouslySetInnerHTML
1663 +* renders injected attributes with server string render
1664 +* renders injected attributes with server stream render
1665 +* renders injected attributes with clean client render
1666 +* renders injected attributes with client render on top of good server markup
1667 +* renders injected attributes with client render on top of bad server markup
1668
1669 src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js
1670 * updates a mounted text component in place
scripts/rollup/results.json
+16 -16
@@ -65,20 +65,20 @@
65 "gzip": 81957
66 },
67 "react-dom-server.development.js (UMD_DEV)": {
68 - "size": 152929,
69 - "gzip": 37451
68 + "size": 119540,
69 + "gzip": 30394
70 },
71 "react-dom-server.production.min.js (UMD_PROD)": {
72 - "size": 25022,
73 - "gzip": 9357
72 + "size": 23004,
73 + "gzip": 8753
74 },
75 "react-dom-server.development.js (NODE_DEV)": {
76 - "size": 120074,
77 - "gzip": 29764
76 + "size": 88637,
77 + "gzip": 22952
78 },
79 "react-dom-server.production.min.js (NODE_PROD)": {
80 - "size": 22559,
81 - "gzip": 8401
80 + "size": 20744,
81 + "gzip": 7879
82 },
83 "ReactDOMServerStream-dev.js (FB_DEV)": {
84 "size": 264750,
@@ -181,20 +181,20 @@
181 "gzip": 50920
182 },
183 "ReactDOMServer-dev.js (FB_DEV)": {
184 - "size": 119528,
185 - "gzip": 29719
184 + "size": 88149,
185 + "gzip": 22908
186 },
187 "ReactDOMServer-prod.js (FB_PROD)": {
188 - "size": 60237,
189 - "gzip": 16296
188 + "size": 53879,
189 + "gzip": 14872
190 },
191 "react-dom-node-stream.development.js (NODE_DEV)": {
192 - "size": 121768,
193 - "gzip": 30291
192 + "size": 90331,
193 + "gzip": 23443
194 },
195 "react-dom-node-stream.production.min.js (NODE_PROD)": {
196 - "size": 23522,
197 - "gzip": 8727
196 + "size": 21682,
197 + "gzip": 8219
198 },
199 "ReactDOMNodeStream-dev.js (FB_DEV)": {
200 "size": 264918,
src/renderers/dom/shared/CSSPropertyOperations.js
+2 -185
@@ -14,21 +14,13 @@
14 var CSSProperty = require('CSSProperty');
15 var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
16
17 -var camelizeStyleName = require('fbjs/lib/camelizeStyleName');
17 var dangerousStyleValue = require('dangerousStyleValue');
19 -var getComponentName = require('getComponentName');
18 var hyphenateStyleName = require('fbjs/lib/hyphenateStyleName');
21 -var memoizeStringOnly = require('fbjs/lib/memoizeStringOnly');
22 -var warning = require('fbjs/lib/warning');
19
20 if (__DEV__) {
25 - var {getCurrentFiberOwnerName} = require('ReactDebugCurrentFiber');
21 + var warnValidStyle = require('warnValidStyle');
22 }
23
28 -var processStyleName = memoizeStringOnly(function(styleName) {
29 - return hyphenateStyleName(styleName);
30 -});
31 -
24 var hasShorthandPropertyBug = false;
25 if (ExecutionEnvironment.canUseDOM) {
26 var tempStyle = document.createElement('div').style;
@@ -40,190 +32,15 @@ if (ExecutionEnvironment.canUseDOM) {
32 }
33 }
34
43 -if (__DEV__) {
44 - // 'msTransform' is correct, but the other prefixes should be capitalized
45 - var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/;
46 -
47 - // style values shouldn't contain a semicolon
48 - var badStyleValueWithSemicolonPattern = /;\s*$/;
49 -
50 - var warnedStyleNames = {};
51 - var warnedStyleValues = {};
52 - var warnedForNaNValue = false;
53 - var warnedForInfinityValue = false;
54 -
55 - var warnHyphenatedStyleName = function(name, owner) {
56 - if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
57 - return;
58 - }
59 -
60 - warnedStyleNames[name] = true;
61 - warning(
62 - false,
63 - 'Unsupported style property %s. Did you mean %s?%s',
64 - name,
65 - camelizeStyleName(name),
66 - checkRenderMessage(owner),
67 - );
68 - };
69 -
70 - var warnBadVendoredStyleName = function(name, owner) {
71 - if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
72 - return;
73 - }
74 -
75 - warnedStyleNames[name] = true;
76 - warning(
77 - false,
78 - 'Unsupported vendor-prefixed style property %s. Did you mean %s?%s',
79 - name,
80 - name.charAt(0).toUpperCase() + name.slice(1),
81 - checkRenderMessage(owner),
82 - );
83 - };
84 -
85 - var warnStyleValueWithSemicolon = function(name, value, owner) {
86 - if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) {
87 - return;
88 - }
89 -
90 - warnedStyleValues[value] = true;
91 - warning(
92 - false,
93 - "Style property values shouldn't contain a semicolon.%s " +
94 - 'Try "%s: %s" instead.',
95 - checkRenderMessage(owner),
96 - name,
97 - value.replace(badStyleValueWithSemicolonPattern, ''),
98 - );
99 - };
100 -
101 - var warnStyleValueIsNaN = function(name, value, owner) {
102 - if (warnedForNaNValue) {
103 - return;
104 - }
105 -
106 - warnedForNaNValue = true;
107 - warning(
108 - false,
109 - '`NaN` is an invalid value for the `%s` css style property.%s',
110 - name,
111 - checkRenderMessage(owner),
112 - );
113 - };
114 -
115 - var warnStyleValueIsInfinity = function(name, value, owner) {
116 - if (warnedForInfinityValue) {
117 - return;
118 - }
119 -
120 - warnedForInfinityValue = true;
121 - warning(
122 - false,
123 - '`Infinity` is an invalid value for the `%s` css style property.%s',
124 - name,
125 - checkRenderMessage(owner),
126 - );
127 - };
128 -
129 - var checkRenderMessage = function(owner) {
130 - var ownerName;
131 - if (owner != null) {
132 - // Stack passes the owner manually all the way to CSSPropertyOperations.
133 - ownerName = getComponentName(owner);
134 - } else {
135 - // Fiber doesn't pass it but uses ReactDebugCurrentFiber to track it.
136 - // It is only enabled in development and tracks host components too.
137 - ownerName = getCurrentFiberOwnerName();
138 - // TODO: also report the stack.
139 - }
140 - if (ownerName) {
141 - return '\n\nCheck the render method of `' + ownerName + '`.';
142 - }
143 - return '';
144 - };
145 -
146 - /**
147 - * @param {string} name
148 - * @param {*} value
149 - * @param {ReactDOMComponent} component
150 - */
151 - var warnValidStyle = function(name, value, component) {
152 - var owner;
153 - if (component) {
154 - owner = component._currentElement._owner;
155 - }
156 - if (name.indexOf('-') > -1) {
157 - warnHyphenatedStyleName(name, owner);
158 - } else if (badVendoredStyleNamePattern.test(name)) {
159 - warnBadVendoredStyleName(name, owner);
160 - } else if (badStyleValueWithSemicolonPattern.test(value)) {
161 - warnStyleValueWithSemicolon(name, value, owner);
162 - }
163 -
164 - if (typeof value === 'number') {
165 - if (isNaN(value)) {
166 - warnStyleValueIsNaN(name, value, owner);
167 - } else if (!isFinite(value)) {
168 - warnStyleValueIsInfinity(name, value, owner);
169 - }
170 - }
171 - };
172 -}
173 -
35 /**
36 * Operations for dealing with CSS properties.
37 */
38 var CSSPropertyOperations = {
178 - /**
179 - * Serializes a mapping of style properties for use as inline styles:
180 - *
181 - * > createMarkupForStyles({width: '200px', height: 0})
182 - * "width:200px;height:0;"
183 - *
184 - * Undefined values are ignored so that declarative programming is easier.
185 - * The result should be HTML-escaped before insertion into the DOM.
186 - *
187 - * @param {object} styles
188 - * @param {ReactDOMComponent} component
189 - * @return {?string}
190 - */
191 - createMarkupForStyles: function(styles, component) {
192 - var serialized = '';
193 - var delimiter = '';
194 - for (var styleName in styles) {
195 - if (!styles.hasOwnProperty(styleName)) {
196 - continue;
197 - }
198 - var isCustomProperty = styleName.indexOf('--') === 0;
199 - var styleValue = styles[styleName];
200 - if (__DEV__) {
201 - if (!isCustomProperty) {
202 - warnValidStyle(styleName, styleValue, component);
203 - }
204 - }
205 - if (styleValue != null) {
206 - serialized += delimiter + processStyleName(styleName) + ':';
207 - serialized += dangerousStyleValue(
208 - styleName,
209 - styleValue,
210 - isCustomProperty,
211 - );
212 -
213 - delimiter = ';';
214 - }
215 - }
216 - return serialized || null;
217 - },
218 -
39 /**
40 * This creates a string that is expected to be equivalent to the style
41 * attribute generated by server-side rendering. It by-passes warnings and
42 * security checks so it's not safe to use this value for anything other than
223 - * comparison. It is only used in DEV for SSR validation. This is duplicated
224 - * from createMarkupForStyles because createMarkupForStyles is expected to
225 - * move out of the client-side renderer and it would be nice to make a clean
226 - * break.
43 + * comparison. It is only used in DEV for SSR validation.
44 */
45 createDangerousStringForStyles: function(styles) {
46 if (__DEV__) {
src/renderers/dom/shared/DOMMarkupOperations.js new
+125
@@ -0,0 +1,125 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule DOMMarkupOperations
10 + */
11 +
12 +'use strict';
13 +
14 +var DOMProperty = require('DOMProperty');
15 +
16 +var quoteAttributeValueForBrowser = require('quoteAttributeValueForBrowser');
17 +var warning = require('fbjs/lib/warning');
18 +
19 +// isAttributeNameSafe() is currently duplicated in DOMPropertyOperations.
20 +// TODO: Find a better place for this.
21 +var VALID_ATTRIBUTE_NAME_REGEX = new RegExp(
22 + '^[' +
23 + DOMProperty.ATTRIBUTE_NAME_START_CHAR +
24 + '][' +
25 + DOMProperty.ATTRIBUTE_NAME_CHAR +
26 + ']*$',
27 +);
28 +var illegalAttributeNameCache = {};
29 +var validatedAttributeNameCache = {};
30 +function isAttributeNameSafe(attributeName) {
31 + if (validatedAttributeNameCache.hasOwnProperty(attributeName)) {
32 + return true;
33 + }
34 + if (illegalAttributeNameCache.hasOwnProperty(attributeName)) {
35 + return false;
36 + }
37 + if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) {
38 + validatedAttributeNameCache[attributeName] = true;
39 + return true;
40 + }
41 + illegalAttributeNameCache[attributeName] = true;
42 + warning(false, 'Invalid attribute name: `%s`', attributeName);
43 + return false;
44 +}
45 +
46 +// shouldIgnoreValue() is currently duplicated in DOMPropertyOperations.
47 +// TODO: Find a better place for this.
48 +function shouldIgnoreValue(propertyInfo, value) {
49 + return (
50 + value == null ||
51 + (propertyInfo.hasBooleanValue && !value) ||
52 + (propertyInfo.hasNumericValue && isNaN(value)) ||
53 + (propertyInfo.hasPositiveNumericValue && value < 1) ||
54 + (propertyInfo.hasOverloadedBooleanValue && value === false)
55 + );
56 +}
57 +
58 +/**
59 + * Operations for dealing with DOM properties.
60 + */
61 +var DOMMarkupOperations = {
62 + /**
63 + * Creates markup for the ID property.
64 + *
65 + * @param {string} id Unescaped ID.
66 + * @return {string} Markup string.
67 + */
68 + createMarkupForID: function(id) {
69 + return (
70 + DOMProperty.ID_ATTRIBUTE_NAME + '=' + quoteAttributeValueForBrowser(id)
71 + );
72 + },
73 +
74 + createMarkupForRoot: function() {
75 + return DOMProperty.ROOT_ATTRIBUTE_NAME + '=""';
76 + },
77 +
78 + /**
79 + * Creates markup for a property.
80 + *
81 + * @param {string} name
82 + * @param {*} value
83 + * @return {?string} Markup string, or null if the property was invalid.
84 + */
85 + createMarkupForProperty: function(name, value) {
86 + var propertyInfo = DOMProperty.properties.hasOwnProperty(name)
87 + ? DOMProperty.properties[name]
88 + : null;
89 + if (propertyInfo) {
90 + if (shouldIgnoreValue(propertyInfo, value)) {
91 + return '';
92 + }
93 + var attributeName = propertyInfo.attributeName;
94 + if (
95 + propertyInfo.hasBooleanValue ||
96 + (propertyInfo.hasOverloadedBooleanValue && value === true)
97 + ) {
98 + return attributeName + '=""';
99 + }
100 + return attributeName + '=' + quoteAttributeValueForBrowser(value);
101 + } else if (DOMProperty.isCustomAttribute(name)) {
102 + if (value == null) {
103 + return '';
104 + }
105 + return name + '=' + quoteAttributeValueForBrowser(value);
106 + }
107 + return null;
108 + },
109 +
110 + /**
111 + * Creates markup for a custom property.
112 + *
113 + * @param {string} name
114 + * @param {*} value
115 + * @return {string} Markup string, or empty string if the property was invalid.
116 + */
117 + createMarkupForCustomAttribute: function(name, value) {
118 + if (!isAttributeNameSafe(name) || value == null) {
119 + return '';
120 + }
121 + return name + '=' + quoteAttributeValueForBrowser(value);
122 + },
123 +};
124 +
125 +module.exports = DOMMarkupOperations;
src/renderers/dom/shared/DOMPropertyOperations.js
+10 -64
@@ -15,9 +15,10 @@ var DOMProperty = require('DOMProperty');
15 var ReactDOMComponentTree = require('ReactDOMComponentTree');
16 var ReactInstrumentation = require('ReactInstrumentation');
17
18 -var quoteAttributeValueForBrowser = require('quoteAttributeValueForBrowser');
18 var warning = require('fbjs/lib/warning');
19
20 +// isAttributeNameSafe() is currently duplicated in DOMMarkupOperations.
21 +// TODO: Find a better place for this.
22 var VALID_ATTRIBUTE_NAME_REGEX = new RegExp(
23 '^[' +
24 DOMProperty.ATTRIBUTE_NAME_START_CHAR +
@@ -27,7 +28,6 @@ var VALID_ATTRIBUTE_NAME_REGEX = new RegExp(
28 );
29 var illegalAttributeNameCache = {};
30 var validatedAttributeNameCache = {};
30 -
31 function isAttributeNameSafe(attributeName) {
32 if (validatedAttributeNameCache.hasOwnProperty(attributeName)) {
33 return true;
@@ -44,6 +44,8 @@ function isAttributeNameSafe(attributeName) {
44 return false;
45 }
46
47 +// shouldIgnoreValue() is currently duplicated in DOMMarkupOperations.
48 +// TODO: Find a better place for this.
49 function shouldIgnoreValue(propertyInfo, value) {
50 return (
51 value == null ||
@@ -58,76 +60,14 @@ function shouldIgnoreValue(propertyInfo, value) {
60 * Operations for dealing with DOM properties.
61 */
62 var DOMPropertyOperations = {
61 - /**
62 - * Creates markup for the ID property.
63 - *
64 - * @param {string} id Unescaped ID.
65 - * @return {string} Markup string.
66 - */
67 - createMarkupForID: function(id) {
68 - return (
69 - DOMProperty.ID_ATTRIBUTE_NAME + '=' + quoteAttributeValueForBrowser(id)
70 - );
71 - },
72 -
63 setAttributeForID: function(node, id) {
64 node.setAttribute(DOMProperty.ID_ATTRIBUTE_NAME, id);
65 },
66
77 - createMarkupForRoot: function() {
78 - return DOMProperty.ROOT_ATTRIBUTE_NAME + '=""';
79 - },
80 -
67 setAttributeForRoot: function(node) {
68 node.setAttribute(DOMProperty.ROOT_ATTRIBUTE_NAME, '');
69 },
70
85 - /**
86 - * Creates markup for a property.
87 - *
88 - * @param {string} name
89 - * @param {*} value
90 - * @return {?string} Markup string, or null if the property was invalid.
91 - */
92 - createMarkupForProperty: function(name, value) {
93 - var propertyInfo = DOMProperty.properties.hasOwnProperty(name)
94 - ? DOMProperty.properties[name]
95 - : null;
96 - if (propertyInfo) {
97 - if (shouldIgnoreValue(propertyInfo, value)) {
98 - return '';
99 - }
100 - var attributeName = propertyInfo.attributeName;
101 - if (
102 - propertyInfo.hasBooleanValue ||
103 - (propertyInfo.hasOverloadedBooleanValue && value === true)
104 - ) {
105 - return attributeName + '=""';
106 - }
107 - return attributeName + '=' + quoteAttributeValueForBrowser(value);
108 - } else if (DOMProperty.isCustomAttribute(name)) {
109 - if (value == null) {
110 - return '';
111 - }
112 - return name + '=' + quoteAttributeValueForBrowser(value);
113 - }
114 - return null;
115 - },
116 -
117 - /**
118 - * Creates markup for a custom property.
119 - *
120 - * @param {string} name
121 - * @param {*} value
122 - * @return {string} Markup string, or empty string if the property was invalid.
123 - */
124 - createMarkupForCustomAttribute: function(name, value) {
125 - if (!isAttributeNameSafe(name) || value == null) {
126 - return '';
127 - }
128 - return name + '=' + quoteAttributeValueForBrowser(value);
129 - },
130 -
71 /**
72 * Get the value for a property on a node. Only used in DEV for SSR validation.
73 * The "expected" argument is used as a hint of what the expected value is.
@@ -153,6 +93,12 @@ var DOMPropertyOperations = {
93 if (value === '') {
94 return true;
95 }
96 + if (shouldIgnoreValue(propertyInfo, expected)) {
97 + return value;
98 + }
99 + if (value === '' + expected) {
100 + return expected;
101 + }
102 return value;
103 }
104 } else if (node.hasAttribute(attributeName)) {
src/renderers/dom/shared/__tests__/CSSPropertyOperations-test.js
+31 -81
@@ -16,97 +16,47 @@ var ReactDOM = require('react-dom');
16 var ReactDOMServer = require('react-dom/server');
17
18 describe('CSSPropertyOperations', () => {
19 - var CSSPropertyOperations;
20 -
21 - beforeEach(() => {
22 - jest.resetModules();
23 - CSSPropertyOperations = require('CSSPropertyOperations');
24 - });
25 -
26 - it('should create markup for simple styles', () => {
27 - expect(
28 - CSSPropertyOperations.createMarkupForStyles({
29 - backgroundColor: '#3b5998',
30 - display: 'none',
31 - }),
32 - ).toBe('background-color:#3b5998;display:none');
33 - });
34 -
35 - it('should ignore undefined styles', () => {
36 - expect(
37 - CSSPropertyOperations.createMarkupForStyles({
38 - backgroundColor: undefined,
39 - display: 'none',
40 - }),
41 - ).toBe('display:none');
42 - });
43 -
44 - it('should ignore null styles', () => {
45 - expect(
46 - CSSPropertyOperations.createMarkupForStyles({
47 - backgroundColor: null,
48 - display: 'none',
49 - }),
50 - ).toBe('display:none');
51 - });
52 -
53 - it('should return null for no styles', () => {
54 - expect(
55 - CSSPropertyOperations.createMarkupForStyles({
56 - backgroundColor: null,
57 - display: null,
58 - }),
59 - ).toBe(null);
60 - });
61 -
19 it('should automatically append `px` to relevant styles', () => {
63 - expect(
64 - CSSPropertyOperations.createMarkupForStyles({
65 - left: 0,
66 - margin: 16,
67 - opacity: 0.5,
68 - padding: '4px',
69 - }),
70 - ).toBe('left:0;margin:16px;opacity:0.5;padding:4px');
20 + var styles = {
21 + left: 0,
22 + margin: 16,
23 + opacity: 0.5,
24 + padding: '4px',
25 + };
26 + var div = <div style={styles} />;
27 + var html = ReactDOMServer.renderToString(div);
28 + expect(html).toContain('"left:0;margin:16px;opacity:0.5;padding:4px"');
29 });
30
31 it('should trim values', () => {
74 - expect(
75 - CSSPropertyOperations.createMarkupForStyles({
76 - left: '16 ',
77 - opacity: 0.5,
78 - right: ' 4 ',
79 - }),
80 - ).toBe('left:16;opacity:0.5;right:4');
32 + var styles = {
33 + left: '16 ',
34 + opacity: 0.5,
35 + right: ' 4 ',
36 + };
37 + var div = <div style={styles} />;
38 + var html = ReactDOMServer.renderToString(div);
39 + expect(html).toContain('"left:16;opacity:0.5;right:4"');
40 });
41
42 it('should not append `px` to styles that might need a number', () => {
84 - var CSSProperty = require('CSSProperty');
85 - var unitlessProperties = Object.keys(CSSProperty.isUnitlessNumber);
86 - unitlessProperties.forEach(function(property) {
87 - var styles = {};
88 - styles[property] = 1;
89 - expect(CSSPropertyOperations.createMarkupForStyles(styles)).toMatch(
90 - /:1$/,
91 - );
92 - });
43 + var styles = {
44 + flex: 0,
45 + opacity: 0.5,
46 + };
47 + var div = <div style={styles} />;
48 + var html = ReactDOMServer.renderToString(div);
49 + expect(html).toContain('"flex:0;opacity:0.5"');
50 });
51
52 it('should create vendor-prefixed markup correctly', () => {
96 - expect(
97 - CSSPropertyOperations.createMarkupForStyles({
98 - msTransition: 'none',
99 - MozTransition: 'none',
100 - }),
101 - ).toBe('-ms-transition:none;-moz-transition:none');
102 - });
103 -
104 - it('should create markup with unitless css custom property', () => {
105 - expect(
106 - CSSPropertyOperations.createMarkupForStyles({
107 - '--foo': 5,
108 - }),
109 - ).toBe('--foo:5');
53 + var styles = {
54 + msTransition: 'none',
55 + MozTransition: 'none',
56 + };
57 + var div = <div style={styles} />;
58 + var html = ReactDOMServer.renderToString(div);
59 + expect(html).toContain('"-ms-transition:none;-moz-transition:none"');
60 });
61
62 it('should set style attribute when styles exist', () => {
src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js
-163
@@ -26,122 +26,6 @@ describe('DOMPropertyOperations', () => {
26 ReactDOMComponentTree = require('ReactDOMComponentTree');
27 });
28
29 - describe('createMarkupForProperty', () => {
30 - it('should create markup for simple properties', () => {
31 - expect(
32 - DOMPropertyOperations.createMarkupForProperty('name', 'simple'),
33 - ).toBe('name="simple"');
34 -
35 - expect(DOMPropertyOperations.createMarkupForProperty('name', false)).toBe(
36 - 'name="false"',
37 - );
38 -
39 - expect(DOMPropertyOperations.createMarkupForProperty('name', null)).toBe(
40 - '',
41 - );
42 - });
43 -
44 - it('should work with the id attribute', () => {
45 - expect(
46 - DOMPropertyOperations.createMarkupForProperty('id', 'simple'),
47 - ).toBe('id="simple"');
48 - });
49 -
50 - it('should create markup for boolean properties', () => {
51 - expect(
52 - DOMPropertyOperations.createMarkupForProperty('checked', 'simple'),
53 - ).toBe('checked=""');
54 -
55 - expect(
56 - DOMPropertyOperations.createMarkupForProperty('checked', true),
57 - ).toBe('checked=""');
58 -
59 - expect(
60 - DOMPropertyOperations.createMarkupForProperty('checked', false),
61 - ).toBe('');
62 -
63 - expect(
64 - DOMPropertyOperations.createMarkupForProperty('scoped', true),
65 - ).toBe('scoped=""');
66 - });
67 -
68 - it('should create markup for booleanish properties', () => {
69 - expect(
70 - DOMPropertyOperations.createMarkupForProperty('download', 'simple'),
71 - ).toBe('download="simple"');
72 -
73 - expect(
74 - DOMPropertyOperations.createMarkupForProperty('download', true),
75 - ).toBe('download=""');
76 -
77 - expect(
78 - DOMPropertyOperations.createMarkupForProperty('download', 'true'),
79 - ).toBe('download="true"');
80 -
81 - expect(
82 - DOMPropertyOperations.createMarkupForProperty('download', false),
83 - ).toBe('');
84 -
85 - expect(
86 - DOMPropertyOperations.createMarkupForProperty('download', 'false'),
87 - ).toBe('download="false"');
88 -
89 - expect(
90 - DOMPropertyOperations.createMarkupForProperty('download', undefined),
91 - ).toBe('');
92 -
93 - expect(
94 - DOMPropertyOperations.createMarkupForProperty('download', null),
95 - ).toBe('');
96 -
97 - expect(DOMPropertyOperations.createMarkupForProperty('download', 0)).toBe(
98 - 'download="0"',
99 - );
100 - });
101 -
102 - it('should create markup for custom attributes', () => {
103 - expect(
104 - DOMPropertyOperations.createMarkupForProperty('aria-label', 'simple'),
105 - ).toBe('aria-label="simple"');
106 -
107 - expect(
108 - DOMPropertyOperations.createMarkupForProperty('aria-label', false),
109 - ).toBe('aria-label="false"');
110 -
111 - expect(
112 - DOMPropertyOperations.createMarkupForProperty('aria-label', null),
113 - ).toBe('');
114 - });
115 -
116 - it('should create markup for numeric properties', () => {
117 - expect(DOMPropertyOperations.createMarkupForProperty('start', 5)).toBe(
118 - 'start="5"',
119 - );
120 -
121 - expect(DOMPropertyOperations.createMarkupForProperty('start', 0)).toBe(
122 - 'start="0"',
123 - );
124 -
125 - expect(DOMPropertyOperations.createMarkupForProperty('size', 0)).toBe('');
126 -
127 - expect(DOMPropertyOperations.createMarkupForProperty('size', 1)).toBe(
128 - 'size="1"',
129 - );
130 - });
131 - });
132 -
133 - describe('createMarkupForProperty', () => {
134 - it('should allow custom properties on web components', () => {
135 - expect(
136 - DOMPropertyOperations.createMarkupForCustomAttribute('awesomeness', 5),
137 - ).toBe('awesomeness="5"');
138 -
139 - expect(
140 - DOMPropertyOperations.createMarkupForCustomAttribute('dev', 'jim'),
141 - ).toBe('dev="jim"');
142 - });
143 - });
144 -
29 describe('setValueForProperty', () => {
30 var stubNode;
31 var stubInstance;
@@ -377,51 +261,4 @@ describe('DOMPropertyOperations', () => {
261 );
262 });
263 });
380 -
381 - describe('injectDOMPropertyConfig', () => {
382 - it('should support custom attributes', () => {
383 - // foobar does not exist yet
384 - expect(
385 - DOMPropertyOperations.createMarkupForProperty('foobar', 'simple'),
386 - ).toBe(null);
387 -
388 - // foo-* does not exist yet
389 - expect(
390 - DOMPropertyOperations.createMarkupForProperty('foo-xyz', 'simple'),
391 - ).toBe(null);
392 -
393 - // inject foobar DOM property
394 - DOMProperty.injection.injectDOMPropertyConfig({
395 - isCustomAttribute: function(name) {
396 - return name.indexOf('foo-') === 0;
397 - },
398 - Properties: {foobar: null},
399 - });
400 -
401 - // Ensure old attributes still work
402 - expect(
403 - DOMPropertyOperations.createMarkupForProperty('name', 'simple'),
404 - ).toBe('name="simple"');
405 - expect(
406 - DOMPropertyOperations.createMarkupForProperty('data-name', 'simple'),
407 - ).toBe('data-name="simple"');
408 -
409 - // foobar should work
410 - expect(
411 - DOMPropertyOperations.createMarkupForProperty('foobar', 'simple'),
412 - ).toBe('foobar="simple"');
413 -
414 - // foo-* should work
415 - expect(
416 - DOMPropertyOperations.createMarkupForProperty('foo-xyz', 'simple'),
417 - ).toBe('foo-xyz="simple"');
418 -
419 - // It should complain about double injections.
420 - expect(function() {
421 - DOMProperty.injection.injectDOMPropertyConfig({
422 - Properties: {foobar: null},
423 - });
424 - }).toThrow();
425 - });
426 - });
264 });
src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
+194 -8
@@ -285,6 +285,7 @@ function expectMarkupMismatch(serverElement, clientElement) {
285 // error, you want to see the error show up both on server and client. Unfortunately,
286 // React refuses to issue the same error twice to avoid clogging up the console.
287 // To get around this, we must reload React modules in between server and client render.
288 +let onAfterResetModules = null;
289 function resetModules() {
290 jest.resetModuleRegistry();
291 PropTypes = require('prop-types');
@@ -296,6 +297,12 @@ function resetModules() {
297 ReactTestUtils = require('react-dom/test-utils');
298 // TODO: can we express this test with only public API?
299 ExecutionEnvironment = require('ExecutionEnvironment');
300 +
301 + // TODO: this is a hack for testing dynamic injection. Remove this when we decide
302 + // how to do static injection instead.
303 + if (typeof onAfterResetModules === 'function') {
304 + onAfterResetModules();
305 + }
306 }
307
308 describe('ReactDOMServerIntegration', () => {
@@ -311,12 +318,6 @@ describe('ReactDOMServerIntegration', () => {
318 expect(e.tagName).toBe('DIV');
319 });
320
314 - itRenders('a div with inline styles', async render => {
315 - const e = await render(<div style={{color: 'red', width: '30px'}} />);
316 - expect(e.style.color).toBe('red');
317 - expect(e.style.width).toBe('30px');
318 - });
319 -
321 itRenders('a self-closing tag', async render => {
322 const e = await render(<br />);
323 expect(e.tagName).toBe('BR');
@@ -352,6 +353,11 @@ describe('ReactDOMServerIntegration', () => {
353 const e = await render(<a href={false} />);
354 expect(e.getAttribute('href')).toBe('false');
355 });
356 +
357 + itRenders('no string prop with null value', async render => {
358 + const e = await render(<div width={null} />);
359 + expect(e.hasAttribute('width')).toBe(false);
360 + });
361 });
362
363 describe('boolean properties', function() {
@@ -407,6 +413,11 @@ describe('ReactDOMServerIntegration', () => {
413 const e = await render(<div hidden={0} />);
414 expect(e.getAttribute('hidden')).toBe(null);
415 });
416 +
417 + itRenders('no boolean prop with null value', async render => {
418 + const e = await render(<div hidden={null} />);
419 + expect(e.hasAttribute('hidden')).toBe(false);
420 + });
421 });
422
423 describe('download property (combined boolean/string attribute)', function() {
@@ -425,10 +436,30 @@ describe('ReactDOMServerIntegration', () => {
436 expect(e.getAttribute('download')).toBe('myfile');
437 });
438
439 + itRenders('download prop with string "false" value', async render => {
440 + const e = await render(<a download="false" />);
441 + expect(e.getAttribute('download')).toBe('false');
442 + });
443 +
444 itRenders('download prop with string "true" value', async render => {
445 const e = await render(<a download={'true'} />);
446 expect(e.getAttribute('download')).toBe('true');
447 });
448 +
449 + itRenders('download prop with number 0 value', async render => {
450 + const e = await render(<a download={0} />);
451 + expect(e.getAttribute('download')).toBe('0');
452 + });
453 +
454 + itRenders('no download prop with null value', async render => {
455 + const e = await render(<div download={null} />);
456 + expect(e.hasAttribute('download')).toBe(false);
457 + });
458 +
459 + itRenders('no download prop with undefined value', async render => {
460 + const e = await render(<div download={undefined} />);
461 + expect(e.hasAttribute('download')).toBe(false);
462 + });
463 });
464
465 describe('className property', function() {
@@ -453,6 +484,11 @@ describe('ReactDOMServerIntegration', () => {
484 const e = await render(<div className={false} />);
485 expect(e.getAttribute('class')).toBe('false');
486 });
487 +
488 + itRenders('no className prop with null value', async render => {
489 + const e = await render(<div className={null} />);
490 + expect(e.hasAttribute('className')).toBe(false);
491 + });
492 });
493
494 describe('htmlFor property', function() {
@@ -467,16 +503,44 @@ describe('ReactDOMServerIntegration', () => {
503 });
504
505 // this probably is just masking programmer error, but it is existing behavior.
470 - itRenders('className prop with true value', async render => {
506 + itRenders('htmlFor prop with true value', async render => {
507 const e = await render(<div htmlFor={true} />);
508 expect(e.getAttribute('for')).toBe('true');
509 });
510
511 // this probably is just masking programmer error, but it is existing behavior.
476 - itRenders('className prop with false value', async render => {
512 + itRenders('htmlFor prop with false value', async render => {
513 const e = await render(<div htmlFor={false} />);
514 expect(e.getAttribute('for')).toBe('false');
515 });
516 +
517 + itRenders('no htmlFor prop with null value', async render => {
518 + const e = await render(<div htmlFor={null} />);
519 + expect(e.hasAttribute('htmlFor')).toBe(false);
520 + });
521 + });
522 +
523 + describe('numeric properties', function() {
524 + itRenders(
525 + 'positive numeric property with positive value',
526 + async render => {
527 + const e = await render(<input size={2} />);
528 + expect(e.getAttribute('size')).toBe('2');
529 + },
530 + );
531 +
532 + itRenders(
533 + 'no positive numeric property with zero value',
534 + async render => {
535 + const e = await render(<input size={0} />);
536 + expect(e.hasAttribute('size')).toBe(false);
537 + },
538 + );
539 +
540 + itRenders('numeric property with zero value', async render => {
541 + const e = await render(<ol start={0} />);
542 + expect(e.getAttribute('start')).toBe('0');
543 + });
544 });
545
546 describe('props with special meaning in React', function() {
@@ -508,6 +572,77 @@ describe('ReactDOMServerIntegration', () => {
572 });
573 });
574
575 + describe('inline styles', function() {
576 + itRenders('simple styles', async render => {
577 + const e = await render(<div style={{color: 'red', width: '30px'}} />);
578 + expect(e.style.color).toBe('red');
579 + expect(e.style.width).toBe('30px');
580 + });
581 +
582 + itRenders('relevant styles with px', async render => {
583 + const e = await render(
584 + <div
585 + style={{
586 + left: 0,
587 + margin: 16,
588 + opacity: 0.5,
589 + padding: '4px',
590 + }}
591 + />,
592 + );
593 + expect(e.style.left).toBe('0px');
594 + expect(e.style.margin).toBe('16px');
595 + expect(e.style.opacity).toBe('0.5');
596 + expect(e.style.padding).toBe('4px');
597 + });
598 +
599 + itRenders('custom properties', async render => {
600 + const e = await render(<div style={{'--foo': 5}} />);
601 + // This seems like an odd way computed properties are exposed in jsdom.
602 + // In a real browser we'd read it with e.style.getPropertyValue('--foo')
603 + expect(e.style.Foo).toBe('5');
604 + });
605 +
606 + itRenders('no undefined styles', async render => {
607 + const e = await render(
608 + <div style={{color: undefined, width: '30px'}} />,
609 + );
610 + expect(e.style.color).toBe('');
611 + expect(e.style.width).toBe('30px');
612 + });
613 +
614 + itRenders('no null styles', async render => {
615 + const e = await render(<div style={{color: null, width: '30px'}} />);
616 + expect(e.style.color).toBe('');
617 + expect(e.style.width).toBe('30px');
618 + });
619 +
620 + itRenders('no empty styles', async render => {
621 + const e = await render(<div style={{color: null, width: null}} />);
622 + expect(e.style.color).toBe('');
623 + expect(e.style.width).toBe('');
624 + expect(e.hasAttribute('style')).toBe(false);
625 + });
626 + });
627 +
628 + describe('aria attributes', function() {
629 + itRenders('simple strings', async render => {
630 + const e = await render(<div aria-label="hello" />);
631 + expect(e.getAttribute('aria-label')).toBe('hello');
632 + });
633 +
634 + // this probably is just masking programmer error, but it is existing behavior.
635 + itRenders('aria string prop with false value', async render => {
636 + const e = await render(<div aria-label={false} />);
637 + expect(e.getAttribute('aria-label')).toBe('false');
638 + });
639 +
640 + itRenders('no aria prop with null value', async render => {
641 + const e = await render(<div aria-label={null} />);
642 + expect(e.hasAttribute('aria-label')).toBe(false);
643 + });
644 + });
645 +
646 describe('unknown attributes', function() {
647 itRenders('no unknown attributes', async render => {
648 const e = await render(<div foo="bar" />, 1);
@@ -519,6 +654,11 @@ describe('ReactDOMServerIntegration', () => {
654 expect(e.getAttribute('data-foo')).toBe('bar');
655 });
656
657 + itRenders('no unknown data- attributes with null value', async render => {
658 + const e = await render(<div data-foo={null} />);
659 + expect(e.hasAttribute('data-foo')).toBe(false);
660 + });
661 +
662 itRenders(
663 'no unknown attributes for non-standard elements',
664 async render => {
@@ -532,6 +672,14 @@ describe('ReactDOMServerIntegration', () => {
672 expect(e.getAttribute('foo')).toBe('bar');
673 });
674
675 + itRenders(
676 + 'no unknown attributes for custom elements with null value',
677 + async render => {
678 + const e = await render(<custom-element foo={null} />);
679 + expect(e.hasAttribute('foo')).toBe(false);
680 + },
681 + );
682 +
683 itRenders(
684 'unknown attributes for custom elements using is',
685 async render => {
@@ -539,6 +687,14 @@ describe('ReactDOMServerIntegration', () => {
687 expect(e.getAttribute('foo')).toBe('bar');
688 },
689 );
690 +
691 + itRenders(
692 + 'no unknown attributes for custom elements using is with null value',
693 + async render => {
694 + const e = await render(<div is="custom-element" foo={null} />);
695 + expect(e.hasAttribute('foo')).toBe(false);
696 + },
697 + );
698 });
699
700 itRenders('no HTML events', async render => {
@@ -2195,4 +2351,34 @@ describe('ReactDOMServerIntegration', () => {
2351 <div dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}} />,
2352 ));
2353 });
2354 +
2355 + describe('dynamic injection', () => {
2356 + beforeEach(() => {
2357 + // HACK: we reset modules several times during the test which breaks
2358 + // dynamic injection. So we resort to telling resetModules() to run
2359 + // our custom init code every time after resetting. We could have a nicer
2360 + // way to do this, but this is the only test that needs it, and it will
2361 + // be removed anyway when we switch to static injection.
2362 + onAfterResetModules = () => {
2363 + const DOMProperty = require('DOMProperty');
2364 + DOMProperty.injection.injectDOMPropertyConfig({
2365 + isCustomAttribute: function(name) {
2366 + return name.indexOf('foo-') === 0;
2367 + },
2368 + Properties: {foobar: null},
2369 + });
2370 + };
2371 + resetModules();
2372 + });
2373 +
2374 + afterEach(() => {
2375 + onAfterResetModules = null;
2376 + });
2377 +
2378 + itRenders('injected attributes', async render => {
2379 + const e = await render(<div foobar="simple" foo-xyz="simple" />, 0);
2380 + expect(e.getAttribute('foobar')).toBe('simple');
2381 + expect(e.getAttribute('foo-xyz')).toBe('simple');
2382 + });
2383 + });
2384 });
src/renderers/dom/shared/warnValidStyle.js new
+150
@@ -0,0 +1,150 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule warnValidStyle
10 + */
11 +
12 +'use strict';
13 +
14 +var emptyFunction = require('fbjs/lib/emptyFunction');
15 +
16 +var warnValidStyle = emptyFunction;
17 +
18 +if (__DEV__) {
19 + var camelizeStyleName = require('fbjs/lib/camelizeStyleName');
20 + var getComponentName = require('getComponentName');
21 + var warning = require('fbjs/lib/warning');
22 + var {getCurrentFiberOwnerName} = require('ReactDebugCurrentFiber');
23 +
24 + // 'msTransform' is correct, but the other prefixes should be capitalized
25 + var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/;
26 +
27 + // style values shouldn't contain a semicolon
28 + var badStyleValueWithSemicolonPattern = /;\s*$/;
29 +
30 + var warnedStyleNames = {};
31 + var warnedStyleValues = {};
32 + var warnedForNaNValue = false;
33 + var warnedForInfinityValue = false;
34 +
35 + var warnHyphenatedStyleName = function(name, owner) {
36 + if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
37 + return;
38 + }
39 +
40 + warnedStyleNames[name] = true;
41 + warning(
42 + false,
43 + 'Unsupported style property %s. Did you mean %s?%s',
44 + name,
45 + camelizeStyleName(name),
46 + checkRenderMessage(owner),
47 + );
48 + };
49 +
50 + var warnBadVendoredStyleName = function(name, owner) {
51 + if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {
52 + return;
53 + }
54 +
55 + warnedStyleNames[name] = true;
56 + warning(
57 + false,
58 + 'Unsupported vendor-prefixed style property %s. Did you mean %s?%s',
59 + name,
60 + name.charAt(0).toUpperCase() + name.slice(1),
61 + checkRenderMessage(owner),
62 + );
63 + };
64 +
65 + var warnStyleValueWithSemicolon = function(name, value, owner) {
66 + if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) {
67 + return;
68 + }
69 +
70 + warnedStyleValues[value] = true;
71 + warning(
72 + false,
73 + "Style property values shouldn't contain a semicolon.%s " +
74 + 'Try "%s: %s" instead.',
75 + checkRenderMessage(owner),
76 + name,
77 + value.replace(badStyleValueWithSemicolonPattern, ''),
78 + );
79 + };
80 +
81 + var warnStyleValueIsNaN = function(name, value, owner) {
82 + if (warnedForNaNValue) {
83 + return;
84 + }
85 +
86 + warnedForNaNValue = true;
87 + warning(
88 + false,
89 + '`NaN` is an invalid value for the `%s` css style property.%s',
90 + name,
91 + checkRenderMessage(owner),
92 + );
93 + };
94 +
95 + var warnStyleValueIsInfinity = function(name, value, owner) {
96 + if (warnedForInfinityValue) {
97 + return;
98 + }
99 +
100 + warnedForInfinityValue = true;
101 + warning(
102 + false,
103 + '`Infinity` is an invalid value for the `%s` css style property.%s',
104 + name,
105 + checkRenderMessage(owner),
106 + );
107 + };
108 +
109 + var checkRenderMessage = function(owner) {
110 + var ownerName;
111 + if (owner != null) {
112 + // Stack passes the owner manually all the way to CSSPropertyOperations.
113 + ownerName = getComponentName(owner);
114 + } else {
115 + // Fiber doesn't pass it but uses ReactDebugCurrentFiber to track it.
116 + // It is only enabled in development and tracks host components too.
117 + ownerName = getCurrentFiberOwnerName();
118 + // TODO: also report the stack.
119 + }
120 + if (ownerName) {
121 + return '\n\nCheck the render method of `' + ownerName + '`.';
122 + }
123 + return '';
124 + };
125 +
126 + warnValidStyle = function(name, value, component) {
127 + var owner;
128 + if (component) {
129 + // TODO: this only works with Stack. Seems like we need to add unit tests?
130 + owner = component._currentElement._owner;
131 + }
132 + if (name.indexOf('-') > -1) {
133 + warnHyphenatedStyleName(name, owner);
134 + } else if (badVendoredStyleNamePattern.test(name)) {
135 + warnBadVendoredStyleName(name, owner);
136 + } else if (badStyleValueWithSemicolonPattern.test(value)) {
137 + warnStyleValueWithSemicolon(name, value, owner);
138 + }
139 +
140 + if (typeof value === 'number') {
141 + if (isNaN(value)) {
142 + warnStyleValueIsNaN(name, value, owner);
143 + } else if (!isFinite(value)) {
144 + warnStyleValueIsInfinity(name, value, owner);
145 + }
146 + }
147 + };
148 +}
149 +
150 +module.exports = warnValidStyle;
src/renderers/dom/stack/client/ReactDOMComponent.js
+42 -8
@@ -15,6 +15,7 @@ var AutoFocusUtils = require('AutoFocusUtils');
15 var CSSPropertyOperations = require('CSSPropertyOperations');
16 var DOMLazyTree = require('DOMLazyTree');
17 var DOMNamespaces = require('DOMNamespaces');
18 +var DOMMarkupOperations = require('DOMMarkupOperations');
19 var DOMProperty = require('DOMProperty');
20 var DOMPropertyOperations = require('DOMPropertyOperations');
21 var EventPluginRegistry = require('EventPluginRegistry');
@@ -30,15 +31,19 @@ var ReactMultiChild = require('ReactMultiChild');
31 var ReactServerRenderingTransaction = require('ReactServerRenderingTransaction');
32 var {DOCUMENT_FRAGMENT_NODE} = require('HTMLNodeType');
33
34 +var dangerousStyleValue = require('dangerousStyleValue');
35 var emptyFunction = require('fbjs/lib/emptyFunction');
36 var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
37 +var hyphenateStyleName = require('fbjs/lib/hyphenateStyleName');
38 var inputValueTracking = require('inputValueTracking');
39 var invariant = require('fbjs/lib/invariant');
40 var isCustomComponent = require('isCustomComponent');
41 +var memoizeStringOnly = require('fbjs/lib/memoizeStringOnly');
42 var omittedCloseTags = require('omittedCloseTags');
43 var validateDOMNesting = require('validateDOMNesting');
44 var voidElementTags = require('voidElementTags');
45 var warning = require('fbjs/lib/warning');
46 +var warnValidStyle = require('warnValidStyle');
47
48 var didWarnShadyDOM = false;
49
@@ -162,6 +167,38 @@ function optionPostMount() {
167 ReactDOMOption.postMountWrapper(inst);
168 }
169
170 +var processStyleName = memoizeStringOnly(function(styleName) {
171 + return hyphenateStyleName(styleName);
172 +});
173 +
174 +function createMarkupForStyles(styles, component) {
175 + var serialized = '';
176 + var delimiter = '';
177 + for (var styleName in styles) {
178 + if (!styles.hasOwnProperty(styleName)) {
179 + continue;
180 + }
181 + var isCustomProperty = styleName.indexOf('--') === 0;
182 + var styleValue = styles[styleName];
183 + if (__DEV__) {
184 + if (!isCustomProperty) {
185 + warnValidStyle(styleName, styleValue, component);
186 + }
187 + }
188 + if (styleValue != null) {
189 + serialized += delimiter + processStyleName(styleName) + ':';
190 + serialized += dangerousStyleValue(
191 + styleName,
192 + styleValue,
193 + isCustomProperty,
194 + );
195 +
196 + delimiter = ';';
197 + }
198 + }
199 + return serialized || null;
200 +}
201 +
202 var setAndValidateContentChildDev = emptyFunction;
203 if (__DEV__) {
204 setAndValidateContentChildDev = function(content) {
@@ -656,21 +693,18 @@ ReactDOMComponent.Mixin = {
693 Object.freeze(propValue);
694 }
695 }
659 - propValue = CSSPropertyOperations.createMarkupForStyles(
660 - propValue,
661 - this,
662 - );
696 + propValue = createMarkupForStyles(propValue, this);
697 }
698 var markup = null;
699 if (this._tag != null && isCustomComponent(this._tag, props)) {
700 if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
667 - markup = DOMPropertyOperations.createMarkupForCustomAttribute(
701 + markup = DOMMarkupOperations.createMarkupForCustomAttribute(
702 propKey,
703 propValue,
704 );
705 }
706 } else {
673 - markup = DOMPropertyOperations.createMarkupForProperty(
707 + markup = DOMMarkupOperations.createMarkupForProperty(
708 propKey,
709 propValue,
710 );
@@ -688,9 +722,9 @@ ReactDOMComponent.Mixin = {
722 }
723
724 if (!this._hostParent) {
691 - ret += ' ' + DOMPropertyOperations.createMarkupForRoot();
725 + ret += ' ' + DOMMarkupOperations.createMarkupForRoot();
726 }
693 - ret += ' ' + DOMPropertyOperations.createMarkupForID(this._domID);
727 + ret += ' ' + DOMMarkupOperations.createMarkupForID(this._domID);
728 return ret;
729 },
730
src/renderers/shared/server/ReactPartialRenderer.js
+42 -10
@@ -11,23 +11,26 @@
11
12 'use strict';
13
14 -var CSSPropertyOperations = require('CSSPropertyOperations');
15 -var DOMPropertyOperations = require('DOMPropertyOperations');
14 +var DOMMarkupOperations = require('DOMMarkupOperations');
15 var {registrationNameModules} = require('EventPluginRegistry');
16 var React = require('react');
17 var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes');
18
19 var assertValidProps = require('assertValidProps');
20 var checkPropTypes = require('prop-types/checkPropTypes');
21 +var dangerousStyleValue = require('dangerousStyleValue');
22 var emptyObject = require('fbjs/lib/emptyObject');
23 var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
24 +var hyphenateStyleName = require('fbjs/lib/hyphenateStyleName');
25 var invariant = require('fbjs/lib/invariant');
26 +var memoizeStringOnly = require('fbjs/lib/memoizeStringOnly');
27 var omittedCloseTags = require('omittedCloseTags');
28 var warning = require('fbjs/lib/warning');
29
30 var toArray = React.Children.toArray;
31
32 if (__DEV__) {
33 + var warnValidStyle = require('warnValidStyle');
34 var {
35 validateProperties: validateARIAProperties,
36 } = require('ReactDOMInvalidARIAHook');
@@ -111,6 +114,38 @@ function validateDangerousTag(tag) {
114 }
115 }
116
117 +var processStyleName = memoizeStringOnly(function(styleName) {
118 + return hyphenateStyleName(styleName);
119 +});
120 +
121 +function createMarkupForStyles(styles, component) {
122 + var serialized = '';
123 + var delimiter = '';
124 + for (var styleName in styles) {
125 + if (!styles.hasOwnProperty(styleName)) {
126 + continue;
127 + }
128 + var isCustomProperty = styleName.indexOf('--') === 0;
129 + var styleValue = styles[styleName];
130 + if (__DEV__) {
131 + if (!isCustomProperty) {
132 + warnValidStyle(styleName, styleValue, component);
133 + }
134 + }
135 + if (styleValue != null) {
136 + serialized += delimiter + processStyleName(styleName) + ':';
137 + serialized += dangerousStyleValue(
138 + styleName,
139 + styleValue,
140 + isCustomProperty,
141 + );
142 +
143 + delimiter = ';';
144 + }
145 + }
146 + return serialized || null;
147 +}
148 +
149 function warnNoop(
150 publicInstance: ReactComponent<any, any, any>,
151 callerName: string,
@@ -233,21 +268,18 @@ function createOpenTagMarkup(
268 }
269 if (!registrationNameModules.hasOwnProperty(propKey)) {
270 if (propKey === STYLE) {
236 - propValue = CSSPropertyOperations.createMarkupForStyles(
237 - propValue,
238 - instForDebug,
239 - );
271 + propValue = createMarkupForStyles(propValue, instForDebug);
272 }
273 var markup = null;
274 if (isCustomComponent(tagLowercase, props)) {
275 if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
244 - markup = DOMPropertyOperations.createMarkupForCustomAttribute(
276 + markup = DOMMarkupOperations.createMarkupForCustomAttribute(
277 propKey,
278 propValue,
279 );
280 }
281 } else {
250 - markup = DOMPropertyOperations.createMarkupForProperty(
282 + markup = DOMMarkupOperations.createMarkupForProperty(
283 propKey,
284 propValue,
285 );
@@ -265,9 +297,9 @@ function createOpenTagMarkup(
297 }
298
299 if (isRootElement) {
268 - ret += ' ' + DOMPropertyOperations.createMarkupForRoot();
300 + ret += ' ' + DOMMarkupOperations.createMarkupForRoot();
301 }
270 - ret += ' ' + DOMPropertyOperations.createMarkupForID('');
302 + ret += ' ' + DOMMarkupOperations.createMarkupForID('');
303 return ret;
304 }
305