Skip to content

Commit

Permalink
Pass SVG attributes through
Browse files Browse the repository at this point in the history
All attributes defined on SVG elements will now be passed directly regardless of the whitelist. The casing specified by user will be preserved, and setAttribute() will be used.

In the future we will remove support for the camel case aliases to the hyphenated attributes. For example, we currently map `strokeWidth` to `stroke-width` but this is now deprecated behind a warning. When we remove support for this we can remove some of the code paths introduced in this commit.

The purpose of this change is to stop maintaining a separate SVG property config. The config still exists for two purposes:

* Allow a migration path for deprecated camelcased versions of hyphenated SVG attributes
* Track special namespaced attributes (they still require a whitelist)

However it is no longer a blocker for using new non-namespaced SVG attributes, and users don't have to ask us to add them to the whitelist.

Fixes #1657
  • Loading branch information
gaearon committed Dec 24, 2015
1 parent 82fe64a commit 232a47a
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 37 deletions.
39 changes: 39 additions & 0 deletions src/renderers/dom/client/__tests__/ReactDOMSVG-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,45 @@ describe('ReactDOMSVG', function() {
ReactDOMServer = require('ReactDOMServer');
});

it('creates initial markup for known hyphenated attributes', function() {
var markup = ReactDOMServer.renderToString(
<svg clip-path="url(#starlet)" />
);
expect(markup).toContain('clip-path="url(#starlet)"');
});

it('creates initial markup for camel case attributes', function() {
var markup = ReactDOMServer.renderToString(
<svg viewBox="0 0 100 100" />
);
expect(markup).toContain('viewBox="0 0 100 100"');
});

it('deprecates camel casing of hyphenated attributes', function() {
spyOn(console, 'error');
var markup = ReactDOMServer.renderToString(
<svg clipPath="url(#starlet)" />
);
expect(markup).toContain('clip-path="url(#starlet)"');
expect(console.error.argsForCall.length).toBe(1);
expect(console.error.argsForCall[0][0]).toContain('clipPath');
expect(console.error.argsForCall[0][0]).toContain('clip-path');
});

it('creates initial markup for unknown hyphenated attributes', function() {
var markup = ReactDOMServer.renderToString(
<svg the-word="the-bird" />
);
expect(markup).toContain('the-word="the-bird"');
});

it('creates initial markup for unknown camel case attributes', function() {
var markup = ReactDOMServer.renderToString(
<svg theWord="theBird" />
);
expect(markup).toContain('theWord="theBird"');
});

it('creates initial namespaced markup', function() {
var markup = ReactDOMServer.renderToString(
<svg>
Expand Down
77 changes: 77 additions & 0 deletions src/renderers/dom/shared/DOMPropertyOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,32 @@ function shouldIgnoreValue(propertyInfo, value) {
(propertyInfo.hasOverloadedBooleanValue && value === false);
}

if (__DEV__) {
var reactProps = {
children: true,
dangerouslySetInnerHTML: true,
key: true,
ref: true,
};
var warnedSVGProperties = {};

var warnDeprecatedSVGProperty = function(name, attributeName) {
if (reactProps.hasOwnProperty(name) && reactProps[name] ||
warnedSVGProperties.hasOwnProperty(name) && warnedSVGProperties[name]) {
return;
}

warnedSVGProperties[name] = true;
warning(
false,
'SVG property %s is deprecated. Use the original attribute name ' +
'%s for SVG tags instead.',
name,
attributeName
);
};
}

/**
* Operations for dealing with DOM properties.
*/
Expand Down Expand Up @@ -124,6 +150,31 @@ var DOMPropertyOperations = {
return name + '=' + quoteAttributeValueForBrowser(value);
},

/**
* Creates markup for an SVG property.
*
* @param {string} name
* @param {*} value
* @return {string} Markup string, or empty string if the property was invalid.
*/
createMarkupForSVGAttribute: function(name, value) {
if (!isAttributeNameSafe(name) || value == null) {
return '';
}
var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ?
DOMProperty.properties[name] : null;
if (propertyInfo) {
var { attributeName, attributeNamespace } = propertyInfo;
if (__DEV__) {
if (!attributeNamespace && name !== attributeName) {
warnDeprecatedSVGProperty(name, attributeName);
}
}
return attributeName + '=' + quoteAttributeValueForBrowser(value);
}
return name + '=' + quoteAttributeValueForBrowser(value);
},

/**
* Sets the value for a property on a node.
*
Expand Down Expand Up @@ -183,6 +234,22 @@ var DOMPropertyOperations = {
}
},

setValueForSVGAttribute: function(node, name, value) {
var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ?
DOMProperty.properties[name] : null;
if (propertyInfo) {
if (__DEV__) {
var { attributeName, attributeNamespace } = propertyInfo;
if (!attributeNamespace && name !== attributeName) {
warnDeprecatedSVGProperty(name, attributeName);
}
}
DOMPropertyOperations.setValueForProperty(node, name, value);
} else {
DOMPropertyOperations.setValueForAttribute(node, name, value);
}
},

/**
* Deletes the value for a property on a node.
*
Expand Down Expand Up @@ -217,6 +284,16 @@ var DOMPropertyOperations = {
}
},

deleteValueForSVGAttribute: function(node, name) {
var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ?
DOMProperty.properties[name] : null;
if (propertyInfo) {
DOMPropertyOperations.deleteValueForProperty(node, name);
} else {
node.removeAttribute(name);
}
},

};

ReactPerf.measureMethods(DOMPropertyOperations, 'DOMPropertyOperations', {
Expand Down
13 changes: 13 additions & 0 deletions src/renderers/dom/shared/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,8 @@ ReactDOMComponent.Mixin = {
if (propKey !== CHILDREN) {
markup = DOMPropertyOperations.createMarkupForCustomAttribute(propKey, propValue);
}
} else if (this._namespaceURI === DOMNamespaces.svg) {
markup = DOMPropertyOperations.createMarkupForSVGAttribute(propKey, propValue);
} else {
markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
}
Expand Down Expand Up @@ -862,6 +864,11 @@ ReactDOMComponent.Mixin = {
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey);
} else if (this._namespaceURI === DOMNamespaces.svg) {
DOMPropertyOperations.deleteValueForSVGAttribute(
getNode(this),
propKey
);
}
}
for (propKey in nextProps) {
Expand Down Expand Up @@ -924,6 +931,12 @@ ReactDOMComponent.Mixin = {
propKey,
nextProp
);
} else if (this._namespaceURI === DOMNamespaces.svg) {
DOMPropertyOperations.setValueForSVGAttribute(
getNode(this),
propKey,
nextProp
);
} else if (
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
Expand Down
37 changes: 0 additions & 37 deletions src/renderers/dom/shared/SVGDOMPropertyConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,46 +23,19 @@ var NS = {
var SVGDOMPropertyConfig = {
Properties: {
clipPath: MUST_USE_ATTRIBUTE,
cx: MUST_USE_ATTRIBUTE,
cy: MUST_USE_ATTRIBUTE,
d: MUST_USE_ATTRIBUTE,
dx: MUST_USE_ATTRIBUTE,
dy: MUST_USE_ATTRIBUTE,
fill: MUST_USE_ATTRIBUTE,
fillOpacity: MUST_USE_ATTRIBUTE,
fontFamily: MUST_USE_ATTRIBUTE,
fontSize: MUST_USE_ATTRIBUTE,
fx: MUST_USE_ATTRIBUTE,
fy: MUST_USE_ATTRIBUTE,
gradientTransform: MUST_USE_ATTRIBUTE,
gradientUnits: MUST_USE_ATTRIBUTE,
markerEnd: MUST_USE_ATTRIBUTE,
markerMid: MUST_USE_ATTRIBUTE,
markerStart: MUST_USE_ATTRIBUTE,
offset: MUST_USE_ATTRIBUTE,
opacity: MUST_USE_ATTRIBUTE,
patternContentUnits: MUST_USE_ATTRIBUTE,
patternUnits: MUST_USE_ATTRIBUTE,
points: MUST_USE_ATTRIBUTE,
preserveAspectRatio: MUST_USE_ATTRIBUTE,
r: MUST_USE_ATTRIBUTE,
rx: MUST_USE_ATTRIBUTE,
ry: MUST_USE_ATTRIBUTE,
spreadMethod: MUST_USE_ATTRIBUTE,
stopColor: MUST_USE_ATTRIBUTE,
stopOpacity: MUST_USE_ATTRIBUTE,
stroke: MUST_USE_ATTRIBUTE,
strokeDasharray: MUST_USE_ATTRIBUTE,
strokeLinecap: MUST_USE_ATTRIBUTE,
strokeOpacity: MUST_USE_ATTRIBUTE,
strokeWidth: MUST_USE_ATTRIBUTE,
textAnchor: MUST_USE_ATTRIBUTE,
transform: MUST_USE_ATTRIBUTE,
version: MUST_USE_ATTRIBUTE,
viewBox: MUST_USE_ATTRIBUTE,
x1: MUST_USE_ATTRIBUTE,
x2: MUST_USE_ATTRIBUTE,
x: MUST_USE_ATTRIBUTE,
xlinkActuate: MUST_USE_ATTRIBUTE,
xlinkArcrole: MUST_USE_ATTRIBUTE,
xlinkHref: MUST_USE_ATTRIBUTE,
Expand All @@ -73,9 +46,6 @@ var SVGDOMPropertyConfig = {
xmlBase: MUST_USE_ATTRIBUTE,
xmlLang: MUST_USE_ATTRIBUTE,
xmlSpace: MUST_USE_ATTRIBUTE,
y1: MUST_USE_ATTRIBUTE,
y2: MUST_USE_ATTRIBUTE,
y: MUST_USE_ATTRIBUTE,
},
DOMAttributeNamespaces: {
xlinkActuate: NS.xlink,
Expand All @@ -94,23 +64,16 @@ var SVGDOMPropertyConfig = {
fillOpacity: 'fill-opacity',
fontFamily: 'font-family',
fontSize: 'font-size',
gradientTransform: 'gradientTransform',
gradientUnits: 'gradientUnits',
markerEnd: 'marker-end',
markerMid: 'marker-mid',
markerStart: 'marker-start',
patternContentUnits: 'patternContentUnits',
patternUnits: 'patternUnits',
preserveAspectRatio: 'preserveAspectRatio',
spreadMethod: 'spreadMethod',
stopColor: 'stop-color',
stopOpacity: 'stop-opacity',
strokeDasharray: 'stroke-dasharray',
strokeLinecap: 'stroke-linecap',
strokeOpacity: 'stroke-opacity',
strokeWidth: 'stroke-width',
textAnchor: 'text-anchor',
viewBox: 'viewBox',
xlinkActuate: 'xlink:actuate',
xlinkArcrole: 'xlink:arcrole',
xlinkHref: 'xlink:href',
Expand Down
Loading

0 comments on commit 232a47a

Please sign in to comment.