Web programming

Code structure, context of this, special DOM elements, progressive enhancement

Horváth Győző
Associate professor
1117 Budapest, Pázmány Péter sétány 1/c., 2.408-as szoba
Tel: (1) 372-2500/8469
horvath.gyozo@inf.elte.hu

Recap

Recap

  • JavaScript language elements
  • DOM programming
  • Event handling details
  • JavaScript built-in objects

Recap – Code structure

  • physical, logical grouping
    • file, module, function, class
  • encapsulation
    • object, class, function, module
  • location of data storage
    • DOM vs JS data structures

Recap – Architecture

Forms, images and tables

Forms and form elements

  • Primary tools for interaction
  • Lots of attributes
  • The analogue approach works
    • tabindextabIndex
    • maxlengthmaxLength
    • placeholderplaceholder
  • Let’s focus on the properties beyond those!

Events

  • form
    • submit, reset – cancelable
  • form elements
    • focus, blur
    • change (when leaving an element)
    • input (when value changes)
    • invalid, search
  • text elements
    • keydown, beforeinput – cancelable
    • input, keyup

Methods

  • form
    • submit(), reset()
  • form elements
    • click()
    • focus(), blur()
  • text elements
    • select()
  • elements with step
    • stepDown(), stepUp()

Properties

  • form
    • elements (HTMLFormControlsCollection)
      • elements['id_or_name'] (Element or RadioNodeList)
  • form elements
    • form, formAction, formEncType, etc.
    • defaultValue, defaultChecked
    • defaultSelected (<option> element)
    • valueAsDate, valueAsNumber
    • indeterminate (checkbox, radio)

Radio

<form action="">
    <input type="radio" name="fruit" id="fruit1" value="apple"><label for="fruit1">Apple</label>
    <input type="radio" name="fruit" id="fruit2" value="pear"><label for="fruit2">Pear</label>
    <input type="radio" name="fruit" id="fruit3" value="plum"><label for="fruit3">Plum</label>
    <input type="radio" name="fruit" id="fruit4" value="lemon"><label for="fruit4">Lemon</label>
</form>
document.addEventListener('click', function (e) {
    // RadioNodeList.value
    const value = document.querySelector('form').elements['fruit'].value
    console.log(value);
})

Select

  • Select
    • value
    • selectedIndex
    • selectedOptions
    • options
    • add(option[, before]), remove(index)
  • Option
    • index
    • text

Select

const from = document.querySelector('select[name=from]')
const to = document.querySelector('select[name=to]')
document.querySelector('button').addEventListener('click', function (e) {
    const selectedOptions = Array.from(from.selectedOptions)
    selectedOptions.forEach(o => to.add(o))
})

Text selection API

  • form elements
    • text/password/search/tel/url/week/month
  • Properties
    • selectionStart, selectionEnd, selectionDirection
  • Methods
    • setSelectionRange(), setRangeText()
<input type="text" value="example text">
<button id="btn1">log selection</button>
<button id="btn2">set selection (2, 5)</button>
const input = document.querySelector('input')
document.querySelector('#btn1').addEventListener('click', function (e) {
    console.log(input.selectionStart, input.selectionEnd)
})
document.querySelector('#btn2').addEventListener('click', function (e) {
    input.setSelectionRange(2, 5)
})

Form validation – HTML and CSS

  • HTML5 attributes
    • novalidate
    • required
    • pattern
    • minlength, maxlength
    • min, max, step
  • CSS pseudo-selectors
    • :valid
    • :invalid
    • :in-range
    • :out-of-range
    • :required
    • :optional
    • :read-only
    • :read-write
    • :checked

Form validation – JavaScript

HTML5 constraint validation API

  • invalid event
  • validationMessage: error message
  • willValidate
  • validity: ValidityState
    • patternMismatch, valueMissing, tooLong, valid, …
  • checkValidity(): boolean (invalid event)
  • reportValidity(): boolean (form element)
  • setCustomValidity(üzenet): custom error message

Example and description

Form validation 1.

Simple form

<form action="">
  <button type="submit" name="btn">Submit</button>
</form>

Form validation 2.

submit event, prevent submission

document.querySelector('form').addEventListener('submit', function (e) {
  console.log(e);
  e.preventDefault();
})

Form validation 3.

Using HTML validators

<form action="">
  <p>Required: <input type="text" required><span></span></p>
  <p>RegExp: <input type="text" pattern="[A-Z]{3}-\d{3}" required><span></span></p>
  <p>Minlength, maxlength: <input type="text" minlength="3" maxlength="6"><span></span></p>
  <p>Number: <input type="number" value="1" min="1" max="10" required><span></span></p>
  <p>Email: <input type="email" required><span></span></p>
  <button type="submit" name="btn">Submit</button>
</form>

Form validation 4.

HTML validators + CSS

input:valid {
    box-shadow: 0 0 5px 1px green;
}
input:invalid {
    box-shadow: 0 0 5px 1px red;
}
input:focus {
    box-shadow: none;
}

input:valid + span:after {
    content: " ✔";
    color: green;
}
input:invalid + span:after {
    content: " ❌";
    color: red;
}

Form validation 5.

Custom error message

const input = document.querySelector('input')
input.addEventListener('input', function (e) {
  if (this.validity.patternMismatch) {
    this.setCustomValidity("Bad text according to the pattern (ABC-123)")
  } else {
    this.setCustomValidity("")
  }
  this.nextElementSibling.innerHTML = this.validationMessage
})

Form validation 6.

Custom error message with invalid event

const input = document.querySelector('input')
input.addEventListener('input', function (e) {
  this.setCustomValidity("")
  this.checkValidity();
  this.nextElementSibling.innerHTML = this.validationMessage
})
input.addEventListener('invalid', function (e) {
  if (this.validity.patternMismatch) {
    this.setCustomValidity("Bad text according to the pattern (ABC-123)")
  } else {
    this.setCustomValidity("")
  }
  this.nextElementSibling.innerHTML = this.validationMessage
})

Form validation 7.

novalidate

<form action="" novalidate>
  <p>RegExp: <input type="text" pattern="[A-Z]{3}-\d{3}" required><span></span></p>
  <button type="submit" name="btn">Submit</button>
</form>

Form validation 8.

Custom validation

<form action="" novalidate>
  <p>From: <input type="time" name="from" required value="20:00"><span></span></p>
  <p>To: <input type="time" name="to" required value="10:00"><span></span></p>
  <button type="submit" name="btn">Submit</button>
</form>
const form = document.querySelector('form')
const from_field = document.querySelector("[name=from]")
const to_field = document.querySelector("[name=to]")

const padTime = timeString => timeString.split(":").map(e => e.padStart(2, "0")).join(":")
function checkTime() {
  const from = padTime(from_field.value)
  const to = padTime(to_field.value)
  return from <= to
}
form.addEventListener('submit', function (e) {
  if (checkTime()) {
    from_field.setCustomValidity("")
  } else {
    from_field.setCustomValidity("from < to")
  }

  if (!this.reportValidity()) {
    e.preventDefault()
  }
})
form.addEventListener('input', function (e) {
  if (e.target.matches('input[type=time]')) {
    if (checkTime()) {
      from_field.setCustomValidity("")
    } else {
      from_field.setCustomValidity("from < to")
    }
    e.target.nextElementSibling.innerHTML = e.target.validationMessage
  }
})

Images

  • <img src="image.png" alt="text" >
  • Its most important property is src
    • Dynamically replace the image
  • Typical image operations
    • Replace image
    • Preload an image
const mem_img = document.createElement('img');
mem_img.src = 'korte.png';
//...
//ha szukseges a csere:
const img = document.querySelector('img');
img.src = mem_img.src;

Preload and replace an image

<img src="">
const images = [
  'https://source.unsplash.com/NRQV-hBF10M/640x480',
  'https://source.unsplash.com/VJTmFSendQ0/640x480',
  'https://source.unsplash.com/1q5KmcSe760/640x480',
  'https://source.unsplash.com/fBRVaRdFMIw/640x480',
  'https://source.unsplash.com/CyhYe8B9PuY/640x480',
  'https://source.unsplash.com/45bI5ezo3gE/640x480',
]
let index = 0
const img = document.querySelector('img')
const prevImage = document.createElement('img')
const nextImage = document.createElement('img')
function nextIndex(i) {
  return (i + 1) % images.length
}
function prevIndex(i) {
  return i === 0 ? images.length - 1 : i - 1
}
function init() {
  img.src = images[0]
  prevImage.src = images[images.length - 1]
  nextImage.src = images[1]
}
function next() {
  index = nextIndex(index)
  prevImage.src = img.src
  img.src = nextImage.src
  nextImage.src = images[nextIndex(index)]
}
function prev() {
  index = prevIndex(index)
  nextImage.src = img.src
  img.src = prevImage.src
  prevImage.src = images[prevIndex(index)]
}
document.addEventListener('keydown', function (e) {
  if (e.key === 'ArrowLeft') {
    prev()
  }
  else if (e.key === 'ArrowRight') {
    next()
  }
})
init()

Preload and replace an image

Table

  • Table
    • rows
    • insertRow(index)
    • deleteRow(index)
  • Row
    • rowIndex
    • sectionRowIndex
    • cells
    • insertCell(index)
    • deleteCell(index)
  • Cell
    • cellIndex
function xyCoord(td) {
  const x =  td.cellIndex
  const tr = td.parentNode
  const y =  tr.sectionRowIndex
  return {x, y}
}

Table – Example

const table = document.querySelector('table')
table.addEventListener('click', function (e) {
  if (e.target.matches('td')) {
    const {x, y} = xyCoord(e.target)
    table.rows[y].cells[x].classList.toggle('piros')
    table.querySelector('caption').innerHTML = `${x}, ${y}`
  }
})

Progressive enhancement of web pages

Website vs. Web Application

Web page Web application
Content Behaviour
Works without JS JS is a must
Content Creation
Simple Complex
Server-side Client-side

The problem

  • What happens if we use a feature that is not available everywhere?
  • For example
    • Originally: no JavaScript
    • company settings
    • older devices
    • limited resource devices (e.g. fridge)
  • Two approaches
    • graceful degradation
    • progressive enhancement

Graceful degradation

  • Purpose: user can still use the UI functionally
  • Approach: tolerating failure
  • If there is an error in one or more components of a complex system, it provides an alternative route to operate the app
  • See noscript tag,alt attribute, table as layout, canvas fallback content

Progressive enhancement

  • The goal is the same, but the approach is different
  • Support for various users and devices
  • The goal is to create a common base (HTML) understood by all the tools
  • Then comes CSS and JavaScript
  • The principle: separation of content, style and behavior

GD vs PE

Graceful degradation Progressive enhancement
Starting point: fully functional version Starting point: basic functionality
If something is unavailable, skip it to access the function If something is available, it makes it available
Top-down approach Bottom-up approach

Progressive enhancement

  • Steps
    • content (semantic HTML)
    • appearance, style (CSS)
    • behavior (JavaScript)

PE advantages

  • Works without JavaScript
  • It runs on many devices
  • Ensure better accessibility
  • builds from the bottom up → cleaner, more modular code
  • future compatibility

Example from the old times

Printing

<p id="printthis">
  <a href="javascript:window.print()">Print this page</a>
</p>

Example

  • Graceful degradation
    • What is JavaScript?
    • How do I turn it on?
    • Do I have the right to turn it on?
<p id="printthis">
  <a href="javascript:window.print()">Print this page</a>
</p>
<noscript>
  <p class="scriptwarning">
    Printing the page requires JavaScript to be enabled. 
    Please turn it on in your browser.
  </p>
</noscript>

Example

  • Progressive enhancement
    • Do we need printing functionality?
<p id="printthis">Thank you for your order. Please print this page for your records.</p>
const pt = document.getElementById('printthis');
if(pt && typeof window.print === 'function'){
  const but = document.createElement('input');
  but.setAttribute('type','button');
  but.setAttribute('value','Print this now');
  but.onclick = function(){
    window.print();
  };
  pt.appendChild(but);
}

Code structure

User interface management

Writing to the DOM

  1. Imperative approach
    • if the given DOM element needs to be preserved (have internal state, e.g. input, animation, storage in DOM)
    • direct manipulation
  2. Declarative approach
    • if items can be regenerated (no internal state)
    • data mapping to UI elements
    • UI = fn(data)
    • HTML generators

Cards

Basic functionality, clicking

Cards

Generating table

<table>
  <tr>
    <td>
      <div class="card">
        <div class="front">1</div>
        <div class="back"></div>
      </div>
    </td>
  </tr>
</table>
let m;

const table = document.querySelector('table')
table.addEventListener('click', onClick)
function onClick(e) {
  const card = e.target.closest('.card')
  if (this.contains(card)) {
    card.classList.toggle('flipped')
  }
}

document.querySelector('button').addEventListener('click', onButtonClick)
function onButtonClick(e) {
  const n = parseInt(document.querySelector('input[type=number]').value)
  m = genMatrix(n)
  table.innerHTML = genTable(m);
}

function random(a, b) {
  return Math.floor(Math.random() * (b - a + 1)) + a
}
function genMatrix(n) {
  return (new Array(n)).fill(0).map(() => (new Array(n)).fill(0).map(() => random(1, 99)))
}
function genTable(m) {
  return `${m.map(row => `
    <tr>
      ${row.map(cell => `
        <td>
          <div class="card">
            <div class="front">${cell}</div>
            <div class="back"></div>
          </div>
        </td>
      `).join('')}
    </tr>
  `).join('')}`
}

Cards

multiple changes for 1 click → redraw

function onClick(e) {
  const card = e.target.closest('.card')
  if (this.contains(card)) {
    const {x, y} = xyCoord(card)

    const selected = m.flatMap(row => row.filter(card => card.flipped))
    m[y][x].flipped = !m[y][x].flipped
    if (selected.length === 2) {
      selected.forEach(card => card.flipped = false)
    }

    table.innerHTML = genTable(m);  
  }
}

function xyCoord(card) {
  const td = card.closest('td')
  const x = td.cellIndex
  const tr = td.parentNode
  const y = tr. sectionRowIndex
  return {x, y}
}

Cards

Animation, redraw, faulty

.card.flipped {
  animation: flip 1s forwards;
}
@keyframes flip {
  100% {
    transform: rotateY(0deg);
  }
}
function onClick(e) {
  const card = e.target.closest('.card')
  if (this.contains(card)) {
    const {x, y} = xyCoord(card)

    const selected = m.flatMap(row => row.filter(card => card.flipped))
    m[y][x].flipped = !m[y][x].flipped
    if (selected.length === 2) {
      selected.forEach(card => card.flipped = false)
    }

    table.innerHTML = genTable(m);  
  }
}

Cards

Animation, imperative approach

let m;
let changed = [];

function onClick(e) {
  const card = e.target.closest('.card')
  if (this.contains(card)) {
    const { x, y } = xyCoord(card)

    if (m[y][x].flipped) { return }
    changed = [];
    const selected = m.flatMap((row, i) => 
      row.map((card, j) => ({i, j, card}))
         .filter(o => o.card.flipped)
    )
    m[y][x].flipped = !m[y][x].flipped
    changed.push({i: y, j: x, card: m[y][x]})
    if (selected.length === 2) {
      selected.forEach(s => s.card.flipped = false)
      changed.push(...selected)
    }

    changed.forEach(c => {
      document.querySelector(`table tr:nth-child(${c.i+1}) td:nth-child(${c.j+1}) .card`).classList.toggle('flipped', c.card.flipped)
    })
  }
}

Cards

Code organization: module + class

<!-- index.html -->
<script type="module" src="index.js"></script>
// index.js
import { AppState } from "./appstate.js";
import { AppView } from "./appview.js";

const appState = new AppState()
new AppView(appState);
// appstate.js
import { random } from "./helper.js"

export class AppState {
  constructor() {
    this.m = 0
    this.changed = []
  }
  
  createMatrix(n) {
    this.m = (new Array(n)).fill(0).map(() => (new Array(n)).fill(0).map(() => ({
      value: random(1, 99),
      flipped: false
    })))
  }

  selectCard(x, y) {
    if (this.m[y][x].flipped) return;

    this.changed = [];
    const selected = this.m.flatMap((row, i) => 
      row.map((card, j) => ({i, j, card}))
        .filter(o => o.card.flipped)
    )
    this.m[y][x].flipped = !this.m[y][x].flipped
    this.changed.push({i: y, j: x, card: this.m[y][x]})
    if (selected.length === 2) {
      selected.forEach(s => s.card.flipped = false)
      this.changed.push(...selected)
    }
  }
}
// appview.js
import { xyCoord } from "./helper.js"

export class AppView {
  constructor(appState) {
    this.appState = appState
    this.table = document.querySelector('table')
    this.table.addEventListener('click', this.onClick.bind(this))
    document.querySelector('button').addEventListener('click', this.onButtonClick.bind(this))
  }
  
  onClick(e) {
    const card = e.target.closest('.card')
    if (this.table.contains(card)) {
      const { x, y } = xyCoord(card)
  
      this.appState.selectCard(x, y)
  
      this.appState.changed
        .forEach(c => {
          document.querySelector(`table tr:nth-child(${c.i+1}) td:nth-child(${c.j+1}) .card`).classList.toggle('flipped', c.card.flipped)
        })
    }
  }
  
  onButtonClick(e) {
    const n = parseInt(document.querySelector('input[type=number]').value)
  
    this.appState.createMatrix(n)
  
    this.table.innerHTML = this.genTable(this.appState.m);
  }
  
  genTable(m) {
    return `${m.map(row => `
    <tr>
      ${row.map(cell => `
        <td>
          <div class="card ${cell.flipped ? 'flipped' : ''}">
            <div class="front">${cell.value}</div>
            <div class="back"></div>
          </div>
        </td>
      `).join('')}
    </tr>
  `).join('')}`
  }
}
// helper.js
export function xyCoord(card) {
  const td = card.closest('td')
  const x = td.cellIndex
  const tr = td.parentNode
  const y = tr.sectionRowIndex
  return { x, y }
}
export function random(a, b) {
  return Math.floor(Math.random() * (b - a + 1)) + a
}

Cards

Refactoring with “smart” DOM ​​manipulation

// appstate.js
export class AppState {
  constructor() {
    this.m = 0
  }
  selectCard(x, y) {
    if (this.m[y][x].flipped) return;

    const selected = this.m.flatMap(row => row.filter(card => card.flipped))
    this.m[y][x].flipped = !this.m[y][x].flipped
    if (selected.length === 2) {
      selected.forEach(card => card.flipped = false)
    }
  }
}
// appview.js
onClick(e) {
  const card = e.target.closest('.card')
  if (this.table.contains(card)) {
    const { x, y } = xyCoord(card)

    this.appState.selectCard(x, y)

    this.appState.m.forEach((row, i) => row.forEach((card, j) => 
      document.querySelector(`table tr:nth-child(${i+1}) td:nth-child(${j+1}) .card`).classList.toggle('flipped', card.flipped)
      // this.table.rows[i].cells[j].querySelector('.card').classList.toggle('flipped', card.flipped)
    ))
  }
}

Summary

  • Further DOM elements
    • forms, pictures, tables
  • Progressive enhancement
  • Code structure
    • imperative, declarative UI management
  • The context of this