Lecture 23 Component-driven development

Building UI tabs should be easy!

Web Components

[Shoelace Tab Panel](https://shoelace.style/components/tab-panel)

Definition Web Components

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

- 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


			export default ButtonCounter = {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
			</button>`

  template: '#my-template'
				
}
		

Using Components


			app.components = {ButtonCounter}

      …

			<h1>Here is my component</h1>
			    <button-counter></button-counter>
			
		

Passing "Arguments"


				BlogPost = {
				props: ['title']
				template: `<h4>{{ title }}</h4>`
				}

				…

				<blog-post title="My First Post">
				<blog-post :title="composition.title">
		

Content for Components


 <FancyButton>
   <span style="color:red">Click me!</span>
   <AwesomeIcon name="plus" />
 </FancyButton>

 <template>
   <button class="fancy-btn">
     <slot>
     <!-- slot outlet -->
      Click Me    <!-- default value -->
     </slot>
   </button>
 </template>
 

HTML Custom Elements

An HTML Custom element consists of…

A JS class

			class MyElement extends HTMLElement {
				constructor() {
					super();
				}
			}
		
+
A tag name

			// Register <my-element>
			customElements.define("my-element", MyElement);
		
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.

Connectedness

Connected Not connected

			class MyElement extends HTMLElement {
				constructor() { super(); }
				connectedCallback() {
					console.log("connected");
				}
				disconnectedCallback() {
					console.log("disconnected");
				}
			}
			customElements.define("my-element", MyElement);
		

What gets logged?


			document.createElement("my-element");
		
  1. "connected" "disconnected"
  2. Nothing
  3. "disconnected"
  4. "connected"
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);
		
  1. "connected" "disconnected"
  2. Nothing
  3. "disconnected"
  4. "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);
		
  1. "connected" "disconnected"
  2. Nothing
  3. "disconnected"
  4. "connected"
The element is appeneded to another element, but they are still not connected.

What gets logged?

<my-element id="my_element_1"></my-element>      
my_element_1.remove();   
  1. "connected" "disconnected"
  2. Nothing
  3. "disconnected"
  4. "connected"
The element is appeneded to another element, but they are still not connected.

Attributes in HTML elements

<input id="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.

When attribute-property reflection is off…

<input id="checkbox" type="checkbox" checked>
> checkbox.checked
< true
> checkbox.checked = false;
> checkbox.outerHTML
< '<input id="checkbox" type="checkbox" checked="">'
> checkbox.checked = true; checkbox.removeAttribute("checked");
> checkbox.checked
< true
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.

Implementing a reflected attribute-property

<click-counter init="3"></click-counter>     
counter.init = 3;   

Implementing a reflected attribute-property


			class ClickCounter extends HTMLElement {
				constructor() { super(); }
				#init = 0;

				get init() { return this.#init; }
				set init(v) { this.#init = v; this.setAttribute("init", v); }

				static get observedAttributes() { return ["init"] }

				attributeChangedCallback(name, oldValue, newValue) {
					if (name === "init") this.#init = newValue;
				}
			}
		

Implementing a reflected attribute-property using the Lit library


			import {html, css, LitElement} from
			"https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js";

			class ClickCounter extends LitElement {
				constructor() { super(); }

				static properties = {
					init: {type: Number},
				};
			}
		
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.

Encapsulation: Shadow DOM

<video src="videos/war-kitten.mp4" controls></video>
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

Class hierarchy

Why do we need that?

Shadow DOM encapsulation

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

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

HTML Design Principles

Many ways to skin a catfish…

' About Contact

About content

Contact content

Generic-tabs

Shoelace tabs

Elix tabs

Some design decisions

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!

Rule of thumb
CSS, not attributes for presentation

- Use CSS properties and `::part()` for styling, not attributes

Current Limitations

- 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.

Attributes are for primitive data

Attributes are for primitive data

Attributes for metadata, child elements for UI-facing text

Elements can contain formatting, and are localizable (accept `lang` and `dir` attributes).

Hands-on practice!

designftw.mit.edu/go/inclass