Skip to content

Instantly share code, notes, and snippets.

@jimthedev
Last active July 23, 2020 11:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jimthedev/b0fbccec1b1e43fb547900c5bbf3fe17 to your computer and use it in GitHub Desktop.
Save jimthedev/b0fbccec1b1e43fb547900c5bbf3fe17 to your computer and use it in GitHub Desktop.
use web component hook for react
import { useCustomElement } from "./useCustomElement";
// My regular react app that is using a web component /
// custom element inside it.
// Notice that we have some handlers, some state, etc.
function Component() {
const [txt, setTxt] = React.useState("init")
function handleClick() {
setTxt("clicked")
}
function handleLeave() {
setTxt("out")
}
function handleEnter() {
setTxt("enter")
}
return (
<div>
<my-web-component
{...useCustomElement({
onClick: handleClick,
onMouseLeave: handleLeave,
onMouseEnter: handleEnter,
title: "Title",
text: txt,
subtext: "I am some subtext"
})}
/>
</div>
)
}
export function useCustomElement(items) {
const ref = React.useRef();
// Events are tricky in wc in react. We need to manually
// attach listeners. We can't rely on synthetic events.
React.useEffect(() => {
matchProperties(
{
handler: (eventType, value) => {
return ref.current.addEventListener(eventType, value);
},
property: (key, value) => {
return (ref.current[key] = value);
}
},
items
);
function matchProperties(matchers, properties) {
for (let pair of Object.entries(items)) {
const [key, value] = pair;
if (
key[0] === "o" &&
key[1] === "n" &&
key[2] == key[2].toUpperCase()
) {
if (typeof matchers.handler === "function") {
const eventType = key.substring(2, key.length).toLowerCase();
matchers.handler(eventType, value);
}
} else {
if (typeof matchers.property === "function") {
matchers.property(key, value);
}
}
}
}
return () => {
matchProperties(
{
handler: (eventType, value) => {
return ref.current.removeEventListener(eventType, value);
}
},
items
);
};
}, []);
// Deal with attributes that aren't events
const entries = Object.entries(items).filter(
([key, val]) => typeof items[key].indexOf !== "undefined"
);
const remainder = entries.reduce((a, item) => {
const [key, value] = item;
return {
...a,
[key]: value
};
}, {});
const result = { ref, ...remainder };
return result;
}
@sslotsky
Copy link

sslotsky commented Jan 4, 2020

This is really cool! A couple thoughts based on my experience with web components, as well as my experience using web components from React which is possibly more than I'd care to admit 😂

  1. The naming convention for event listeners works fine, but I think most frameworks that interop with WC just do something like onMyEvent, then to attach the listener they'd e.g. addEventListener('myEvent'). Although your method might be easier to deal with. So of course this doesn't really improve things in any meaningful way but might make it feel more natural?
  2. To be clear, when you call setAttribute you are setting an attribute and not a prop. You could see if the attribute is defined on the node with ref.current.hasAttribute(key), and if that is true then call setAttribute. If it's false, I think you can just set a prop by doing ref.current[key] = value.
  3. Also if the value you're dealing with is rich data, e.g. an object or array, you might even just default to setting the prop with ref.current[key] = value. These types of data can't be set with an attribute anyway, so you may as well assume they are props.

In case you haven't seen it, custom-elements-everywhere.com gives pretty good descriptions of how the different frameworks approach setting properties. They tend to do some combination of the above. I really like where you're going with this and I think it could be really useful to other React devs who find themselves working with WC.

@jimthedev
Copy link
Author

@sslotsky awesome. Thank you for the feedback!

  1. I will work on the on syntax you described. One thing I didn’t want to have to do was maintain a list of all possible events. The camel casing gets a bit strange and unfortunately mouseleave and other eventTypes aren’t camel cased. Tradeoffs exist but certainly we can play around with this. I agree my personal preference is still to find a way to use the onMouseLeave syntax but without an extensive camel casing map I am not sure how to make it happen.

2/3 You are correct, these should likely be props in some (or many) cases. I was thinking about adding in square bracket syntax alla Angular but perhaps there is a better solution here that involves data type checking and/or checking for the existence of a prop. Camel translation again becomes a question but I will look and see what others are doing here.

Perhaps the name of this thing should be useCustomElement instead of useWebComponent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment