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.
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…
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:
Another option is use a ref to pass the object/array as property instead attribute:
constApp=()=>{constref=useRef()constdata=[{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-datasetref={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.
constApp=()=>{constref=useRef()constdata=[{x:50,y:25},{x:29,y:47}]constcustomEventHandler=function(e){const[realTarget]=e.composedPath()constextra=e.detail// do something with them}useEffect(()=>{if(ref.current){ref.current.data=data// set the propertyref.current.addEventListener('custom-event',customEventHandler)}})return(<h1>My awesome app</h1><x-datasetref={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:
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) */constevents=Object.entries(props).filter(([k,v])=>k.match(eventPattern)).map(([k,v])=>({[k]:v}))/** Get only the complex props (objects and arrays) */constcomplexProps=Object.entries(props).filter(([k,v])=>typeofv==='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(consteventofevents){const[key,impl]=Object.entries(event)[0]consteventName=toKebabCase(key.replace('onEvent','')).replace('-','')/** Add the listeners Map if not present */if(!element[listeners]){element[listeners]=newMap()}/** 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)deletenewProps[key]}}
For each event handler, we need to:
convert the camel case name to kebab case: Eg. onEventToggleAccordion to toggle-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(constpropofcomplexProps){const[key,value]=Object.entries(prop)[0]deletenewProps[key]element[key]=value// assign the complex prop as property instead attribute}
Finally, call the React.createElement function to create our element:
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.