-
Notifications
You must be signed in to change notification settings - Fork 560
Add create-custom-element-type RFC #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
- Start Date: 2018-01-22 | ||
- RFC PR: (leave this empty) | ||
- React Issue: (leave this empty) | ||
|
||
# Summary | ||
|
||
The changes in this RFC specifically address how React passes data to custom | ||
elements and listens for their DOM events. | ||
|
||
# Basic example | ||
|
||
This RFC has two parts. | ||
|
||
1. React would change its current behavior such that when it passes data to a | ||
custom element, it uses JavaScript properties instead of calling | ||
`setAttribute`. | ||
|
||
For example: | ||
|
||
``` | ||
<my-element fooBar={baz}> | ||
``` | ||
|
||
Would become equivalent to: | ||
|
||
``` | ||
myElement.fooBar = baz; | ||
``` | ||
|
||
2. React would add a new API, `ReactDOM.createCustomElementType()` which would | ||
create a wrapper React component that knows how to map properties and event | ||
handlers back to a custom element. | ||
|
||
# Motivation | ||
|
||
These two changes are meant to address | ||
[#7249](https://github.com/facebook/react/issues/7249), | ||
[#7901](https://github.com/facebook/react/issues/7901), | ||
[#11347](https://github.com/facebook/react/issues/11347) and a handful of other | ||
related issues. The goal is to make it easier to integrate [custom | ||
elements](https://developers.google.com/web/fundamentals/web-components/customelements) | ||
in React projects. | ||
|
||
As shown on [Custom Elements | ||
Everywhere](https://custom-elements-everywhere.com/#react), using a custom | ||
element in React today requires a handful of workarounds to pass complex | ||
JavaScript data (objects, and arrays) to custom element properties or listen for | ||
their DOM events. | ||
|
||
# Detailed design | ||
|
||
We're proposing React make both of these changes: | ||
|
||
## 1. Prefer JavaScript properties on Custom Elements | ||
|
||
By default, setting a prop on a custom element will use a JavaScript property | ||
setter. This changes React's current behavior, which uses `setAttribute()`. | ||
|
||
For example: | ||
|
||
```jsx | ||
<my-element fooBar={baz}> | ||
``` | ||
|
||
Would become equivalent to: | ||
|
||
```jsx | ||
myElement.fooBar = baz; | ||
``` | ||
|
||
When calling `ReactDOMServer.renderToString()`, props on custom elements should | ||
be set as attributes. Camel case props should be converted to all lowercase | ||
attributes. | ||
|
||
Example: | ||
|
||
```jsx | ||
const baz = 'hello'; | ||
<my-element fooBar={baz}> // before renderToString() | ||
'<my-element foobar="hello">' // after renderToString() | ||
``` | ||
|
||
Note: Some custom element libraries use a heuristic where camel case properties | ||
are converted to dash-cased attributes. E.g. `.fooBar` becomes `foo-bar=""`. | ||
Prior experience has shown that this actually confuses a fair number of | ||
developers. Plus there is prior art in HTML for doing all lowercase attributes, | ||
e.g. `contenteditable`, `autocomplete`, `tabindex`. For these reasons, we think | ||
it's easiest to convert camel cased properties to lowercase attributes, without | ||
a delimiter, when calling `ReactDOMServer.renderToString()`. | ||
|
||
## 2. ReactDOM.createCustomElementType() | ||
|
||
Developers can use the new `createCustomElementType()` API to export a wrapper | ||
React component which knows how to map props to the underlying custom element. | ||
Because of the changes outlined in the first point, this wrapper would only be | ||
necessary in situations where: | ||
|
||
- an element only exposes an attribute API with no corresponding property. | ||
- during SSR, a property name cannot easily be mapped back to an attribute name | ||
by lowercasing it. | ||
- serializing a property value to an attribute value requires a custom function, | ||
e.g. calling `JSON.stringify()`. | ||
- you want to be able to declaratively listen for DOM events from a custom | ||
element (currently not supported in React, | ||
[#7901](https://github.com/facebook/react/issues/7901)). | ||
|
||
Example signature: | ||
|
||
```js | ||
ReactDOM.createCustomElementType(tagName[, configuration]) | ||
``` | ||
|
||
Example configuration: | ||
|
||
```js | ||
export default ReactDOM.createCustomElementType('x-foo', { | ||
propName: { | ||
// Attribute name to map to. | ||
attribute: string | null, | ||
|
||
// Serialize function to convert prop value | ||
// to attribute string. | ||
serialize: function | null, | ||
|
||
// Indicates the prop is an event, and the value | ||
// should be an event handler function. | ||
// If the event key is used, then all of the other | ||
// keys (property, attribute, serialize) should throw | ||
// compile warnings if set. | ||
event: string | null | ||
} | ||
propName2: { | ||
... | ||
} | ||
... | ||
}); | ||
``` | ||
|
||
Example usage: | ||
|
||
```jsx | ||
// XFoo.js | ||
export default ReactDOM.createCustomElementType('x-foo', { | ||
longName: { | ||
attribute: 'longname' | ||
}, | ||
someJSONdata: { | ||
attribute: 'somejsondata', | ||
serialize: JSON.stringify | ||
}, | ||
onURLChanged: { | ||
event: 'urlchanged' | ||
} | ||
}); | ||
``` | ||
|
||
```jsx | ||
// App.js | ||
import XFoo from './XFoo'; | ||
import React from 'react'; | ||
import {render} from 'react-dom'; | ||
|
||
function App() { | ||
const name = 'Ronald McDonald'; | ||
const data = {hello: 'world'}; | ||
const handleChange = function() { | ||
console.log('the url changed!'); | ||
} | ||
|
||
return ( | ||
<XFoo longName={name} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm having some ideas now that I've seen this part specifically. The 95% use-case is probably that you're going to be able to have access to the custom element constructor where you're writing the React code. The rest of the time, you may want deferred upgrades. Part 1 - you have the custom element constructorIf you have the custom element constructor, you actually don't need to define a wrapper or any information about it. You have the properties it defines as well as a way to diff and construct it if it needs to be patched. In this scenario, React could internally just check if it's a custom element constructor and set props if defined, or attributes otherwise. They don't need the tag name, either. I PR'd this to Preact awhile back for reference: preactjs/preact#715. SSR-storyThis assumes you have server-side support for whatever DOM features would be utilised (JSDOM / Undom / Domino, etc). If this is not the case, then it should fallback to Part 2. Until there's support for declarative shadow DOM (whatwg/dom#510) custom elements that have shadow DOM would render the host + light DOM as it normally would with out shadow DOM (no special APIs to mention here, just the standard Due to the tight coupling this part has with the DOM, it is assumed that it would be implemented as part of ReactDOM, as opposed to React. Part 2 - deferred upgrades (no constructor)You do what you're proposing in this RFC. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the case of "Part 1" how would server-side rendering work? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You'd have to have JSDOM support or another server side implementation support, but it would render out to host + light DOM and then rehydrate on the client. I was making the assumption this would be implemented in react-dom, if possible. I'll update. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @treshugart hm I'm confused by some of your comments. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ugh, hate the new github behavior where shift-enter submits a comment instead of inserting a newline. Continuing on from above... In your comment you start off by talking about Part 1 and using the element constructor. But Part 1 of my proposal assumed someone was just using the tag name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sure, but what I'm saying is that most of the time you have access to the constructor; this is the happy path and I think it should be the primary consideration by this RFC. I believe itt enables better ergonomics because you can infer all your information from the constructor. Manual configuration is necessary when you don't have it, but it might be worth considering making this secondary.
Ok, that's fair. Looking back I think this would naturally happen anyways if using react-dom and it was being run under a node-dom env. Let's not worry about this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @treshugart Are you suggesting folks would never use their custom element tag name and would instead always use the constructor in their render functions? That's not actually something I would want to do tbh. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, our ideals here are quite divergent. I was hopeful that using a custom element in React could be the same way you'd use a React component. I almost never use custom element tag names and it's been quite liberating. Sure in HTML you do, but this isn't HTML. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'm concerned that if you're using the constructor, it may be tough to distinguish a custom element from a React component. Because they are different and may have subtle differences in their behavior, it seems like if you want something to be indistinguishable from a React component it may be better to use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that's fair. I'm 100% happy with this RFC as is and am super stoked you're doing it. I really hope this gets in. As an aside, and in terms of ergonomics, I think it would be a nice addition to eventually add support for constructors because it clean up the API, essentially making the usage of a custom element just as simple using a React component. The fact that two components can have inconsistencies isn't a WC / React thing, it's a component author thing and I think that's an orthogonal problem. |
||
someJSONdata={data} | ||
onURLChanged={handleChange} /> | ||
) | ||
} | ||
|
||
render(<App />, document.querySelector('#app')) | ||
``` | ||
|
||
# Drawbacks | ||
|
||
- Changing to setting properties instead of calling `setAttribute()` on custom | ||
elements would be an API breaking change. | ||
- However, developers can use `ReactDOM.createCustomElementType()` to get | ||
things back in working order. | ||
- Or, if the attribute name matches their property name, things will "just | ||
work". | ||
|
||
# Alternatives | ||
|
||
There has been an immense amount of discussion of alternatives at | ||
[#11347](https://github.com/facebook/react/issues/11347). The current proposal | ||
was arrived at with the React team and the community after working through about | ||
5 different contenders. For custom element authors, switching to a | ||
properties-first approach would be very convenient for them and unlock the | ||
ability to easily pass objects and arrays to custom elements in JSX. For folks | ||
who want an additional layer of security, or just prefer to look of working with | ||
React components throughout their app, they could use the | ||
`ReactDOM.createCustomElementType()` API. | ||
|
||
One possible area where we could tweak things would be map camelCased properties | ||
to dash-cased attributes, instead of going all lowercase. Because all Polymer | ||
elements follow this heuristic (and many/most(?) custom elements are written in | ||
Polymer), it would mean that they should all continue to "just work" with no | ||
code changes. | ||
|
||
# Adoption strategy | ||
|
||
**If we implement this proposal, how will existing React developers adopt it?** | ||
|
||
This would only affect the subset of React developers who also use custom | ||
elements. If their custom elements expose a JS properties interface (which is | ||
recommended in [Google's best practices | ||
docs](https://developers.google.com/web/fundamentals/web-components/best-practices#always-accept-primitive-data-strings-numbers-booleans-as-either-attributes--or-properties)) | ||
then it may require no code changes at all. However, if they don't have | ||
corresponding properties for their attributes, they will either need to: update | ||
their element, or update their React app to explicitly call `setAttribute()`, or | ||
use the proposed `ReactDOM.createCustomElementType()` API. | ||
|
||
**Is this a breaking change?** | ||
|
||
Yes but we estimate that the number of users affected would be small. | ||
|
||
**Can we write a codemod?** | ||
|
||
Someone could probably write a codemod specific to their component to update it | ||
in all of their apps. | ||
|
||
**Should we coordinate with other projects or libraries?** | ||
|
||
I can't think of a reason why that would be necessary, but perhaps others have | ||
opinions :) | ||
|
||
# How we teach this | ||
|
||
There are already docs on [using Web Components in | ||
React](https://reactjs.org/docs/web-components.html), so we could update them to | ||
explain these changes. We would also need to update the `ReactDOM` API docs to | ||
explain the new `createCustomElementType()` API. Probably would want to link to | ||
the Web Components docs from that API explainer. | ||
|
||
Again, we imagine this should only affect a small subset of the React community, | ||
those folks currently trying to integrate custom elements into their apps. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not call it just
createCustomElement
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion. This was the name suggested by @gaearon when we were first kicking around the idea. facebook/react#11347 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, I think it would be a little misleading to call it
createCustomElement
since what it returns is not actually a custom element, but instead a React component that wraps a custom element.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe createCustomElementWrapper in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.createCustomElementTypeWrapper()
?