Webprogramozás
Horváth Győző
Egyetemi docens
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
– megakadályozhatófocus
, blur
change
(elem elhagyásakor)input
(value
változásakor)invalid
, search
keydown
, beforeinput
– megakadályozhatóinput
, keyup
submit()
, reset()
click()
focus()
, blur()
select()
step
-es elemek
stepDown()
, stepUp()
elements
(HTMLFormControlsCollection)
elements['id_or_name']
(Element vagy RadioNodeList)form
, formAction
, formEncType
, stb.defaultValue
, defaultChecked
defaultSelected
(<option>
elem)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
eseményvalidationMessage
: hibaüzenetwillValidate
: lesz-e ellenőrizvevalidity
: ValidityState
patternMismatch
, valueMissing
, tooLong
, valid
, …checkValidity()
: logikai (invalid
esemény)reportValidity()
: logikai (form elem)setCustomValidity(üzenet)
: egyedi hibaüzenetEgyszerű űrlap
<form action="">
<button type="submit" name="btn">Submit</button>
</form>
submit
esemény, küldés megakadályozása
document.querySelector('form').addEventListener('submit', function (e) {
console.log(e);
e.preventDefault();
})
HTML validátorok használata
<form action="">
<p>Kötelező szöveg: <input type="text" required><span></span></p>
<p>RegExp szöveg: <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>Szám mező: <input type="number" value="1" min="1" max="10" required><span></span></p>
<p>Email mező: <input type="email" required><span></span></p>
<button type="submit" name="btn">Submit</button>
</form>
HTML validátorok + 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;
}
Saját hibaüzenet
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
})
Saját hibaüzenet invalid eseménnyel
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 szöveg: <input type="text" pattern="[A-Z]{3}-\d{3}" required><span></span></p>
<button type="submit" name="btn">Submit</button>
</form>
Saját validáció
<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="kep.png" alt="szöveg" >
src
const mem_kep = document.createElement('img');
mem_kep.src = 'korte.png';
//...
//ha szukseges a csere:
const kep = document.querySelector('img');
kep.src = mem_kep.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 xyKoord(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} = xyKoord(e.target)
table.rows[y].cells[x].classList.toggle('piros')
table.querySelector('caption').innerHTML = `${x}, ${y}`
}
})
Progressive enhancement
Weboldal | Webalkalmazás |
---|---|
Tartalom | Funkció |
Működik JS nélkül | JS kell hozzá |
Fogyasztás | Létrehozás |
Egyszerű | Komplex |
Szerveroldali | Kliensoldali |
noscript
tag, alt
attribútum, táblázat mint layout, canvas
fallback contentKönnyed lefokozás | Progresszív fejlesztés |
---|---|
Kiindulás: teljes funkcionalitású verzió | Kiindulás: alap funkció |
Ha valami nem elérhető, akkor azt kihagyva érhető el a funkció | Ha valami elérhető, akkor azt elérhetővé teszi |
Fentről lefele építkezik | Lentről felfele építkezik |
Nyomtatás
<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);
}
Felület kezelése
Alap funkcionalitás, kattintás
Tábla generálása
<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('')}`
}
1 kattintásra több is változik → újrarajzolás
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}
}
Animáció, újrarajzolás, hibás
.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);
}
}
Animáció, imperatív megközelítés
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)
})
}
}
Kódszervezés: modul + osztály
<!-- 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
}
Refaktorálás, “okos” DOM manipulálással
// 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
-lexiaAzaz a this
kontextusa
this
kontextusa//Global
var name = 'Peter'; // window.name
function hello() {
return this.name; // this === global (window)
}
hello(); // window.hello()
//Method call
var peter = {
name: 'Peter',
describe: function () {
return this.name; // this === peter
}
}
peter.describe();
//Constructor call
var Person = function(name) {
this.name = name; // this === instance object
}
var peter = new Person('Peter');
//Setting the context of this with call and apply
var peter = {
name: 'Peter',
hello: function () {
return this.name;
}
};
var julia = {
name: 'Julia',
hello: function () {
return this.name;
}
};
peter.hello.call(julia); // "Julia"
var peter = {
name: 'Peter',
age: 42,
describe: function () {
function getAge() {
return this.age; // this === global (window)
}
return this.name + ':' + getAge(); // global call, ~ window.getAge()
}
}
peter.describe(); // "Peter:undefined"
//With call and apply
var peter = {
name: 'Peter',
age: 42,
describe: function () {
function getAge() {
return this.age; // this depends on the call
}
return this.name + ':' + getAge.call(this);
}
}
peter.describe(); // "Peter:42"
//ES5 bind()
var peter = {
name: 'Peter',
age: 42,
describe: function () {
var getAge = (function () {
return this.age; // inner this is always outer this
}).bind(this);
return this.name + ':' + getAge();
}
}
peter.describe(); // "Peter:42"
//ES6 fat arrow syntax
var peter = {
name: 'Peter',
age: 42,
describe: function () {
var getAge = () => this.age;
return this.name + ':' + getAge();
}
}
peter.describe(); // "Peter:42"
document.querySelector
const $ = document.querySelector;
$('p') // Illegal invocation
// Magyarázat
const document = {
somethingInside: ...,
querySelector: function (sel) {
this.somethingInside
}
}
document.querySelector() // --> this === document
window.$() // --> this === window, window.somethingInside nincs
// Megoldás: a kontextus rögzítése
const $ = document.querySelector.bind(this);
// Alapeset
class AppView {
constructor(appState) {
this.elem = document.querySelector('something')
this.elem.addEventListener('click', this.onClick)
}
onClick(e) {
// this === document.querySelector('something')
}
}
// Bind
class AppView {
constructor(appState) {
this.elem = document.querySelector('something')
this.elem.addEventListener('click', this.onClick.bind(this))
}
onClick(e) {
// this === appView
}
}
// Fat arrow
class AppView {
constructor(appState) {
this.elem = document.querySelector('something')
this.elem.addEventListener('click', e => this.onClick(e))
}
onClick(e) {
// this === appView
}
}
// Class property initializer
class AppView {
constructor(appState) {
this.elem = document.querySelector('something')
this.elem.addEventListener('click', this.onClick)
}
onClick = (e) => {
// this === appView
}
}
this
kontextusa