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¶
- Needed for higher grades (A, B)
- Choose to complete one or two exercises
- one exercise: 2 points
- two exercises: 3 points
- Exercises
- Tab Group web component
- Accordion Group web component
UI Libraries
libraries that provide custom UI components
Common approaches to UI components using HTML, CSS, JS¶
- "Older way"
- Component content in traditional HTML
- link to UI library CSS from HTML
- link to UI library JS from HTML
- UI library JavaScript transforms "static" content into interactive content
- Automatic e.g. using special classnames
- Manual, by calling a "create"-function on specific elements
- Extensions 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¶
- "New way": Web Components Standard - Custom Elements API
- fully encapsulated element
- new HTML tag for each web component
- link to UI library JS from HTML
- link to UI library CSS from HTML (sometimes embedded in JS)
- Use new HTML tags provided by UI library
Web Components¶
- Really badly named (makes Googling difficult).
- Similar to modern UI Libraries
- React
- Svelte
- Vue
- Angular
- Use of the Custom Elements API (much easier to Google).
- Encapsulate HTML, CSS and JavaScript in a custom element that can be used in your HTML code.
Why use a Web Component?¶
- Reusability: Designed to plug into a variety of applications without need for modification or special accommodations.
- Extensibility: Can be combined with other components to create new behaviors.
- Replaceability: Can be swapped for other components with similar functionality.
- Encapsulation: Self-contained and expose functionality through an interface while hiding the details of internal processes.
- Independence: Have minimal dependencies on other components and can operate in different environments and contexts.
Why base your project on Web Components?¶
- Modular Design: Systems are divided into reusable, self-contained components.
- Reusability: Components can be used in different projects or parts of a project.
- Interactions: Components communicate through well-defined interfaces.
- Isolation: Components encapsulate their own logic, reducing dependencies.
- Scalability: Independent development allows easy system expansion.
- Maintenance: Updates to one component have minimal impact on others.
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 usually built using the traditional method.
- While some frameworks are built around the concept of defining Web Components, these frameworks are not very popular.
- Web Components are just that, individual components.
- Most frameworks are starting to use some Web Components where applicable.
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 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 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.
- We can extend existing classes to inherit properties from the existing class (inheritance).
constructor()
method is run when a new object using the class is created.super()
is used to run the constructor of the parent class
- Different kinds of methods can be added to a class e.g.
- get methods that are run when properties are accessed
- set methods that are run when properties are set
- "ordinary" methods
- We use
this
to refer to the instance in our methods.
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
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 documentadoptedCallback
: called each time the element is moved to a new documentattributeChangedCallback
: called when specified attributes change (see documentation for details)
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.
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.
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
shadowRoot
property to your instance which will point to its shadow DOM "document" i.e.this.shadowRoot
- We can use
this.shadowRoot
like 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, but we can also create it in our JavaScript file with our class.- This keeps everything for the web component contained in our 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
true
to 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>
</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);
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 →.dataset
property
Using attributes¶
- In the class declaration, read attributes using e.g. this.getAttribute()
- If you have
data-*
attributes, you can access them usingthis.dataset.key
orthis.dataset[key]
(kebab-case → camelCase) - Remember: In your class declaration,
this
refers to the custom element in the main DOM- unless in an anonymous event listener defined using a function expression (
function(event) {...}
) wherethis
will 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>
(substitutetag
with 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 CONTENT
is used if no slot data is provided from main HTML- The
<slot>
element without aname
attribute 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 slottedp
elements that are descendants of.card
elements:
.card ::slotted(p) {...}
- Slotted elements still exist in the main 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
Better way of styling slotted elements¶
- 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.
Moving elements instead of using slots¶
- Slotted elements still reside in the main 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¶
connectedCallback()
lifecycle method is called when web component is added to main DOM- This means that when
connectedCallback()
is called, we know it is in the 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)
- However, for the assignment exercises in this course you do not need to do this as we will not be moving our web components around.
- 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
this
is 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
this
to 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 to a shadow DOM).
// 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 to for events, same as browser events:
element.addEventListener(eventName, callbackfn);
- Create event
const event = new Event(eventName : String);
- Distpatch created event (
element
becomesevent.target
)
element.dispatchEvent(event);
Options when creating events¶
new Event(typeArg)
new Event(typeArg, eventInit);
eventInit
is 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
. Value indicates if event will 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
change
event is fired for<input>
,<select>
, and<textarea>
elements when an alteration to the element's value is committed by the user. Unlike theinput
event, thechange
event is not necessarily fired for each alteration to an element'svalue
." - "The
InputEvent
interface represents an event notifying the user of editable content changes."
Creating events with custom data¶
- Use the
CustomEvent
interface
const event = CustomEvent(eventName, { detail: yourData });
- Custom data will be accessible via the
detail
propertyevent.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