A labor folyamán a hallgatók a laborvezető segítségével önállóan végeznek feladatokat a webes technológiák gyakorlati megismerése érdekében.
Felhasznált technológiák és eszközök:
-
webböngészők beépített hibakereső eszközei,
-
npm, a NodeJS csomagkezelője,
-
Visual Studio Code kódszerkesztő alkalmazás,
- otthoni vagy egyéni munkavégzéshez használható bármilyen más kódszerkesztő vagy fejlesztőkörnyezet, de a környezet kapcsán felmerülő eltérésekről önállóan kell gondoskodni.
Az elkészült jegyzőkönyvet egy PDF formájában kell feltölteni a tárgy oldalán, a szükséges további erőforrásokkal (projekt, HTML, CSS, JavaScript fájlok) egy ZIP fájlba csomagolva. Ügyeljen rá, hogy a ZIP fájlba artifakt ne kerüljön (fordítás eredményeképpen előálló fájlok, pl. a bin/obj mappa tartalma). Az eredmények is itt lesznek. A jegyzőkönyv sablonja DOCX formátumban innen letölthető.
A jegyzőkönyvben csak a szükséges mértékű magyarázatot várjuk el. Ahol másképpen nincs jelezve, eredményközlés is elegendő. Képernyőképek bevágásához a Windows-ban található Snipping Tool eszköz használható, vagy az Alt+PrtScr billentyűkombinációval az aktuálisan fókuszált ablak teljes egésze másolható.
A hiányos vagy túl bőbeszédű megoldásokra vagy a teljes jegyzőkönyvre helyes megoldás esetén is pontlevonás adható!
A laborvezető jelen dokumentum alapján vezeti végig a labort. A dokumentumban az alábbi módon van jelölve, hogy a jegyzőkönyvben dokumentálni szükséges egy-egy lépést:
Töltse ki a jegyzőkönyvben található szükséges adatokat: a nevét, Neptun kódját, a labor idejét és helyét.
- Nyissuk meg a Visual Studio Code-ot egy üres munkamappában!
- A Terminal (Ctrl+ö / View > Integrated Terminal) segítségével indítsuk el a http-server kiszolgálót:
http-server
!
A korábban megismert HTML és CSS adják a weboldalunk vázát, alapműködését és kinézetét, viszont a korai dokumentum-alapú weboldalaktól áttértünk a dinamikus weboldalakra, melyek futás időben módosítják az aktuális dokumentumot (a DOM-ot), így interakciót kezelhetünk, és a weboldalunkra (a kliens oldalra) önálló alkalmazásként tekintünk.
Az alkalmazásainkhoz dinamizmust (időbeni változást) szkripteléssel rendelünk, erre JavaScriptet használunk. A JavaScript egy dinamikusan típusos, interpretált szkriptnyelv, a hozzá tartozó futtatókörnyezetek végrehajtó egységei pedig alapvetően egyszálúak, így nincsen kölcsönös kizárási problémánk.
Érdemes továbbá megemlíteni a felhasználandó típusokat (function
, object
, string
, number
, undefined
, boolean
, symbol
), az ezek közötti szabad konverziót és a JavaScript eseményhurkot (event loop). Az event loop a JavaScriptet folyamatosan befejeződésig futtatja ("Run-to-completion"), amíg a futás be nem fejeződik, majd aszinkron eseményre vár. Az események bekövetkeztével az eseményhez regisztrált eseménykezelők lefutnak. Az események lehetnek:
- felhasználói interakció,
- időzítés,
- IO műveletek eredménye (pl. AJAX, Websocket).
A fontosabb kulcsgondolatok tehát röviden:
- interpretált futtatás,
- DOM dinamikus manipulációja,
- dinamikus típusosság és típuskonverzió,
- egyszálúság, event loop és aszinkronitás.
Említésre méltó még, hogy a JavaScript (klasszikus értelemben véve) nem objektum-orientált, az osztályok koncepciója a nyelvben később jelent meg és nem minden böngészőben támogatott; a nyelv a prototipikus öröklés módszerét alkalmazza az objektumorientált megközelítéshez. Ezen kívül különös sajátosságai vannak, a this
kulcsszó pl. nem az aktuális objektumra, hanem az aktuális függvényre mutat (kivétel az arrow syntax, ami a this
-t az eredeti értéken hagyja).
A laboron egy egyszerű "offline" To-Do alkalmazást készítünk.
Az alkalmazás alapjaként egy egyszerű HTML oldal szolgál, amihez a saját JavaScriptünket írjuk. A JS kódot HTML-ben is elhelyezhetnénk, viszont az nem karbantartható és alapvetően nem best practice, úgyhogy saját .js fájlba fogjuk tenni a kódot, amit behivatkozunk. A stílusozást Bootstrappel oldjuk meg.
A böngésző különböző körülmények függvényében cache-elheti a fájljainkat, ezért a frissítést ilyenkor kézzel kell megoldanunk. Ne felejtsük el menteni a fájlt, ezután a böngészőben állítsuk be az F12 Developer Tools-ban a Network fülön az "Always refresh from server"/"Disable cache" vagy hasonló elnevezésű beállítást!
A kiinduló index.html tartalma legyen az alábbi:
<!doctype html>
<html>
<head>
<title>To-Do | Mobil- és webes szoftverek</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css">
</head>
<body class="container">
<h1 class="jumbotron my-2">To-Do | Mobil- és webes szoftverek</h1>
<nav class="nav nav-pills nav-justified my-2 text-nowrap">
<a class="todo-tab nav-item nav-link active" href="#all">
All <span class="badge badge-secondary">1</span>
</a>
<a class="todo-tab nav-item nav-link" href="#active">
Active <span class="badge badge-secondary">1</span>
</a>
<a class="todo-tab nav-item nav-link" href="#inactive">
Inactive <span class="badge badge-secondary"></span>
</a>
<a class="todo-tab nav-item nav-link" href="#done">
Done <span class="badge badge-secondary"></span>
</a>
</nav>
<div class="container my-2" id="todo-list">
<div class="row">
<button class="btn btn-outline-success fas fa-check" title="Mark as done"></button>
<a class="list-group-item col" href="#">
My first to-do of the day
</a>
<div class="btn-group">
<button class="btn btn-outline-secondary fas fa-plus" title="Mark as active"></button>
<button class="btn btn-outline-secondary fas fa-minus" title="Mark as inactive"></button>
<button class="btn btn-outline-danger fas fa-trash" title="Remove"></button>
</div>
</div>
</div>
<div class="card my-2">
<form id="new-todo-form" class="card-body">
<h5 class="card-title">Add new to-do</h5>
<div class="input-group">
<input type="text" id="new-todo-title" class="form-control" placeholder="Type a name for the new to-do..."
autocomplete="off" autofocus="true">
<button type="submit" class="input-group-append btn btn-outline-default fas fa-arrow-up" title="Add new to-do"></button>
</div>
</form>
</div>
<script src="todo.js" type="text/javascript"></script>
</body>
</html>
Láthatjuk, hogy a statikus oldal az alábbiakból tevődik össze:
- cím,
- fülek az összes, aktív, inaktív és kész elemek szűrésére,
- a to-do elemek listája, az egyes elemek mellett az értelmezett műveletek,
- új elem hozzáadása panel, melyen az új to-do bejegyzés szövegét kell megadnunk egy űrlapon.
A <body>
végén egy <script>
a todo.js fájlra hivatkozik, így hozzuk azt is létre. A szkript az oldal lényegi tartalmának betöltődése után fut le, így nem kell majd várakoznunk a dokumentum teljes betöltődésére. A gyakorlatban ez változó, szokás a <head>
elemben in betölteni JS fájlokat amikor kritikus, viszont az gátolja a HTML megjelenését, amíg a JS fájl le nem töltődik.
Az egyes to-do-k modelljére érdemes saját osztályt definiálnunk. A böngészők jelenleg nem támogatják teljes mértékben a class
kulcsszót, így a "klasszikus" megoldást alkalmazzuk.
JavaScriptben egy függvény konstruktorfüggvény, ha a
this
változón tulajdonságokat helyez el és nem tér vissza semmivel. Ekkor anew
kulcsszóval meghívva a függvényt az konstruktorként funkcionál és athis
értékét kapjuk vissza. Ezen felül azinstanceof
kulcsszóval megvizsgálhatjuk, hogy adott függvény konstruktora által készített objektumról van-e szó.
A fülek lehetséges állapotai az "all", "active", "inactive" és "done", az "all" kivételével ezeket az állapotokat veheti fel egy to-do elem is.
A todo.js elejére vegyük fel a Todo konstruktorfüggvényt és a konkrét példányokat tároló (üres) tömböt, valamint a lehetséges állapotokat:
function Todo(name, state) {
this.name = name;
this.state = state;
}
var todos = [];
var states = ["active", "inactive", "done"];
var tabs = ["all"].concat(states);
console.log(tabs);
A legnyilvánvalóbb módja a hibakeresésnek az, ha a konzolra írunk. Az F12 segítségével a Console fülön láthatjuk a kimenetet.
Iratkozzunk fel a form submit
eseményére és kezeljük az új to-do elem létrehozását! A feliratkozást megtehetjük HTML-ből és JavaScriptből is, most az utóbbit alkalmazzuk!
var form = document.getElementById("new-todo-form");
var input = document.getElementById("new-todo-title");
form.onsubmit = function (event) {
event.preventDefault(); // meggátoljuk az alapértelmezett működést, ami frissítené az oldalt
if (input.value && input.value.length) { // ha érvényes érték van benne
todos.push(new Todo(input.value, "active")); // új to-do-t aktív állapotban hozunk létre
input.value = ""; // kiürítjük az inputot
// TODO: újrarajzolni a listát
}
}
Definiálnunk kell még a gombokat, amiket a Todo-hoz fogunk rendelni. Nem volna szükség a modellek definiálására, elvégre is a JS egy dinamikus nyelv, de struktúrát ad a kódnak, objektum-orientáltabban kezelhető.
A VS Code-ban valószínűleg az IntelliSense nyomára tudunk bukkanni a JS kód írása közben. Ennek az oka nem a JavaScript, hanem a háttérben futó TypeScript fordító. Mivel minden JavaScript egyben TypeScript kód is, ezért a típusinformációk kinyerhetők a kódból. Ez a TypeScript nagy előnye a JS-sel szemben. Fordítási hibáink nem lesznek JavaScriptben, de az IntelliSense segítségét ki lehet így használni.
function Button(action, icon, type, title) {
this.action = action; // a művelet, amit a gomb végez
this.icon = icon; // a FontAwesome ikon neve (class="fas fa-*")
this.type = type; // a gomb Bootstrapbeni típusa ("secondary", "danger" stb.)
this.title = title; // a gomb tooltip szövege
}
var buttons = [ // a gombokat reprezentáló modell objektumok tömbje
new Button("done", "check", "success", "Mark as done"),
new Button("active", "plus", "secondary", "Mark as active"),
// az objektumot dinamikusan is kezelhetjük, ekkor nem a konstruktorral példányosítjuk:
{ action: "inactive", icon: "minus", type: "secondary", title: "Mark as inactive" },
new Button("remove", "trash", "danger", "Remove"),
];
Így már fel tudunk venni új elemet, viszont ez nem látszik a felületen, ugyanis csak memóriában dolgoztunk, nem módosítottuk a DOM-ot. Írjunk egy függvényt, ami az összes to-do elemet kirajzolja a felületre! A jelenlegi sablon alapján kódból összeállítjuk a DOM-részletet:
function renderTodos() {
var todoList = document.getElementById("todo-list"); // megkeressük a konténert, ahová az elemeket tesszük
todoList.innerHTML = ""; // a jelenleg a DOM-ban levő to-do elemeket töröljük
todos.forEach(function (todo) { // bejárjuk a jelenlegi todo elemeket (alternatív, funkcionális bejárással)
var item = document.createElement("a"); // az elemet tároló <a>
item.className = "list-group-item col";
item.href = "#";
item.innerHTML = todo.name;
var buttonContainer = document.createElement("div"); // a gombok tárolója
buttonContainer.className = "btn-group";
buttons.forEach(function (button) { // a gomb modellek alapján legyártjuk a DOM gombokat
var btn = document.createElement("button"); // <button>
btn.className = "btn btn-outline-" + button.type + " fas fa-" + button.icon;
btn.title = button.title;
if (todo.state === button.action) // azt a gombot letiljuk, amilyen állapotban van egy elem
btn.disabled = true;
// TODO: a gomb klikk eseményének kezelése
buttonContainer.appendChild(btn); // a <div>-be tesszük a gombot
});
var row = document.createElement("div"); // a külső konténer <div>, amibe összefogjuk az elemet és a műveletek gombjait
row.className = "row";
row.appendChild(item); // a sorhoz hozzáadjuk az <a>-t
row.appendChild(buttonContainer); // és a gombokat tartalmazó <div>-et
todoList.appendChild(row); // az összeállított HTML-t a DOM-ban levő #todo-list elemhez fűzzük
});
}
renderTodos(); // kezdeti állapot kirajzolása
Most már látjuk, hogy mi fog kerülni a // TODO
komment helyére a form elküldésekor:
renderTodos();
Ha abba a hibába esnénk, hogy a DOM elemeket egyesével szeretnénk eltávolítani a DOM-ból a szülő elem
children
tulajdonságának segítségével, vigyáznunk kell, ugyanis ez egy "élő" kollekció: miközben az elemeket töröljük, a kollekció length tulajdonsága is változik! Persze egy egyszerűfor
ciklussal megoldható, de mindenképpen a végétől indulva járjuk be a kollekciót, amíg az ki nem ürül!
Illesszen be egy képernyőképet néhány hozzáadott tennivalóról!
A DOM elemekre kattintva be tudjuk állítani az aktuális állapotot, ezt a DOM elemhez eseménykezelő rendelésével tehetjük meg. Eseménykezelőt a HTML-ben az on\*
attribútumok megadásával tudunk kötni, JavaScriptben a DOM API-t használva pl. az elem referenciáját megszerezve az .addEventListener("eseménynév", callbackFüggvény)
függvény meghívásával vagy a megfelelő feliratkoztató függvény beállításával (pl. onclick = callbackFüggvény
). A JS kódot az alábbival egészítsük ki:
var currentTab; // a jelenleg kiválasztott fül
function selectTab(type) {
currentTab = type; // eltároljuk a jelenlegi fül értéket
for (var tab of document.getElementsByClassName("todo-tab")) {
tab.classList.remove("active");// az összes fülről levesszük az .active osztályt
if (tab.getAttribute("data-tab-name") == type)
tab.classList.add("active");
}
renderTodos();
}
selectTab("all");
A fenti minta, amikor egy függvényt a definiálása után közvetlenül meghívunk, egy csúnyább, de elterjedt alternatívával szokták alkalmazni, ez az ún. self-invoking function declaration, aminek sok változata ismeretes, ez az egyik:
(var selectTab = function(type) { /* ... */})("all");
A selectTab
függvény hívását a HTML-ből kössük a klikk eseményre, cseréljük le a tabok tartalmát:
<a class="todo-tab nav-item nav-link active" data-tab-name="all" href="#all" onclick="selectTab('all')">
All <span class="badge badge-secondary">1</span>
</a>
<a class="todo-tab nav-item nav-link" data-tab-name="active" href="#active" onclick="selectTab('active')">
Active <span class="badge badge-secondary">1</span>
</a>
<a class="todo-tab nav-item nav-link" data-tab-name="inactive" href="#inactive" onclick="selectTab('inactive')">
Inactive <span class="badge badge-secondary"></span>
</a>
<a class="todo-tab nav-item nav-link" data-tab-name="done" href="#done" onclick="selectTab('done')">
Done <span class="badge badge-secondary"></span>
</a>
Az elemhez adatparamétert is rendeltünk, ezt az attribútumot a data-
előtaggal láttuk el jelezvén, hogy az attribútum kizárólag adathordozásra szolgál. A this
paraméter az aktuális DOM elemet jelent ebben a kontextusban, ezt kapja meg a selectTab
függvényünk.
Az elemek állapotának változását hasonlóképpen kezelhetjük, amikor a gombokat gyártjuk a renderTodos()
függvényben, az eseménykezelőket ott helyben fel tudjuk regisztrálni (a // TODO
komment helyére kerüljön):
btn.onclick = button.action === "remove"
? function () { // klikk eseményre megerősítés után eltávolítjuk a to-do-t
if (confirm("Are you sure you want to delete the todo titled '" + todo.name + "'?")) {
todos.splice(todos.indexOf(todo), 1); // kiveszünk a 'todo'-adik elemtől 1 elemet a todos tömbből
renderTodos();
}
}
: function () { // klikk eseményre beállítjuk a to-do állapotát a gomb által reprezentált állapotra
todo.state = button.action;
renderTodos();
}
Érdekesség a
confirm()
függvény, amely böngészőben natívan implementált: a felhasználónak egy egyszerű megerősítő ablakot dob fel a megadott szöveggel, és blokkolva várakozik a válaszra. A válasz egy boolean érték, így azif
törzse csak akkor fut le, ha a felhasználó OK-val válaszol. Hasonló azalert()
, az viszont csak egy OK-zható figyelmeztetést dob fel, ami nem tér vissza semmivel.
Egészítsük ki a renderTodos()
függvényt, hogy frissítse a fülek mellett található badge-ben megjelenő számértékeket:
document.querySelector(".todo-tab[data-tab-name='all'] .badge").innerHTML = todos.length || "";
for (var state of states)
document.querySelector(".todo-tab[data-tab-name='" + state + "'] .badge").innerHTML = todos.filter(function (t){ return t.state === state; }).length || "";
A
querySelector()/querySelectorAll()
API-val egy CSS szelektort adhatunk meg a document-en vagy egy elemen, és az illeszkedő első/összes elemet kapjuk vissza.
A
filter()
függvénynek egy callbacket adunk át, ez fog kiértékelődni minden elemre: ha a feltétel igaz, akkor az elemet visszakapjuk, különben nem. Magyarul: azokra az elemekre szűrünk, amelyek állapota az aktuálisan bejárt állapot ("active", "inactive", "done"), tehát megszámoljuk, hány elem van az adott státuszban. Ezen felül, ha az értékfalsey
, tehát esetünkben 0, helyette üres stringet adunk vissza, így nem fog megjelenni a badge.
Utolsó lépésként logikus, hogy az aktuális fül alapján szűrjük le az elemeket. Ezt a renderTodos()
apró módosításával tudjuk megtenni, a todos.forEach()
helyett írjuk az alábbit:
var filtered = todos.filter(function(todo){ return todo.state === currentTab || currentTab === "all"; });
filtered.forEach(function (todo) { // ...
A legtöbb böngészőben már használható az arrow syntax, ami amellett, hogy jelentősen rövidebb kódot eredményez, a
this
működését is "feljavítja": igazából a this értékét nem változtatja meg, mint minden függvény, hanem a körülötte levő értékén hagyja. A fenti kód arrow syntax-szal és egy kis fényezéssel, erősen funkcionális megközelítésben az alábbi lehet:todos.filter(todo => ["all", todo.state].includes(currentTab)).forEach(todo => {
Illesszen be egy-egy képernyőképet a tennivalók állapotainak változtatásáról, a különböző oldalakon történő megjelenésükről!
Legyenek fel-le mozgathatók a to-do elemek az all
listában!
- Hozzon létre két új gombot, amely a felfelé és lefelé mozgatást jelzik az elemnél! Használja a
fas fa-arrow-up
ésfas fa-arrow-down
osztályokat az ikonokhoz! A gombok csak azall
fülön legyenek láthatók! - A gomb legyen letiltva, ha nem mozgatható a megadott irányba az elem!
- A gombra kattintva az elem kerüljön előrébb/hátrébb az elemek listájában!
Illesszen be egy-egy képernyőképet néhány tennivalóról sorrendezés előtt és után!
Illessze be a releváns kódrészleteket!
Egy to-do listának nem sok értelme van, ha nem menthetők el az adataink. A mentésre egyértelmű lehetőséget biztosít a localStorage
és a sessionStorage
. Mindkettő kulcs-érték tároló, a kulcsok és értékek egyaránt string
típusúak. A különbség a kettő között az élettartamuk: míg a localStorage
- bár korlátos méretű - a böngészőt újraindítva is megtartja állapotát, a sessionStorage
a böngészőt/fület bezárva elvész. A sessionStorage
adatokat memóriában, a localStorage
adatokat viszont perzisztensen, fájlban tárolja a böngésző.
A tároláshoz minden renderelési ciklus elején volna érdemes mentenünk. Bár az alkalmazásunk renderTodos()
függvénye nevéből fakadóan a DOM-ot manipulálja, ez az a pont, ahol bármilyen változásról értesülünk. Fontos, hogy tartsuk be a separation of concerns elvet: mindenki a saját feladatával foglalkozzon! Ezért ne itt valósítsuk meg a perzisztálást, hanem egy saját függvényben, amit meghívunk minden változást indukáló ponton a kódban:
- elem állapotának változása,
- elem létrehozása,
- elem törlése.
Komplexebb alkalmazásfejlesztő keretrendszerekben is problémát okoz a változásokról történő értesülés, a React, az AngularJS és az Angular mind más és más módszereket alkalmaznak a változások detektálására.
A tároláshoz a localStorage.setItem(key, value)
függvényt használjuk. A sorosítandó objektumot egyszerűen JSON-be sorosíthatjuk: JSON.stringify(object)
, illetve visszafejthetjük: JSON.parse(string)
.
Fontos, hogy a
JSON.parse()
által visszafejtett objektumok egyszerű objektumok, ha a forrás objektumunkon pl. függvények is szerepeltek, azok a deszerializált objektumon nem lesznek elérhetők!
A részfeladatok tehát:
- készítsen egy függvényt, ami elmenti a teljes todos tömb tartalmát
localStorage
-ba, - bármilyen változás hatására (elem állapotváltozása, létrejötte, törlése) mentse el a függvény segítségével az elemeket,
- alkalmazás indulásakor egyetlen alkalommal töltse vissza az összes eltárolt todo elemet, és ez legyen a
todos
változó kiinduló tartalma!
A storage tartalmát böngészőtől függően különböző helyen tudjuk megvizsgálni, jellemzően a Storage vagy Debugger fülön található.
Illesszen be egy képernyőképet a lokális tárolóban (localStorage) található perzisztált to-do elemekről!
Illessze be a releváns kódrészleteket!