Think way back to when you first started learning HTML. Do you remember the first form you built? Do you remember the first time you used the readonly attribute or discovered that adding the required attribute to an <input> would make the field required? Most importantly, do you remember how you discovered those attributes? For me it was Google, W3Schools (it shames me to say) and Mozilla Developer Network (MDN)— and occasionally the W3C standards and drafts.
In a world where there are as many front end frameworks as there are cats you can fit in a bag, it’s becoming increasingly common to mix styles of code and frameworks. X Library uses a slightly different module pattern than Y library and/or similar dependency libraries, etc. — hence, the learning curve. You never use a framework right out of the box. You have to learn what tools the framework provides and figure out how they can solve your problems.
Let’s compare frameworks to brick houses; they’re built using lots of smaller pieces and tools. On some projects you’ll need each and every last brick to complete the house. On others, you’ll need only a portion of that. Why buy 1,000 bricks if you only need 100? If you could create your own brick factory and fulfill custom orders instead of always ordering 1,000 bricks, wouldn’t you? Your end cost would be significantly lower. Web Components are essentially our brick/element factory. We acknowledge that large frameworks will always have a place in our projects, but having a base set of elements that will work with any framework lowers the cost of each project they’re used in.
Why Web Components?
Every framework and library has its own way of defining components, controllers and views. The end product that the browser consumes is HTML. This is why at Ontraport, when it comes to custom elements, we closely follow the APIs that are shipped with the native elements.
We do this for a couple of reasons:
- HTML is the lowest common denominator and industry standard for describing a web document and or application.
- When you get hired, regardless of what your framework of preference is, we hope you know HTML.
- It’s less of a learning curve (on both ends – developer and consumer) to follow what is already in place. For example, using <h1 disabled> over <h1 noclicky/> – in HTML “disabled” is already a familiar keyword, whereas “noclicky” isn’t found anywhere in the HTML spec.
- It allows us to create a “portable” API, meaning if you are used to disabling a <input> via adding the disabled attribute to the element, you should be able to do that with all of the elements.
That said, using the Web Component APIs does mean that we are avoiding some of the issues that come along with use of a framework. Here are some of the caveats that we have found and how we addressed them.
Attribute(s) or Method(s)?
This one has come up quite frequently. In my experience, traditional JavaScript frameworks provide functionality via methods in the component/model layer. When working with traditional HTML elements, most of the functionality is accessed via manipulation of the element attributes. When developing your own elements, it’s hard to know when a desired action should be controlled via a method or an attribute. I wouldn’t say it’s one or the other; an element can have both. We’ve identified that there are two types of attributes. Here’s how we break them down:
- Stateless – An attribute that doesn’t have a value attached to it. Think opened, required, hidden, disabled. Another way to look at them is calling them boolean attributes.
- Stateful – An attribute that always has a value associated with it. Think step=”3″. Stateful attributes are candidates for having a method to help manage its state. Consider the following code.
elementPrototype.next = function(){
this.setAttribute( ‘step’, 1*this.getAttribute(‘step’) + 1 )
}
elementPrototype.prev = function() {
this.setAttribute( ‘step’, 1*this.getAttribute(‘step’) – 1 )
}
Here, we have defined next and prev helper methods to increment/decrement the value of the step attribute. This makes it much easier to interact with the element. It’s self documenting: it highlights the fact that incrementing/decrementing the step attribute is a core piece of the functionality of this element. Even with the next/prev methods, we haven’t pigeon-holed ourselves into an unfriendly API; we still can pull the value of the step attribute and add 2 to it.
In both cases they could be controlled via CSS classes, but using attributes allows us to create a more cohesive HTML, CSS and JavaScript API. For example, now you can use:
op-element[opened] {
display: block
}
Or
op-element[step=1] {
color: red
}
Notice the CSS and Javascript are working together to control the state of the element.
Developing a Standard Set of Attribute Names
Here’s our current list of naming attributes:
- opened – overlay
- modal – overlay
- autoclosedisabled – overlay
- templated – op-element, used to fix the double insertion of templates for browsers that use the webcomponentsjs
- inert
- disabled – op-element – standard with styling
- hidden – op-element
- step – op foil deck
- rotate – icon – number degrees that the icon is rotated
- icon – what icon to use
- minimize – hide flake
- noback – hide the back button in flake
- maximize – full size in flake
If I was to refactor this list, I would combine opened and maximize and kill off autoclosedisabled. Opened and maximize have caused some confusion as they have different nomenclature but a similar function. So what it boils down to in the end is brutal consistency when naming your attributes. Just because it feels more right for one element doesn’t mean that there’s a good case in making a new attribute. You’ll regretfully end up convoluting the collective API of your elements (assuming you are developing a collection of them, not just one).
Rule of 3 API: Alteration
There is something to be said about creating a new HTML element to play with — either for use with your existing framework or by itself — and then discovering that you initially did not understand the problem you were trying to solve when you created the element. This is why at Ontraport we try to get through 3 API iterations for every element. This idea coincides with one of our core engineering principles that our Senior Principal Architect, Steven Schneider, preaches: “If you have to do something three or more times, turn it into a method.”
This works for two reasons.
One, the first go is seldom perfect. The second time around you have a way better handle on what you are dealing with. The third time around, however, you’re polishing what you’ve already done.
Secondly, this model works assuming that your elements/units are small. The most “complex” element we have is only 400 lines of HTML, CSS and JavaScript. You can read it in a very short period of time. Making changes to it is often pretty trivial.
For example, the op-button element went through four major API iterations. We:
- Added styles for <a> and then removed them.
- Added and removed a method to return the element name. This was used at one point to help template the element and polyfill the styles.
- Removed the use of the shadow root. It turns out it wasn’t needed for this element. This led to reworking the CSS and the base styles.
- Fixed the key code handler to listen for the right keys.
All of these changes occurred over a period of two months and were only discovered after the element was in a staging environment and other developers started playing with the element and its API. Interestingly, the current state of the element didn’t veer off too far from the original tech spec.
Events
Emitting events is a core part of writing custom elements. At first I designed the event names to be generic, but after a couple of elements I started prefixing them with element names. As of the publication of this article, this is the list of all of the events currently in play:
- minimize – op-flake
- maximize – op-flake
- home – op-flake
- back – op-flake
- close – op-flake
- select – op foil deck
- op-dialog-dismiss
- op-dialog-confirm
- op-overlay-opened
- op-overlay-closed
Although the non-prefixed events save you keystrokes when binding to them, they help differentiate themselves from the native events. I strongly recommend prefixing your events with the element name they originate from. This helps enforce the idea that your element is a first class one. Also, if you leverage/try to use some of the native element names for custom functionally, they might not bubble up due to the shadow root.
Portability
This is different than the portable API I mentioned in the first set of bullet points. We view vanillaJS Web Components as the first set of building bricks used to create any project. This allows the lead developer implementing the project to pick any framework to use as the mortar, giving you the flexibility to quickly complete projects with different requirements while still being able to use familiar components.
But what about browser support? Don’t you need polyfills for 100% coverage? And don’t the polyfills have dependencies? Yes, they do, but you would be surprised at the native support for the APIs that the polyfills use. At Ontraport we are currently only using HTML Imports, the custom element APIs and the template element. As of the publication of this article, we have great native support for the browsers that need the polyfills. Here is a quick run down of the APIs needed to fill in support:
API | Native Support | Dependencies – Native Support |
WeakMap | 65% |
|
MutationObserver | 86% |
|
window.requestAnimationFrame | 88% | |
event.preventDefault | * | |
Custom/Event constructor | 96% |
* No support in Internet Explorer, affects all versions
Based on the info in the table, we feel very confident that support for the polyfills will not be dropped in the near future.
Ship a Support System
Just like any software project, documentation is key, but I feel in this case it needs to fit into the natural way we have learned how to use HTML. It needs to fit into your development workflow.
- Need a centralized location for the docs and demos
- Easy to search, just like you found out about readonly, disabled and required
- Not a unique problem to Web Components
- Post the tech spec template for Web Components
Using custom elements is a first step to break away from giant monolithic frameworks that will allow us to use our core UI components across all projects. If we try to view our custom elements as native elements, I believe they’ll have a longer shelf life than any of the other components built with other frameworks. This provides us with a solid foundation of bricks with which to build future projects at Ontraport.