Custom HTML elements and technologies that faciliate their development
Definition
Components on the Web
Any reusable blob of HTML, CSS, and JS
When we are generally talking about components in web development,
that includes web components, but also abstractions that various frameworks provide
that predate web components,
or even manually re-using HTML syntax that has certain CSS and JS applied to it
(e.g. `<div class="pie-chart" style="--p: 80%></div>`).
Web Components
Reactivity
Encapsulation
Modularity
Reusability
- Reactive: Just like regular HTML elements, you can change attributes and behavior adapts automatically
- Encapsulation: Implementation is hidden from the rest of the page
- Modularity: Developed independently, testable independently
- Reusability: after creating component once, use in many places
In recent years, a new architecture is emerging: instead of separating by concerns,
separating by self-contained, independent, reusable components.
However, [both can coexist](https://adactio.com/journal/14103): one can separate by component, and then separate concerns within each component.
Two types of components
General purpose
App-specific
Vue.js Components
Component Defintion
exportdefault ButtonCounter ={data(){return{count:0}},template:`
<button @click="count++">
You clicked me {{ count }} times.
</button>`template:'#my-template'}
Component is a reusable Vue.js instance
takes same arguments as normal instance
data: computed: watch:, etc.
new template: argument specifies component HTML
alternatively, put template in HTML, give its ID to template:
Using Components
app.components = {ButtonCounter}
…
<h1>Here is my component</h1><button-counter></button-counter>
register your component with the app that will use it
you get a custom tag name you can use to place the component
html is case-insensitive so use kebab-case (hyphens not capital letters) for the tag name
Passing "Arguments"
BlogPost = {
props: ['title']
template: `<h4>{{ title }}</h4>`
}
…
<blog-posttitle="My First Post"><blog-post:title="composition.title">
specify props: in component definition
they become custom attributes of the custom element
and also properties of the component
use in template as {{ title }}
and in your js via this.title
don't change them! intended for parent to child, not vice versa
Content for Components
<FancyButton><spanstyle="color:red">Click me!</span><AwesomeIconname="plus"/></FancyButton><template><buttonclass="fancy-btn"><slot><!-- slot outlet -->Click Me <!-- default value --></slot></button></template>
<slot> specifies where in the template the contents of the custom tag should go
can include arbitrary html and other custom tags (components)
slot can contain default content for when none is provided
The tag name *must* contain a hyphen. Tag names with hyphens are guaranteed to never clash with any future native HTML elements.
Here we have created a rudimentary custom element that shows how many times it has been clicked.
We create custom elements by extending the `HTMLElement` class, and then calling `customElements.define()` to associate the class with a tag name.
Here we have only defined a private method for updating the element's rendering
so the actual API of the component to other developers appears like a regular HTML element.
However, we could also expose methods, accessors, or properties that may be useful.
When subclassing HTMLElement, there are special method names that do certain things for us.
Here, we are rendering the element and attaching its event handler when it actually gets attached to a DOM tree,
not when it's created. This is the recommended way for using any DOM methods on the element.
The element never gets connected to a DOM tree, and thus, never gets disconnected from it.
What gets logged?
let a = document.createElement("div");let b = document.createElement("my-element");
a.append(b);
"connected""disconnected"
Nothing
"disconnected"
"connected"
The element is appeneded to another element, but they are still not connected.
What gets logged?
let a = document.createElement("my-element");
document.body.append(a);
"connected""disconnected"
Nothing
"disconnected"
"connected"
The element is appeneded to another element, but they are still not connected.
What gets logged?
<my-elementid="my_element_1"></my-element>
my_element_1.remove();
"connected""disconnected"
Nothing
"disconnected"
"connected"
The element is appeneded to another element, but they are still not connected.
Attributes in HTML elements
<inputid="spinner"type="number"min="5">
> spinner.min
< '5'
> spinner.min = 4
> spinner.outerHTML
< '<input id="spinner" type="number" min="4">'
> spinner.setAttribute("min", 3)
> spinner.min
< '3'
Most HTML attributes are *reflected* as JS properties.
This is a two way binding: every time the property is updated, the attribute updates and vice versa.
In fact, in the few cases where it doesn't work as a two way binding it can be a huge source of confusion.
An example that has confused thousands of developers is the `checked` attribute/property of checkboxes.
Instead of being in sync, the HTML attribute represents a different thing: whether the checkbox is *initially* checked.
Since implementing reactive attributes-proeprties in vanilla JS involves so much boilerplate,
there is a proliferation of libraries to make this easier.
One of the most popular right now is [Lit](https://lit.dev/), from Google
and it provides a number of other conveniences too.
This is a single `<video` element, right?
But then how are all these controls rendered?
Let’s find out by inspecting!
Definition
Shadow DOM
DOM subtree that is rendered but invisible to DOM methods
Shadow host: The regular DOM node that the shadow DOM is attached to.
Shadow tree: The DOM tree inside the shadow DOM.
Shadow root: The root node of the shadow tree.
Class hierarchy
ShadowRoot
DocumentFragment
Node
EventTarget
Object
Why do we need that?
Shadow DOM encapsulation
Style isolation
Local ids, classes, all identifiers
DOM methods localized to shadow root
Creating shadows
> let shadowRoot1 = element1.attachShadow({mode: "open"})
> shadowRoot1
< #shadow-root (open)
> element1.shadowRoot
< #shadow-root (open)
> let shadowRoot2 = element2.attachShadow({mode: "closed"})
> shadowRoot2
< #shadow-root (closed)
> element2.shadowRoot
< null
- We create shadow trees by using the [`element.attachShadow()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow) method
- Shadows can be open or closed. Open shadow trees can still be accessed from the outside, using the `shadowRoot` property on their shadow host.
Closed shadow trees are even more encapsulated and cannot be accessed at all except through the return value of the `attachShadow()` method at the time of their creation.
- Closed shadow trees closer resemble the kinds of shadow trees browsers use internally
- Common practice for web components is open trees and thus we are going to only use open trees from now on.
- The shadow tree replaces element content entirely
- To insert the actual element content somewhere in the shadow tree, we use the [`<slot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) element
- Often, we want to place different elements in different places of the shadow tree.
The [`<slot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) element
accepts a `name` attribute as well for this very thing.
- We then use the `slot` attribute on the light DOM elements we want to assign to these named slots.
- We can still use an unnamed `<slot>` element as a catch-all for all elements without a `slot` attribute, as well as non-element nodes.
- If we specify `slot="nonexistent"` on a light DOM element, it's just hidden
- If we assign multiple elements to the same slot, they are all slotted in order.
Style encapsulation
this.shadowRoot.innerHTML =`<style>
p {
margin: 0;
}
</style>`;
- Most WCs need to include CSS as well to style their internals
- You can include this CSS in the shadow DOM, so that it cannot affect the rest of the page
- This way you can also use very loose selectors (e.g. `input`) without worrying about clashing with author styles
- Prefer to keep your CSS separate? Look into [`adoptedStylesheets`](https://dev.to/westbrook/why-would-anyone-use-constructible-stylesheets-anyways-19ng)
- Shadow DOM can also help us encapsulate and reuse styles that are not visible to the outside page
- Note that these styles cannot refer to elements outside the shadow host.
- The shadow host itself can be matched via the special pseudo-class [`:host`](https://developer.mozilla.org/en-US/docs/Web/CSS/:host)
- Inherited properties pass through shadow DOM boundaries
- Custom properties are inherited properties, so they can be used to customize encapsulated styles
Composition
It’s shadow roots all the way down!
Observers
Also note that we used a separate variable for the styles here as an attempt to keep them separate.
Observers
“Do something when something changes”
More lightweight than events
Useful Observers
- **[`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)**: React to size changes
- **[`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)**: React to DOM changes
- **[`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)**: React to changes in the relative position of elements
For pre-selecting, attribute on tab container, on tab, or on tab panel?
Order-dependent or linked by name?
WWABD? (What would a browser do?)
Design web components that could plausibly be native HTML elements.
Follow established conventions and patterns.
Sometimes this is not easy, as the Web Platform is not very consistent with itself.
In that case, look at the majority syntax, and place more emphasis on newer syntax.
Rule of thumb
Do not modify the element’s light DOM (unnecessarily)
- If I inspect a custom element right after it's initialized (before any interaction), I should basically see the same HTML I wrote.
- If you need to create new elements, create them in the shadow DOM
- Children created by your element are part of its implementation and should be private.
Without the protection of a shadow root, outside JavaScript may inadvertently interfere with these children.
- If you need to add or modify attributes, consider doing so on a shadow DOM element instead.
- `class` and other global attributes are for users of your component, do not mess with them, ever
- Elements that need to express their state should do so using attributes. The class attribute is generally considered to be owned by the developer using your element, and writing to it yourself may inadvertently stomp on developer classes.
- If you need to communicate state, use attributes, not classes
- It's ok to update attributes to reflect changed state (e.g. think of the native `<details open>`)
Rule of thumb
Do not modify the DOM of the host document
- If you need to inject CSS, do so in your element’s shadow DOM, not by inserting `<style>` elements to the host pagez!
- Use CSS properties and `::part()` for styling, not attributes
Current Limitations
No way to react to style changes
No conditionals
- Since CSS does not yet support conditionals on custom properties, you may need to make concessions wrt the syntax of your custom properties
e.g. use numbers instead of nice readable keywords. This will change soon.