You can test a sample project here:
gugadev / react-ce-ubigeo
Example using a custom JSX pragma for enable better tooling for custom elements.
This project was bootstrapped with Create React App.
How to test
Just run the app and drop/choose the ubigeo.txt
file that is inside public/
folder.
Background
A element, by definition and spec, cannot accept complex properties like objects or arrays. This is a problem when we want to use these kinds of properties in a React project.
For example, this code doesn't work:
const App = function() {
const data = { a: true }
return (
<div className="my-app">
<my-comp data={data} />
</div>
)
}
Because in runtime, the data passed as attribute is converted to string using .toString()
. For that reason, if you pass an object, you will ended up receiving an [object Object]
(because { a: true }.toString()
).
Another problem of using custom elements in JSX is respect to custom…
Online demo here:
Hey, you can use web components in JSX code anyway.
Yeah, sure. However, there are certain use cases where you cannot use a web component following React guidelines, like passing complex properties such Objects and Arrays and binding custom events. So, what could we do as a workaround for these? Let's see.
Passing objects/arrays to custom elements
There are some options. The easiest way is use JSON.stringify
to pass it as a attribute:
const App = () => {
const data = [
{ x: 50, y: 25 },
{ x: 29, y: 47 }
]
return (
<h1>My awesome app</h1>
<x-dataset data={JSON.stringify(data)} />
)
}
Another option is use a ref to pass the object/array as property instead attribute:
const App = () => {
const ref = useRef()
const data = [
{ x: 50, y: 25 },
{ x: 29, y: 47 }
]
useEffect(() => {
if (ref.current) {
ref.current.data = data // set the property
}
})
return (
<h1>My awesome app</h1>
<x-dataset ref={ref} />
)
}
Hmm, I prefer the second one. And you?
Binding custom events
This is a very common case when we deal with custom elements. When you need to attach a listener to a custom event, you need to use a ref and use addEventListener
yourself.
const App = () => {
const ref = useRef()
const data = [
{ x: 50, y: 25 },
{ x: 29, y: 47 }
]
const customEventHandler = function(e) {
const [realTarget] = e.composedPath()
const extra = e.detail
// do something with them
}
useEffect(() => {
if (ref.current) {
ref.current.data = data // set the property
ref.current.addEventListener('custom-event', customEventHandler)
}
})
return (
<h1>My awesome app</h1>
<x-dataset ref={ref} />
)
}
Pretty simple, right? But, could we make it even easier? Yeah! using a custom JSX pragma.
Creating a custom JSX pragma
This is not a very simple way when we create the pragma, but, once that, you don't need to add aditional logic like example above. You will ended up using custom elements as any regular React component!
The following code is a fork of jsx-native-events that I've extended adapt it to my needs.
First of all, what is a JSX pragma?
JSX Pragma
Pragma is just the function that transform JSX syntax to JavaScript. The default pragma in React is React.createElement
.
So, that you understand this, let's see we have the following sentence:
<button type="submit">
Hello
</button>
Is transformed to:
React.createElement(
'button',
{ type: 'submit' },
'Hello'
)
That's why we need to import React
event if we don't use it explicitly!
So, what if we can take control over this transform process? That's exactly a pragma let us. So, let's code it.
So, what we did here? First, we need to get the check if it's an custom element. If is, assign a ref
callback. Inside this callback we need to handle the logic.
Once inside the ref
callback, get all the custom events and the complex properties. For the first one, the event handler name must start with the prefix onEvent
(necessary to not conflict with regular JSX events). For the properties, we are going to check if the type is an object (typeof).
/** Map custom events as objects (must have onEvent prefix) */
const events =
Object
.entries(props)
.filter(([k, v]) => k.match(eventPattern))
.map(([k, v]) => ({ [k]: v }))
/** Get only the complex props (objects and arrays) */
const complexProps =
Object
.entries(props)
.filter(([k, v]) => typeof v === 'object')
.map(([k, v]) => ({ [k]: v }))
At this point, we have both, the custom event handlers and the complex properties. The next step is iterate the event handlers and the complex properties.
for (const event of events) {
const [key, impl] = Object.entries(event)[0]
const eventName = toKebabCase(
key.replace('onEvent', '')
).replace('-', '')
/** Add the listeners Map if not present */
if (!element[listeners]) {
element[listeners] = new Map()
}
/** If the listener hasn't be attached, attach it */
if (!element[listeners].has(eventName)) {
element.addEventListener(eventName, impl)
/** Save a reference to avoid listening to the same value twice */
element[listeners].set(eventName, impl)
delete newProps[key]
}
}
For each event handler, we need to:
- convert the camel case name to kebab case: Eg.
onEventToggleAccordion
totoggle-accordion
. - Add the event handler to the listeners map to remove the listener later.
- add the listener to the custom element.
For the properties is pretty similar and simple:
for (const prop of complexProps) {
const [key, value] = Object.entries(prop)[0]
delete newProps[key]
element[key] = value // assign the complex prop as property instead attribute
}
Finally, call the React.createElement
function to create our element:
return React.createElement.apply(null, [type, newProps, ...children])
And that's all. Now, just left use it.
Using the custom JSX pragma
There are two ways of using a custom pragma. The first is through the tsconfig.json
:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "webComponents" // default is "React.createElement"
}
}
The second one is through a comment at top of the files:
/** @jsx webComponents */
Any of these two options you use need to import our pragma:
import React from 'react'
import webComponents from '../lib/ce-pragma' // our pragma
// our component
Now, you can use your custom elements as any regular React component:
/** @jsx webComponents */
import { SFC } from 'react'
import webComponents from '../lib/ce-pragma'
export default function MyScreen() {
const handleCustomEvent = function (e) {
}
return (
<div>
<my-custom-component
data={[ { a: true} ]}
onMyCustomEvent={handleCustomEvent}
/>
</div>
)
}
Conclusion
Using a custom pragma sounds like a very situable solution for now. Maybe in a short-term future React have better custom elements support. All could be possible in the crazy and big JavaScript ecosystem.
Top comments (0)