One company’s relationship with custom elements

At Canopy, our journey with custom elements resembles a teen drama whose main characters finally got together and graduated high school, but now it’s up for renewal and the writers don’t know where to take it.

It has all of the elements of that story:

  • One side ignores the other
  • A chance encounter sparks something exciting
  • Infatuation and full commitment
  • Rejection
  • Even more rejection
  • Success
  • Hard, painful realities
  • Uncertain future

This blogpost takes you through those phases, including unbending webkits, exciting software conferences, rejected pull requests, forked polyfills, underwhelming transformation layers, painfully brittle tests, and enduring through all of it.

One side ignores the other

The story starts in 2015 with my coworker Bret indoctrinating me with anti-Polymer and anti-web-component sentiment. “You have to interact with them through an imperative API,” he said. “There’s a global registry and you can’t control how you name them!”

Even still, another coworker, Kent, went forward and dabbled with web components in late 2015. But he ran into cross-browser compatibility issues with the polyfill code itself. So we gave up on web components. They found their way into what-we-hate-at-Canopy lore.

The type of thing you smirk at with your friends when they come around the corner of the high school hallway.

A chance encounter sparks something exciting

Fast forward to the 2016 UtahJS conference. Bret attended a conference talk by Cory Brown who peeled back the layers of polymer, web components, and custom elements.

We discovered that web components were actually made up of four separate specs and that we didn’t have to use all parts of that spec. You can have reusable, framework-agnostic components via custom elements, without using shadow dom, html imports (which are now dead in favor is ESM imports), or html templates.

Cory also recommended using custom elements for leaf node components to start out with, instead of as a full blown UI framework.

Actual footage of Bret Little

This excited us.

Bret and I were completely unaware of custom elements existing separately from web components. A lowish-level api for reusable components was exactly what we were looking for.

We had a mixture of AngularJS and React in a single page application, all coexisting via single-spa and sofe. Our company styleguide was a combination of raw css and some javascript components — the css was reusable across AngularJS and React, but the javascript components had to be reimplemented in each framework.

Maybe custom elements could solve all of this for us. We could build leaf node components like buttons, inputs, and multiselects as custom elements to be reused by AngularJS, React, or anything else.

Infatuation and full commitment

What really sealed the deal, was when I stumbled across the custom elements spec and Rob Dodson’s Custom Elements Anywhere article in fall 2016.

I was exposed to concepts like autonomous custom elements that had their own <tag-name /> and customized built-in elements which extended native elements with things like <button is="my-button" />. Both of those sounded perfect for different styleguide components at Canopy.

I immediately started planning how we could get custom elements into our styleguide. I found out that Chrome (mostly) supported custom elements and that the custom elements polyfill brought browser support to everything else.

Like any good teen romance, I committed fully to the idea and was ready to overcome anything to make it happen. No red flag, no matter how serious, would stop me.

However, there were a few red flags:

One was that my scrollbar gets really small when I read about the controversy surrounding customized builtin elements. Especially when a Webkit (Safari) developer says that they will not implement this portion of the spec and that they only approved the spec so that they could try to block it later. 47 downvotes and some toxic flame wars should have deterred me, but I was unperturbed.

Another red flag was that framework support for interacting with custom elements is unfortunately limited. Even modern frameworks like React struggle to allow developers to control whether data is passed down as html attributes or javascript properties. Passing data back up via DOM events has even sketchier support (think refs instead of jsx).

Finally, writing code for a custom element is unfamiliar, more low level than framework components, and something my team did not have experience in.

But none of this phased me.

Rejection

Sometimes in a relationship, it seems like the harder you try the less you succeed.

I was determined to make all of this custom elements stuff work. I realized the custom elements polyfill didn’t support customized builtins, so I took it upon myself to pull request the polyfill. In January 2017, I made twelve commits over a two week period that brought support for customized builtins to the polyfill. Community feedback was helpful and positive, but I didn’t hear back from a maintainer until March.

The news was bad. The maintainer explained that the polyfill owners were in the process of rewriting the polyfill from the ground up and that my code would not be merged. He was kind and told me the news regrettably, but I was back to square one. My code would not be merged and I would have to rewrite it.

Not to be deterred, I re-implemented customized builtins for the rewritten polyfill. Three weeks and eight big commits later, at the end of March, I succeeded in doing so. I opened my second pull request, with hopes high.

However, that pull request has been drowned out in 76 comments whose content varies from how to implement it, whether to implement it, frustration at the maintainers not being responsive, frustration at lack of unanimity, and also some sprinkled-in productive conversation.

I did not know what to do, so I asked the twitterverse how to best navigate the politics of web standards:

A couple of months later, I took to desperation:

Only the next day to be feeling optimistic:

Finally, at the end of June, I reached an understanding with the maintainers on an implementation of customized builtins that would be merged and published.

However, that implementation is pretty tough. It requires patching the polyfill to only support customized builtins via an extra script tag. I do not know how to start such an implementation, since observing DOM changes to call custom element lifecycle methods is something that only the first script should do and would be hard for the second script to hook into.

I’m not sure how to pull it off; and, for a while, was frankly burned out by all the seemingly wasted effort.

The endeavor has stalled from June 2017 to present.

Even more rejection

But it didn’t stop there.

In January 2017, I attempted to overcome another hurdle: interop with React, AngularJS, and other frameworks.

This was not a problem I expected to have. The whole point of custom elements was that they were native to the browser, which means they can be used by any framework. Right?

However, it is not that simple. Frameworks need to pass data into custom elements, and they can pass things either as html attributes or as javascript properties. On the one hand, html attributes can only be strings, but they make things more readable when you Inspect Element. On the other hand, javascript properties can be strings, numbers, objects, or any data type, but they do not show up in the DOM inspector.

Currently, React’s documentation for web component interop is very incomplete. Discussion in the issue queues taught me that react always passes data down as string html attributes. There is no way to pass other data types to custom elements, without using refs. And if you want to get data back from the custom elements (usually done via custom events fired on the dom node), you’re stuck with refs as well.

And using refs for every button and input sounded awful.

So I discussed the problem in this ongoing github issue until I thought a consensus had been reached. Sebastian Markbage was the React core team member leading the discussion. I pull requested React with a solution for attributes vs properties.

For (probably good) reasons, my pull request was rejected.

I wasn’t sure where to go from there — using refs for every custom element was inhibitive. Not having polyfill support for customized builtin elements was discouraging. Not to mention that writing a custom element at all was turning out to be challenging.

Things were not going well.

Success

But, with enough effort, we made progress:

  • To solve our customized builtins problem, I decided to publish my fork of the custom elements polyfill to npm (yarn add @joeldenning/customelements). We have been using it in production for over six months with no major bugs.
  • For React interop, I wrote a generic wrapper around custom elements that allowed us to use custom elements without refs.
  • Unfortunately, React itself had a bug that prevented the use of customized builtin-elements at all (even with refs). This was due to a change in the custom element spec before the 1.0 release. So I put together a fix that was merged and published in React 16.
  • For AngularJS interop, I wrote a directive with limited, but maybe-easy-to-use custom elements support.
  • For actually implementing the custom elements themselves, I wrote some code to transform preact components into custom elements. The idea was that our team, which already had expertise in React, wouldn’t have to learn all the nitty gritties when writing a custom element.
  • We set up automated, cross-browser tests (via saucelabs) that allow us to test the custom elements in IE11, Safari, Edge, Chrome, and Firefox.

All of this was launched to customers in about March 2017 and has been working fine!

Cold, hard realities

But, alas, the honeymoon phase of a relationship inevitably ends.

It turns out that even after all my work, there is still a pretty steep learning curve for our developers to write their own custom elements. You know things are getting complex when you have to write a wiki explaining why things are so damn complicated.

And even that is not enough.

Our team still struggles to grok everything and get it all working together. Our preact to custom element transformation is not seamless and we still have to deal with weird things.

For example, passing “children” to a custom element is very murky. Should the children be created as dom elements inside of the custom element? If so, what happens if the custom element wants to create some children elements of its own? Will that throw off React’s reconciliation algorithm because there are DOM nodes popping up that React didn’t create?

Another example is that Preact might not be the best fit for writing custom elements. The render function on Preact components is meant for creating and updating dom elements, but when writing a custom element you many times are only updating the dom element without creating it. Because of this, the render method of many of our Preact custom elements returns null and only makes updates to the custom elements in the other lifecycle methods. All of this is sort of a weird pattern that isn’t very natural for React/Preact developers.

Also, we had to resort to setTimeout(() => {...}, 50) in our testing code. The problem is that the custom elements polyfill doesn’t (or, at least, didn’t) provide a way to know when the polyfill was finished doing all the custom element things. So if you do customButtonElement.actionType = 'flat', there is no way to know in your tests when the custom element has had proper time to respond to the change and update the button to be a flat button. Throw in some socket connections from your build server to SauceLabs and you’ve got some gnarly race conditions. Waiting 50ms is the best workaround we have right now.

Finally, just last week, I found yet another bug with React, related to customized builtin elements that extend native input elements. This completely prevents <input> from being extended via <input is="my-input" /> . I haven’t even taken the time to report this one to the React team yet, but hope to do so in the next week or so.

All of these pains, along with a few others, have consistently popped up.

Uncertain future

So what now?

As a company that wishes to have a polyglot of javascript frameworks that all follow a consistent styleguide, it is hard to know what to do.

Maybe in the next few months, I’ll revive my efforts to bring customized builtins to the official polyfill. And maybe we’ll swap out our preact-to-custom-element layer for something with a community behind it, like SkateJS.

Or maybe not?

Although trying to reinvigorate life into the polyfill pull request would be cool, our forked version is already working for us. The initial work was a tremendous amount of effort, but now we have it. So there’s not much urgency to move back to the official version.

Additionally, the ongoing maintenance for our preact-to-custom-elements thing is not overwhelming. It’s not ideal, but it is working for us.

Or another option could be to ditch custom elements altogether in favor of something easier. However, I don’t know of any such thing — custom elements are the most common answer to the “share components across frameworks” problem. Maybe single-spa parcels?? Exciting, but maybe too unproven and drinking-our-own-kool-aid to bet on.

Conclusion

So I think you get the picture…we don’t know where Canopy will end up. The teen drama is ongoing and open ended.

We have tried hard, and the results are mixed.

We’ll stick with custom elements as long as we find it useful and worth it. And perhaps one day custom elements will be second nature for the javascript world. Or maybe Canopy’s experience is representative of how things have been for years with custom elements, and how they might be for years to come.

It is impossible to know, but will be exciting to find out.

Development at Canopy Tax

Our amazing software is made possible because of our ridiculously talented tech team. Check out their latest work and see how you can join the best talent in the state.

Joel Denning

Written by

@Joelbdenning

Development at Canopy Tax

Our amazing software is made possible because of our ridiculously talented tech team. Check out their latest work and see how you can join the best talent in the state.