Skip to content

Commit 9eeef55

Browse files
Merge pull request #78 from bschlenk/support-custom-elements
feat: support custom elements in React 16
2 parents bfe2023 + 7b2c5a8 commit 9eeef55

File tree

7 files changed

+187
-13
lines changed

7 files changed

+187
-13
lines changed

lib/attributes-to-props.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ DOMProperty.injection.injectDOMPropertyConfig(
1515
* @param {Object} attributes - The attributes.
1616
* @return {Object} - The props.
1717
*/
18-
function attributesToProps(attributes) {
19-
attributes = attributes || {};
18+
function attributesToProps(attributes = {}) {
2019
var props = {};
2120
var propertyName;
2221
var propertyValue;
@@ -49,6 +48,8 @@ function attributesToProps(attributes) {
4948
reactProperty = config.svg[propertyName];
5049
if (reactProperty) {
5150
props[reactProperty] = propertyValue;
51+
} else if (utilities.reactSupportsUnknownAttributes()) {
52+
props[propertyName] = propertyValue;
5253
}
5354
}
5455

lib/dom-to-react.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
var React = require('react');
22
var attributesToProps = require('./attributes-to-props');
3+
var utilities = require('./utilities');
34

45
/**
56
* Converts DOM nodes to React elements.
@@ -41,8 +42,12 @@ function domToReact(nodes, options) {
4142
continue;
4243
}
4344

44-
// update values
45-
props = attributesToProps(node.attribs);
45+
props = node.attribs;
46+
if (!shouldPassAttributesUnaltered(node)) {
47+
// update values
48+
props = attributesToProps(node.attribs);
49+
}
50+
4651
children = null;
4752

4853
// node type for <script> is "script"
@@ -83,4 +88,12 @@ function domToReact(nodes, options) {
8388
return result.length === 1 ? result[0] : result;
8489
}
8590

91+
function shouldPassAttributesUnaltered(node) {
92+
return (
93+
utilities.reactSupportsUnknownAttributes() &&
94+
node.type === 'tag' &&
95+
utilities.isCustomComponent(node.name, node.attribs)
96+
);
97+
}
98+
8699
module.exports = domToReact;

lib/utilities.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
var React = require('react');
12
var hyphenPatternRegex = /-([a-z])/g;
23

34
/**
@@ -61,7 +62,52 @@ function invertObject(obj, override) {
6162
return result;
6263
}
6364

65+
/**
66+
* Check if a given tag is a custom component.
67+
*
68+
* @see https://github.com/facebook/react/blob/master/packages/react-dom/src/shared/isCustomComponent.js
69+
* @param {string} tagName - The name of the html tag.
70+
* @param {Object} props - The props being passed to the element.
71+
* @return {boolean}
72+
*/
73+
function isCustomComponent(tagName, props) {
74+
if (tagName.indexOf('-') === -1) {
75+
return props && typeof props.is === 'string';
76+
}
77+
switch (tagName) {
78+
// These are reserved SVG and MathML elements.
79+
// We don't mind this whitelist too much because we expect it to never grow.
80+
// The alternative is to track the namespace in a few places which is convoluted.
81+
// https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts
82+
case 'annotation-xml':
83+
case 'color-profile':
84+
case 'font-face':
85+
case 'font-face-src':
86+
case 'font-face-uri':
87+
case 'font-face-format':
88+
case 'font-face-name':
89+
case 'missing-glyph':
90+
return false;
91+
default:
92+
return true;
93+
}
94+
}
95+
96+
/**
97+
* Check if the installed React version supports setting unknown attributes
98+
* on elements.
99+
*
100+
* @return {boolean}
101+
*/
102+
function reactSupportsUnknownAttributes() {
103+
const majorStr = React.version.split('.')[0];
104+
const major = parseInt(majorStr);
105+
return major >= 16;
106+
}
107+
64108
module.exports = {
65109
camelCase: camelCase,
66-
invertObject: invertObject
110+
invertObject: invertObject,
111+
isCustomComponent: isCustomComponent,
112+
reactSupportsUnknownAttributes: reactSupportsUnknownAttributes
67113
};

test/attributes-to-props.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1+
const React = require('react');
12
const assert = require('assert');
23
const attributesToProps = require('../lib/attributes-to-props');
34

45
describe('attributesToProps', () => {
6+
let actualReactVersion;
7+
beforeEach(() => {
8+
actualReactVersion = React.version;
9+
});
10+
11+
afterEach(() => {
12+
React.version = actualReactVersion;
13+
});
14+
515
describe('HTML DOM', () => {
616
it('converts attributes to React props', () => {
717
assert.deepEqual(
@@ -108,6 +118,16 @@ describe('attributesToProps', () => {
108118
}
109119
);
110120
});
121+
122+
it('does not include unknown attributes for older react versions', () => {
123+
React.version = '0.14';
124+
assert.deepEqual(
125+
attributesToProps({
126+
unknownAttribute: 'someValue'
127+
}),
128+
{}
129+
);
130+
});
111131
});
112132

113133
describe('SVG DOM properties', () => {
@@ -134,22 +154,32 @@ describe('attributesToProps', () => {
134154
);
135155
});
136156

137-
it('does not convert incorrectly capitalized properties', () => {
157+
it('includes but does not convert incorrectly capitalized properties', () => {
138158
assert.deepEqual(
139159
attributesToProps({
140160
'XLINK:HREF': '#',
141161
ychannelselector: 'G',
142162
ZoomAndPan: 'disable'
143163
}),
144164
{
145-
/*
146-
xlinkHref: '#',
147-
yChannelSelector: 'G',
148-
zoomAndPan: 'disable'
149-
*/
165+
'XLINK:HREF': '#',
166+
ychannelselector: 'G',
167+
ZoomAndPan: 'disable'
150168
}
151169
);
152170
});
171+
172+
it('does not include incorrectly capitalized properties on older React versions', () => {
173+
React.version = '0.14';
174+
assert.deepEqual(
175+
attributesToProps({
176+
'XLINK:HREF': '#',
177+
ychannelselector: 'G',
178+
ZoomAndPan: 'disable'
179+
}),
180+
{}
181+
);
182+
});
153183
});
154184

155185
describe('style', () => {

test/dom-to-react.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ const domToReact = require('../lib/dom-to-react');
55
const { data, render } = require('./helpers/');
66

77
describe('dom-to-react parser', () => {
8+
let actualReactVersion;
9+
beforeEach(() => {
10+
actualReactVersion = React.version;
11+
});
12+
13+
afterEach(() => {
14+
React.version = actualReactVersion;
15+
});
16+
817
it('converts single DOM node to React', () => {
918
const html = data.html.single;
1019
const reactElement = domToReact(htmlToDOM(html));
@@ -131,4 +140,32 @@ describe('dom-to-react parser', () => {
131140
React.createElement('svg', { viewBox: '0 0 512 512', id: 'foo' }, 'Inner')
132141
);
133142
});
143+
144+
it('passes props unaltered for custom elements', () => {
145+
const html = data.html.customElement;
146+
const reactElement = domToReact(htmlToDOM(html));
147+
148+
assert.deepEqual(
149+
reactElement,
150+
React.createElement(
151+
'custom-button',
152+
{
153+
class: 'myClass',
154+
'custom-attribute': 'value'
155+
},
156+
null
157+
)
158+
);
159+
});
160+
161+
it('handles custom element the same as everything else with older reacts', () => {
162+
React.version = '0.14';
163+
const html = data.html.customElement;
164+
const reactElement = domToReact(htmlToDOM(html));
165+
166+
assert.deepEqual(
167+
reactElement,
168+
React.createElement('custom-button', { className: 'myClass' }, null)
169+
);
170+
});
134171
});

test/helpers/data.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"img": "<img src=\"http://stat.ic/img.jpg\" alt=\"Image\"/>",
1212
"void": "<link/><meta/><img/><br/><hr/><input/>",
1313
"comment": "<!-- comment -->",
14-
"doctype": "<!DOCTYPE html>"
14+
"doctype": "<!DOCTYPE html>",
15+
"customElement": "<custom-button class=\"myClass\" custom-attribute=\"value\"></custom-button>"
1516
},
1617
"svg": {
1718
"simple": "<svg viewBox=\"0 0 512 512\" id=\"foo\">Inner</svg>",

test/utilities.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
const React = require('react');
12
const assert = require('assert');
2-
const { camelCase, invertObject } = require('../lib/utilities');
3+
const {
4+
camelCase,
5+
invertObject,
6+
isCustomComponent,
7+
reactSupportsUnknownAttributes
8+
} = require('../lib/utilities');
39

410
describe('utilties.camelCase', () => {
511
[undefined, null, 1337, {}, []].forEach(value => {
@@ -80,4 +86,44 @@ describe('utilities.invertObject', () => {
8086
);
8187
});
8288
});
89+
90+
describe('utilities.isCustomComponent', () => {
91+
it('returns true if the tag contains a hyphen and is not in the whitelist', () => {
92+
assert.equal(isCustomComponent('my-custom-element'), true);
93+
});
94+
95+
it('returns false if the tag is in the whitelist', () => {
96+
assert.equal(isCustomComponent('annotation-xml'), false);
97+
assert.equal(isCustomComponent('color-profile'), false);
98+
assert.equal(isCustomComponent('font-face'), false);
99+
});
100+
101+
it('returns true if the props contains an `is` key', () => {
102+
assert.equal(isCustomComponent('button', { is: 'custom-button' }), true);
103+
});
104+
});
105+
106+
describe('utilities.reactSupportsUnknownAttributes', () => {
107+
let actualVersion;
108+
beforeEach(() => {
109+
actualVersion = React.version;
110+
});
111+
112+
afterEach(() => {
113+
React.version = actualVersion;
114+
});
115+
116+
it('should return true for React 16 and above', () => {
117+
React.version = '16.6.0';
118+
assert.equal(reactSupportsUnknownAttributes(), true);
119+
});
120+
121+
it('should return false for React < 16', () => {
122+
React.version = '15.1.2';
123+
assert.equal(reactSupportsUnknownAttributes(), false);
124+
125+
React.version = '0.14';
126+
assert.equal(reactSupportsUnknownAttributes(), false);
127+
});
128+
});
83129
});

0 commit comments

Comments
 (0)