Lecture overview¶
- Project reminder
- Assignment 4 + optional Assignment 5
- UI Libraries
- Web Components
Project¶
- Deliverables
- GUI Analysis (1-3 points)
- Specification (1-3 points)
- Implementation Pass/Fail + optional bonus points (+1-3 points)
- Point requirements for grades
- A: minimum 6 points
- B: minimum 5 points
- C: minimum 4 points
- D: minimum 3 points
- E: minimum 2 points
Optional project points for implementation¶
- +1 point: use 1 web component that you built. Ok if it is non-interactive.
- +2 points: use 2 web components that you built. At least 1 should be interactive
- +3 points: use 3 web components that you built. At least 2 should be interactive
- Use of ARIA attributes not required.
- See end of lecture for info about ARIA attributes.
- The Web Component(s) must be used in at least 2 places with different content in each place.
- Web components you built as part of Assignment 4 do not count.
Assignment 4¶
- Build your own UI components using the Web Components standard
- ARIA attributes
Optional Assignment 5¶
- Required for higher grades on LAB1 (A, B).
- Done individually, i.e. not in your regular lab pair.
- Choose to complete 1 or 2 exercises:
- 1 exercise: 2 points
- 2 exercises: 3 points
- Exercises
- Radiobutton web component.
- Accordion Group web component.
- Tab Group web component.
UI Libraries
libraries that provide custom UI components
Common approaches to UI components using HTML, CSS, JS¶
- Traditional approach
- Component content in traditional HTML (native HTML elements like
<div>and<span>with classes/ids). - Link to UI library CSS from HTML.
- Link to UI library JS from HTML.
- Component content in traditional HTML (native HTML elements like
- UI library JavaScript transforms "static" content into interactive content.
- Automatic e.g. using special classnames.
- Manual, by calling a "create"-function on specific elements.
- Essentially an extension of what we have been doing in Assignments 2 and 3.
Accordion, using traditional UI components¶
- Bootstrap
- Write HTML with correct structure, use CSS classes
- https://getbootstrap.com/docs/5.1/components/accordion/
- https://www.ida.liu.se/~729G87/course-material/lectures/examples/uicomponents/bootstrap/accordion.html
- jQuery UI
- Write HTML with correct structure, run accordion function.
- https://jqueryui.com/accordion/
- https://www.ida.liu.se/~729G87/course-material/lectures/examples/uicomponents/jqueryui/accordion.html
- UIKit
- Write HTML with correct structure, use CSS classes
- https://getuikit.com/docs/accordion
- https://www.ida.liu.se/~729G87/course-material/lectures/examples/uicomponents/uikit/accordion.html
Common approaches to UI components using HTML, CSS, JS¶
- Web Components Standard
- Component represented by a new HTML tag for a fully encapsulated element with built-in functionality.
- Link to UI library JS from HTML.
- Link to UI library CSS from HTML (often some defaults are embedded in JS).
- Use new HTML tags provided by UI library
- Abstracts away some of the complexity inherent to working with the DOM through three separate systems at the same time.
Web Components¶
- Really badly named since frameworks often have their own "components" which might, or might not, work on similar principles.
- (Makes Googling difficult)
- Similar to modern UI Libraries like React, Svelte, Vue, or Angular but without buy-ins, opinions or back-end requirements.
- Use of the open standards for the Custom Elements API and the shadow DOM.
- (much easier to Google these phrases)
- Encapsulate HTML, CSS and JavaScript in a custom element that can be used in your HTML code.
Accordion, as implemented by some Web Component libraries¶
- Shoelace
- Elix
- PatternFly Elements
What about React, Vue, Angular, etc?¶
- Full frameworks, not just UI libraries.
- Frameworks are meant to be complete ecosystems.
- Frameworks do have large UI libraries, but these are usually built using the traditional method.
- While some frameworks are built around the concept of defining Web Components, the major ones aren't.
- Web Components are just that, individual components.
- Many frameworks are starting to use some Web Components where applicable.
Web Components pros and cons¶
Pros:¶
- Reusable: Not tied to a specific context or use-case.
- Extensible: Can be combined without complex frameworks.
- Replaceable: Drop-in replacements are simple.
- Independent: Doesn't rely on other components or frameworks.
- Isolated: Problems with one component can't easily cause problems in other components.
- Open/Libre/Free: Based on open web standards set by the W3C.
- Maintainable?
Cons:¶
- Bottlenecked: Relies on the DOM and Events for all communication in or out.
- Limited: Can only be expressed through elements, attributes and properties.
- Non-optimized: Can have larger overheads compared to a monolithic framework since more separate scripts need to be downloaded and executed.
- Fragmented: Independent components are nice but managing one dependency per component compared to a dependency on a single framework also has costs.
- Dependency hell?
Web Components
Creating custom elements with encapsulated style and DOM (the shadow DOM)
Web Components¶
- Use of the Custom Elements API
- Encapsulate HTML, CSS and JavaScript in a custom elements that can be used in your HTML code
- Examples
- a
<slide-show>component that you can use to create a slide-show - a
<user-card>component that you can use to show user information - a
<product-card>component that you can use to show product info
- a
Building blocks of Web Components¶
- Custom Elements API: Used to define new elements that can be used in HTML.
- Shadow DOM: A separate DOM with its own styles (CSS) and behavior (JS) that we can attach to a custom element - CSS and JS from the main DOM (the regular DOM of the web page) does not directly affect the shadow DOM!
- You can style a custom component from the outside to a certain degree. CSS properties that are inherited will still apply inside the shadow DOM, and CSS variables that apply inside the shadow DOM can be set in the main DOM.
- You can control the behavior of things inside the shadow DOM by passing the correct Events.
- Both of the above requires that the Web Component is built with this in mind.
- HTML templates: A special element type that is not rendered in the browser but can be cloned and used as a template for your custom element.
Defining custom elements¶
Custom elements - defining new HTML tags¶
- We define a custom JavaScript class that extends an existing class, e.g.
HTMLElement,HTMLParagraphElement, etc. - Option to define special life cycle methods used at certain stages of the element's life cycle.
- We use that custom class and define a new tag to use in our HTML code.
Classes in JavaScript (ES6+)¶
- Template for creating objects with the
classkeyword. - We can extend existing classes to inherit properties from existing classes using
extends. - The
constructor()method is run when a new object using the class is created.super()is used to call the constructor of the parent class.
- Different kinds of methods can be added to a class e.g.
getmethods that are run when properties are to be readsetmethods that are run when properties are set- "ordinary" methods
- We use
thisto refer to the current instance in our methods.- Differs from
selfin Python because it is a keyword and has slightly different semantics depending on where it is used.
- Differs from
Example: Class declaration
class Rectangle {
constructor(height, width) {
console.log("Constructor for Rectangle called.");
this.height = height;
this.width = width;
this._color = null;
}
// getter for _color property
get color() {
return this._color;
}
// setter for color property
set color(value) {
console.log("Setter for color property called.");
if (["red", "blue", "green"].includes(value)) {
this._color = value;
} else {
console.log("ERROR: Bad color!");
}
}
// getter for area property
get area() {
console.log("Getter for area property called.");
// NOTE: use this.methodname() to call a method
// defined in the class
return this.calcArea();
}
// method for calculating the area
calcArea() {
console.log("calcArea() method called.");
return this.height * this.width;
}
}
const square = new Rectangle(10, 10);
console.log(`width: ${square.width}`); // 10
console.log(`area: ${square.area}`); // 100
console.log(`initial color: ${square.color}`); // null
square.color = "magenta";
console.log(`color magenta?: ${square.color}`); // null
square.color = "red";
console.log(`color red?: ${square.color}`); // null
In [2]:
class Rectangle {
constructor(height, width) {
console.log("Constructor for Rectangle called.");
this.height = height;
this.width = width;
this._color = null;
}
// getter for _color property
get color() {
return this._color;
}
// setter for color property
set color(value) {
console.log("Setter for color property called.");
if (["red", "blue", "green"].includes(value)) {
this._color = value;
} else {
console.log("ERROR: Bad color!");
}
}
// getter for area property
get area() {
console.log("Getter for area property called.");
// NOTE: use this.methodname() to call a method
// defined in the class
return this.calcArea();
}
// method for calculating the area
calcArea() {
console.log("calcArea() method called.");
return this.height * this.width;
}
}
const square = new Rectangle(10, 10);
console.log(`width: ${square.width}`); // 10
console.log(`area: ${square.area}`); // 100
console.log(`initial color: ${square.color}`); // null
square.color = "magenta";
console.log(`color magenta?: ${square.color}`); // null
square.color = "red";
console.log(`color red?: ${square.color}`); // null
Constructor for Rectangle called. width: 10 Getter for area property called. calcArea() method called. area: 100 initial color: null Setter for color property called. ERROR: Bad color! color magenta?: null Setter for color property called. color red?: red
Define custom element using a class¶
class MyElement extends HTMLElement {
constructor() {
super();
// more code for initialization etc
}
// Example lifecycle method
// This method that is called the custom element is added to the main DOM
connectedCallback() {
// this is where you add event listeners to elements in the shadow DOM
}
// rest of class code goes here, i.e. various method definitions
}
// REQUIRED: use kebab-case for the name of the custom element
customElements.define("my-element", MyElement);
// We can now use the custom element by writing <my-element></my-element> in our
// HTML code.
Special lifecycle callback methods for custom element classes¶
- There are special methods that you can optionally define in your custom element class
- The methods are called at specific events during an elements lifecycle. E.g.:
connectedCallback: called when the the custom element is added to the DOMdisconnectedCallback: called when the element is removed from the DOMadoptedCallback: called each time the element is moved to a new DOMattributeChangedCallback: called when specified attributes change (see documentation for details)
Adding a shadow DOM¶
Purpose of a shadow DOM¶
- Hide elements inside of web component from the main DOM.
- Prevent styles from main DOM from affecting elements inside web component.
- Prevent styles from web component from affecting elements outside web component.
- I.e. Encapsulation and Isolation
The shadow DOM¶
(Image from https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM)
Using a shadow DOM¶
- Create a shadow DOM in your class using
this.attachShadow({mode: 'open'})
{mode: 'open'}allows you to inspect a shadow DOM in the browser- This adds a
shadowRootproperty to your instance which will point to its shadow DOM "document" i.e.this.shadowRoot - We can use
this.shadowRootlike we would use document, e.g.
this.shadowRoot.querySelectorAll("div")
this.shadowRoot.appendChild(newElement)
Class that adds a shadow DOM¶
class MyElement extends HTMLElement {
constructor() {
super();
// create a shadow DOM for the custom element
// {mode: 'open'} allows it to be inspected in the browser
this.attachShadow({ mode: "open" });
}
// rest of class code goes here, i.e. various method definitions
}
// REQUIRED: use kebab-case for the name of the custom element
customElements.define("my-element", MyElement);
// We can now use the custom element by writing <my-element></my-element> in our
// HTML code.
Using <template> to populate a shadow DOM¶
HTML Templates¶
- The
<template>elements are not rendered in the browser. - We can define a
<template>, then clone it and use its contents for our shadow DOM. - The
<template>can be defined in our main HTML page (i.e. in the main DOM)... - But we usually create it in our JavaScript file with our class to keep everything for the web component contained in one JS file.
Cloning our <template> and adding its contents to a shadow DOM¶
- Create/Attach shadow DOM.
{mode: 'open'}allows it to be inspected in browser
this.attachShadow({mode: 'open'});
- Clone template and add contents to shadow DOM. Use the argument
trueto make a deep copy and also clone contents
this.shadowRoot.appendChild(template.content.cloneNode(true));
JS-file with template, class & attached shadow DOM¶
const template = document.createElement("template");
template.innerHTML = `
<style>
/* CSS rules for the component */
</style>
<div>
<p>This is a template</p>
</div>
`;
class MyElement extends HTMLElement {
constructor() {
super();
// create a shadow DOM for the custom element.
// {mode: 'open'} allows it to be inspected
this.attachShadow({ mode: "open" });
// put a clone (true == deep copy) of our template into the shadow DOM
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
// rest of class code goes here, i.e. various method definitions
}
// define custom component (requires use of kebab-case)
window.customElements.define("my-element", MyElement);
Sidenote: Syntax highlighting of template strings¶
- No syntax highlighting of template strings using default settings in VS Code (or most editors AFAIK).
- Extensions can add this functionality.
- For VS Code, e.g. Web component HTML formatter and highlighter
template.innerHTML = `
<style>
/* CSS rules for the component: */
p {
color: #ffffff;
}
</style>
<!-- HTML content for the component: -->
<div>
<p>This is a template</p>
</div>
`;
template.innerHTML = /*html*/ `
<style>
/* CSS rules for the component: */
p {
color: #ffffff;
}
</style>
<!-- HTML content for the component: -->
<div>
<p>This is a template</p>
</div>
`
Adding content using attributes, slots or moving nodes¶
Using attributes to customize the web component¶
- Add attributes to your tag, use like arguments for your component
- Also, remember
data-*attributes →.datasetproperty
Using attributes¶
- In the class declaration, read attributes using e.g. this.getAttribute()
- If you have
data-*attributes, you can access them usingthis.dataset.keyorthis.dataset["key"](kebab-case → camelCase) - Remember: In your class declaration,
thisrefers to the custom element in the main DOM (this is often called the light DOM of the Web Component, i.e. the part that is visible to the main DOM).- unless in an anonymous event listener defined using a function expression (
function(event) {...}) wherethiswill refer to the event target - however, arrow function expressions do not bind
this, more about this later
- unless in an anonymous event listener defined using a function expression (
Named slots in web components¶
- Inside custom element in main HTML
- Use
<tag slot="slotname"></tag>(substitutetagwith any tag you want). - HTML elements inside your custom element will not be rendered
- Multiple elements can use the same slotname, in this case all will be slotted into the corresponding
<slot name="slotname">in the template
- Use
- Inside template HTML
- Use
<slot name="slotname">FALLBACK CONTENT</slot> FALLBACK CONTENTis used if no slot data is provided from main HTML- The
<slot>element without anameattribute will slot any elements inside a custom element without a slot attribute
- Use
Styling slotted elements is a bit quirky¶
- Refer to slot parent CSS using
slot[name="name-of-slot"] - Style a slotted element (child of a
<slot>element) using the pseudo-element::slotted(<compound-selector>). E.g. to style all slottedpelements that are descendants of.cardelements:
.card ::slotted(p) {...}
- Slotted elements exist in the light DOM so they are also affected by CSS in the main DOM.
- Best practice: Use slots if you are styling the elements using CSS from the main DOM
Styling slotted elements inside the Web Component¶
- For text you can use use the following in
<template>:
<p><slot name="name-of-slot">TEXT MISSING</slot></p>
- Then in your HTML, use the
<span>tag:
<span slot="name-of-slot">This is the real text</span>
- Now you can style
<p>in your your template instead of styling the slotted element. - The same approach can be used with any element type.
Moving slotted elements from light DOM to shadow DOM¶
- Slotted elements reside in the light DOM and get their styles from the main DOM CSS
- Use e.g.
this.querySelectorAll(<selector>)to access child elements of the custom element. - A DOM node can only have one parent, so appending a node to another container node will move it, e.g.
this.shadowRoot.appendChild(<custom-element-child>)
Demo
Web Component adding image, moving node into a shadow DOM
Using slots vs moving elements into the shadow DOM¶
- Use slots if you want the user of the web component to
- Style the slotted content from their CSS.
- Change the content of the web component using their own JavaScript (e.g. add/remove slotted content).
- Move elements into the shadow DOM if you want to
- Style the content in the shadow DOM.
- Don't need to support web component user adding/removing content beyond the initial content.
Web Components
Adding interaction
Where to add event listeners¶
- The
connectedCallback()lifecycle method is called when a custom element is added to main DOM. - Best practice: add your event listeners in
connectedCallback()
Where to remove event listeners¶
- Event listeners should be removed in
disconnectedCallback() - Reason: so that you do not end up with multiple event listeners when the custom element removed and re-attached to the main DOM by other scripts (running on main page)
- For the assignment exercises in this course you do not need to do this as we will not be moving our web components around.
- However, if you move web components in your project → remember to remove the listeners!
Outline for defining a Web Component¶
const template = document.createElement("template");
// note the use of backticks (string template)
template.innerHTML = `
<!-- component template style & html goes here -->
<style>
</style>
<div></div>
`;
class NameOfCustomClass extends HTMLElement {
constructor() {
super();
// create a shadow DOM for the custom element. {mode: 'open'} allows it to be inspected
this.attachShadow({ mode: "open" });
// put a clone (true == deep copy) of our template into the shadow DOM
this.shadowRoot.appendChild(template.content.cloneNode(true));
// initialization code here
}
// this method is run when our custom element is inserted into the document DOM
connectedCallback() {
// add event listeners here
}
disconnectedCallback() {
// remove event listeners here
// all event listeners can be removed from an element using element.removeEventListener()
}
}
// define custom component (requires use of kebab-case)
window.customElements.define("html-element-name", NameOfCustomClass);
Class methods as event handlers¶
- For legacy compatibility, when an event handler is run, the variable
thisis bound to the event target inside the event handler. - However, when we use a method as an event handler, it is very likely that we want to use
thisto refer to the class instance. - To be able to do this, we need to wrap our event handler in an arrow expression.
Example: Method as an event handler¶
class MyWidget extends HTMLElement {
constructor() {
// constructor code here
}
// This is a event handler method you want to use
eventHandler(event) {
// event handler code needs to refer to the MyWidget object using `this`
this.shadowRoot.querySelector(...);
}
connectedCallback() {
// Add event listeners like this
this.shadowRoot
.querySelector(...)
.addEventListener("click", (event) => {
this.eventHandler(event);
});
// DO NOT do it like this; will result in `this` being bound to event.target
// in the handler
this.shadowRoot
.querySelector(...)
.addEventListener("click", this.eventHandler);
}
}
Demo
highlightable paragraph, <p-highlight> toggle color when clicked
Dynamic creation of elements in your web component¶
- There are actually two steps needed:
- Create the element.
- Add it to the DOM (or, more often, to a shadow DOM inside the component).
// Create a new element:
let newElement = document.createElement(tagName);
// Add `newElement` as the last child of `element`:
element.appendChild(newElement);
// Add `newElement` after `element` as a new sibling:
element.after(newElement);
// Add `newElement` before `element` as a new sibling:
element.before(newElement);
Building blocks of Web Components¶
- Custom elements: API for defining new elements that can be used in HTML
- Shadow DOM: A separate DOM with its own styles (CSS) that we can attach to a custom element - CSS from "normal" page does not affect the shadow DOM!
- HTML templates: A special element type that is not rendered in the browser, can cloned and used as a template for your custom element
Example: Animated door widget¶
<!-- main html -->
<door-widget>
<p>Text on door</p>
<p>Text behind door</p>
</door-widget>
<!-- template cloned into shadow DOM -->
<div class="door-widget">
<div class="door">
<div class="knob"></div>
<!-- first p will be moved here -->
</div>
<div class="content">
<!-- second p will be moved here -->
</div>
</div>
Firing and listening to synthetic events
Make it possible to listen to custom events that happen in your custom element.
Creating and firing events¶
- We can create events from e.g. our own components
- Synthetic events, as opposed to those fired by the browser
- Listen for events with a string
eventNamerepresenting the name of the custom event, otherwise same as browser events:
element.addEventListener(eventName, callbackfn);
- Create event
const event = new Event(eventName);
- Distpatch created event (
elementbecomesevent.target)
element.dispatchEvent(event);
Options when creating events¶
new Event(typeArg)
new Event(typeArg, eventInit);
eventInitis a plain object with the following propertiesbubbles(optional). default =false. If set totrue, the event will bubble.cancelable(optional). default =false. If set totrue, the event can be cancelled.composed(optional). default =false. If set totrue, the event can trigger listeners outside of a shadow root.
- More info on https://developer.mozilla.org/en-US/docs/Web/API/Event/Event
What event type should I choose?¶
- Try to follow the specification of the event type. E.g.
- "The
changeevent is fired for<input>,<select>, and<textarea>elements when an alteration to the element's value is committed by the user. Unlike theinputevent, thechangeevent is not necessarily fired for each alteration to an element'svalue." - "The
InputEventinterface represents an event notifying the user of editable content changes."
Creating events with custom data¶
- Use the
CustomEventinterface
const event = CustomEvent(eventName, { detail: yourData });
- Custom data will be accessible via the
detailpropertyevent.detail→yourData
Example: Door widget + synthetic events¶
- Click on doorknob of closed door →
- open door
- change value of widget to text behind the door
- fire input event
- Click on open door →
- close door
- change value of widget to text on door
- fire input event
Demo
Using the <door-widget> in a christmas calendar
ARIA attributes¶
ARIA = Accessible Rich Internet Applications¶
- Recommendation by W3C, project started 2006.
- Set of defined attributes that web content and web applications more accessible (e.g. when using a screen reader).
- Example
<div id="percent-loaded" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100">
</div>
- Attributes inform ARIA aware browsers and application of the semantic meaning of user interface components.
What to use ARIA attributes for?¶
- Indicating widget states, properties and roles
- Full compliance with WAI-ARIA is not required for the course, but we will be using some parts.
- More info