Web programming
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
tabindex
→tabIndex
maxlength
→maxLength
placeholder
→placeholder
submit
, reset
– cancelablefocus
, blur
change
(when leaving an element)input
(when value
changes)invalid
, search
keydown
, beforeinput
– cancelableinput
, keyup
submit()
, reset()
click()
focus()
, blur()
select()
step
stepDown()
, stepUp()
elements
(HTMLFormControlsCollection)
elements['id_or_name']
(Element or RadioNodeList)form
, formAction
, formEncType
, etc.defaultValue
, defaultChecked
defaultSelected
(<option>
element)valueAsDate
, valueAsNumber
indeterminate
(checkbox, 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);
})
value
selectedIndex
selectedOptions
options
add(option[, before])
, remove(index)
index
text
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))
})
selectionStart
, selectionEnd
, selectionDirection
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)
})
novalidate
required
pattern
minlength
, maxlength
min
, max
, step
:valid
:invalid
:in-range
:out-of-range
:required
:optional
:read-only
:read-write
:checked
HTML5 constraint validation API
invalid
eventvalidationMessage
: error messagewillValidate
validity
: ValidityState
patternMismatch
, valueMissing
, tooLong
, valid
, …checkValidity()
: boolean (invalid
event)reportValidity()
: boolean (form element)setCustomValidity(üzenet)
: custom error messageSimple form
<form action="">
<button type="submit" name="btn">Submit</button>
</form>
submit
event, prevent submission
document.querySelector('form').addEventListener('submit', function (e) {
console.log(e);
e.preventDefault();
})
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>
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;
}
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
})
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
})
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>
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
}
})
<img src="image.png" alt="text" >
src
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;
<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()
rows
insertRow(index)
deleteRow(index)
rowIndex
sectionRowIndex
cells
insertCell(index)
deleteCell(index)
cellIndex
function xyCoord(td) {
const x = td.cellIndex
const tr = td.parentNode
const y = tr.sectionRowIndex
return {x, y}
}
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}`
}
})
Web page | Web application |
---|---|
Content | Behaviour |
Works without JS | JS is a must |
Content | Creation |
Simple | Complex |
Server-side | Client-side |
noscript
tag,alt
attribute, table as layout, canvas
fallback contentGraceful 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 |
Printing
<p id="printthis">
<a href="javascript:window.print()">Print this page</a>
</p>
<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>
<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);
}
User interface management
Basic functionality, clicking
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('')}`
}
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}
}
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);
}
}
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)
})
}
}
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
}
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)
))
}
}
this