729G87 Interaction Programming¶

Lecture 4¶

Johan Falkenjack, johan.falkenjack@liu.se¶

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
    • https://shoelace.style/components/details
  • Elix
    • https://component.kitchen/elix/ExpandableSection
  • PatternFly Elements
    • https://patternflyelements.org/components/accordion/

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

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 DOM
    • disconnectedCallback: called when the element is removed from the document
    • adoptedCallback: called each time the element is moved to a new document
    • attributeChangedCallback: 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.

Demo


Web Component without shadow DOM


https://codepen.io/jodyfoo/pen/xxzjeqa

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.

Demo


Web Component with shadow DOM


https://codepen.io/jodyfoo/pen/mdKLgmb

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);

Demo


Web Component using a template


https://codepen.io/jodyfoo/pen/oNyMBOp

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 using this.dataset.key or this.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) {...}) where this will refer to the event target
    • however, arrow function expressions do not bind this, more about this later

Demo


Web Component using a attributes


https://codepen.io/jodyfoo/pen/VwdBprV

Named slots in web components¶

  • Inside custom element in main HTML
    • Use <tag slot="slotname"></tag> (substitute tag 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
  • 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 a name attribute will slot any elements inside a custom element without a slot attribute

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

Demo


Web Component with named slots in template


https://codepen.io/jodyfoo/pen/abKaoRr

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


https://codepen.io/jodyfoo/pen/dyKqWwP

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


https://codepen.io/jodyfoo/pen/PoadaNw

Dynamic creation of elements in your web component¶

  • There are actually two steps needed:
      1. Create the element.
      1. 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);

Demo


Dynamic creation of elements inside shadow DOM


https://codepen.io/jodyfoo/pen/jOKeEpP

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>

Demo


Animated Door


https://codepen.io/jodyfoo/pen/rNKqWmp

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 becomes event.target)
element.dispatchEvent(event);

Options when creating events¶

new Event(typeArg)
new Event(typeArg, eventInit);
  • eventInit is a plain object with the following properties
    • bubbles (optional). default = false. If set to true, the event will bubble.
    • cancelable (optional). default = false. If set to true, 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 the input event, the change event is not necessarily fired for each alteration to an element's value."
    • https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event
  • "The InputEvent interface represents an event notifying the user of editable content changes."
    • https://developer.mozilla.org/en-US/docs/Web/API/InputEvent

Creating events with custom data¶

  • Use the CustomEvent interface
const event = CustomEvent(eventName, { detail: yourData });
  • Custom data will be accessible via the detail property
    • event.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


Door Widget + synthetic events


https://codepen.io/jodyfoo/pen/BaVqQOz

Demo


Using the <door-widget> in a christmas calendar


https://www.ida.liu.se/~729G87/course-material/lectures/examples/christmas-calendar/calendar.html

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
    • https://www.w3.org/TR/wai-aria-1.1/#widget_roles