2. gyakorlat¶
Ezen a gyakorlaton folytatjuk a Contacts
alkalmazásunk megvalósítását.
Összegezzük, hogy hol is tartunk:
- Van egy
Contact
modell osztályunk - Egy
home.html
és a hozzá tartozó controller (csak egy egyszerű képmegjelenítés) /contact/create
: a létrehozáshoz szükséges form (contact-create.html
), illetve a hozzá tartozó controller, mely egyszerűen logolja a hozzáadni kívánt kontaktot.- A
create-contact
template-ben használjuk a bootstrap CSS könyvtárat
A továbbiakban szeretnénk az összes CRUD műveletet támogatni, így ezeken fogunk végigmenni. Előtte azonban csinálunk egy kis refactoring-ot, mivel több template-ben is szeretnénk majd a Bootstrap-et használni, illetve a különböző oldalakon ugyanazt a menüt szeretnénk használni.
Thymeleaf fragment-ek¶
A Thymeleaf kínál megoldást erre a problémára, melyet úgy hívnak, hogy fragment. Egy fragment olyan template részlet, melyet más template-ek használhatnak (include-olhatnak), így azokat egyszerű lesz más oldalakon beágyazni.
Szervezzük ki a <head>
részben megadott Bootstrap specifikus CSS és JS includokat!
Ehhez először hozzunk létre egy mappát a resources/templates
alá fragments
néven!
Ide hozzunk létre egy header.html
állományt, mely a header fragment kódját fogja tartalmazni!
A contact-create.html
template-ből emeljük át a <head>
kódját a fragment állományba!
Ekkor a fragment tartalma a következő lesz:
1 2 3 4 5 6 7 8 9 10 11 |
|
Ez így még nem használható, mivel a fragment-eknek nevet kell adnunk!
Ehhez a <head>
elemre a következő attribútumot kell elhelyeznünk (a Thymeleaf-es XML namespace megadás mellett):
1 |
|
A th:fragment="header"
adja meg a fragment nevét.
Fontos, hogy egy ilyen állományban több fragment is definiálható, akár egymásba ágyazva is.
Miután megvan a beemelendő fragment, hivatkozzuk is azt a contact-create.html
-ból!
Ehhez a <body>
előtt a következőt kell megadnunk!
1 2 3 |
|
A fenti kód hatására a az üres <div>
-et lecseréljük a fragment/header
állományban található header
nevű fragment-re (a ::
előtti rész a fragment forrását, az az utáni pedig a nevét adja meg), mármint annak tartalmára.
A Thymeleaf többféle fragment include-ot is támogat, melyből a replace
csak egy.
A teljes lista:
replace
– lecseréli az aktuális tag-et a fragment tag-reinsert
– ugyanaz, mint a replace, de nem lecseréli a tag-et, hanem a belsejébe helyezi el a fragment kódjátinclude
– ez már deprecated, szóval ne használjuk
Miután ezzel megvagyunk, egy dolgot még meg kell tennünk. A header-ben a következőt adtuk meg:
1 |
|
Nyilván nem szeretnénk, ha az összes template-ben a 'Create Contact' szöveg jelenne meg.
Ennek megoldására használhatjuk a paraméterezett fragment-eket.
Ilyen esetben, mintha egy függvényt hívnánk, adjuk át a paramétereket a következő módon a contact-create.html
-ben:
1 |
|
A fragment-et ebben az esetben a következőképpen módosítjuk:
1 2 3 4 |
|
Fontos, hogy a th:fragment
-ben is adjuk meg a paramétert, így az IDE is tud nekünk segíteni a highlight-al.
Menü elkészítése¶
Készítsük el az alkalmazás menüjét!
Hasonlóképpen szeretnénk behúzni a menüt, azokon az oldalakon ahol szükség lehet rá, így készítsünk neki egy fragment-et a resources/templates/fragments
alá menu.html
néven!
A tartalma legyen a következő (induljunk ki egy bootstrap-es példából, majd alakítsuk azt a projektünknek megfelelően):
1 2 3 4 5 6 7 8 9 10 11 12 |
|
A navigációban két link szerepel szerepel majd, az egyik a create
-re mutat, melyet már elkészítettünk, a másik pedig a /contact
-ra, mely a listázásért lesz felelős (később készítjük el).
Ezután a menu fragment-et a contact-create.html
-ben a következőképpen használhatjuk!
1 |
|
Egy dolog azonban még hiányozhat, mégpedig a nav-on belüli most a 'Create' tagjére van elhelyezve az active
class, mely segítségével a Bootstrap más formázást ad az éppen aktív oldalnak.
A menu fragment fogadjon egy current
nevű paramétert, melyet felhasználunk a fragment-ben és csak a paraméterül kapott menüelemre aggatjuk rá az active
class-ot!
A megoldásban segítségünkre jön, ha ismerjük a következőket:
th:class
th:classappend
Ezen attribútumok segítségével feltételtől függően határozhatjuk meg egy elem class
attribútumát (feltétel nélkül is használhatjuk, de arra ott a sima class
attribútum).
Például:
1 |
|
A th:class
a teljes class
attribútumot átírja, viszont a th:classappend
hozzáfűzi az eddigiekhez a megadott értéket.
Nézzük is a módosított menu fragment-et:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Az első sorban a fragment kap egy current
paramétert, mely alapján a 8. és a 9. sorban eldöntjük, hogy melyik menüelem legyen kiemelve.
Miután ez megvan a contact-create.html
-ben szereplő felhasználást is módosítsuk:
1 |
|
Létrehozás finomítása¶
Jelen esetben, ha hozzáadunk egy új Contact
-ot az alkalmazáshoz, akkor azt szimplán logoljuk.
Ennél többre lesz szükségünk a normál működéshez.
Készítsük el a Repository
rétegünket, mely az adatok tárolásáért felelős.
Ehhez először hozzuk létre a hu.suaf.contacts.repository
package-t, majd helyezzük el bele a következőket:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
A ContactRepository
-n helyezzük el a @Repository
annotációt, mellyel jelezzük, hogy az adatok tárolásáért felel ez a komponens!
Egyszerűen a memóriában tároljuk jelenleg a Contact
-ok egy listáját.
Ennek függvényében át kell alakítanunk a ContactController
-t is, viszont jobb ha már most egy további réteget is behozunk a rendszerbe, melyet szokás service rétegnek nevezni.
A service rétegben adjuk meg majd az összes üzleti logikát, ami egy telefonkönyv alkalmazásnál nem féltétlen a legkomplexebb, de magát a koncepciót jól be tudjuk mutatni rajta.
Készítsük el a hu.suaf.contacts.service
package-t, melyen belül a ContactService
a következőképpen néz ki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Jelen pillanatban, annyit csinálunk, hogy a contactRepository
-t injektáljuk, illetve az addContact
-ban továbbítjuk a kérést a repository-nak.
Ezután a ContactController
createContact
metódusát a következőképpen módosítjuk:
1 2 3 4 5 |
|
természetesen immár a contactService
-t be kell injektálnunk ebbe az osztályba (mondjuk egy setter injektálással):
1 2 3 4 5 6 |
|
Kontaktok listázása¶
Nézzük mire van szükségünk ahhoz, hogy a kontaktokat kilistázzuk egy újabb oldalon!
Elsőként szükségünk van a kontaktok listájának lekérésére a ContactRepostiry
-ban:
1 2 3 |
|
A fenti kód egyszerűen visszaadja a memóriában tárolt Contact
objektumok listáját.
Hasonlóan a ContactService
csak továbbítja a kérést a repository felé:
1 2 3 |
|
Ezek után nézzük, hogy a ContactController
mit is kell, hogy csináljon:
1 2 3 4 5 |
|
Amikor a /contact
URL-re érkezik kérés (az osztályon a @RequestMapping("/contact")
annotáció szerepel), akkor lekérjük a kontaktok listáját melyet a model
számára átadunk mint attribútum (contacts
néven), melyet így aztán a view-ban fel tudunk használni.
Miután ezt a beállítást megtettük átadjuk a vezérlést a home.html
template-nek, hogy megjeleníthesse a generált HTML tartalmat a böngészőben.
A home.html
eddig csak egy képet tartalmazott, melyet most teljesen lecserélünk, hiszen szeretnénk behúzni a header
és a menu
fragment tartalmakat is. illetve magukat a kontaktokat megjeleníteni.
Ennek tükrében a home.html
tartalma a következőképpen alakulhat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
A kódban az újdonság a th:each
alkalmazása, mellyel a Thymeleaf-ben iterálhatunk végig egy-egy kollekción, pontosabban a következőkön lehet alkalmazni a th:each
-et:
- objektumok, amik implementálják a
java.util.Iterable
interfészt - objektumok, amik implementálják a
java.util.Map
interfészt - tömbök
A ContactController
-ben átadott modell attribútumot a ${contacts}
-el érhetjük el, mely tartalmát rendre felveszi a contact
változó.
Az iterálás során használhatunk egy status változót is, mely mindenféle hasznos infot ad az iteráció során:
- index: az aktuális index (0-tól kezdődően)
- count: az eddig feldolgozott elemek száma
- size: az iterált objektumban lévő elemek száma
- even/odd: az iteráció indexe páros/páratlan
- first: ellenőrzésre szolgál, hogy az aktuális iteráció az első-e
- last: ugyanaz, mint az előző csak az utolsó elemre
Példa a használatára:
1 |
|
Ennek eredményeképpen minden páratlan indexű sor félkövérrel lesz formázva.
Figyeljük meg, hogy a th:each
-en belül vesszővel elválasztva deklaráljuk a status
változót.
Amennyiben a each-n belül vesszővel megadunk egy további változót is, akkor az lesz a status
változónk.
Amennyiben nem definiáljuk külön, hogy milyen néven szeretnénk elérni az iteráció státuszát, akkor is létrejön egy implicit változó, melyet a rendszer a következő néven hoz létre: iterációs változó (contact
) neve + Stat
postfix.
Jelen esetben tehát contactStat
lett volna a neve ennek a státusz változónak.
Ezek után próbáljuk is ki a funkciókat!
Törlés¶
Miután tudunk létrehozni és listázni, jól jön, ha egy-egy kontaktot tudunk törölni is.
A ContactRepository
képességeinek bővítéséhez a következőket tehetjük:
1 2 3 |
|
A ContactService
ebben az esetben is csak továbbadja a kérésünket:
1 2 3 4 |
|
A törlést id
alapján valósítottuk meg.
A törlésnek nem kell külön lapot készítenünk, azt a listázás közben egy további oszlopban jelenítjük majd meg.
Először nézzük meg magát a ContactController
-t, hogy hogyan kezeli a törlésre kapott kérést:
1 2 3 4 5 |
|
Az első érdekes dolog a @PathVariable
és a hozzá tartozó id
paraméter az URL-ben (/delete/{id}
).
Az URL-ben a paramétereket mindig a {...}
jelek közé írjuk és amit a kapcsos zárójelek közé írunk az lesz a paraméter neve.
Ez azért fontos, mert név alapján történik a megfeleltetés, azaz a deleteContact(@PathVariable long id)
-ban megadott id név párt alkot az URL-ben megadott {id}
-val.
Amennyiben nem használjuk ezt a lehetőséget, akkor a @PathVariable
-ben kell megadjuk az URL-ben szereplő paraméter nevét.
Például:
1 |
|
A deleteContact
többi része nem jelenthet problémát, hiszen az csak továbbítja a kérést a contactService
felé.
Ezután alakítsuk ki a template-et úgy, hogy az utolsó oszlopban, legyen egy kis szemetes kuka, melyre kattintva a /contact/delete/{id}
-ra küldjünk egy kérést.
Megjegyzés
Mivel nem tudunk DELETE
kérést küldeni, így muszáj vagyunk egy GET
kéréssel elrendezni ezt a dolgot, mely jelen esetben egy egyszerű linkre való kattintással történik.
A home.html
-t bővítsük úgy, hogy annak fejlécébe bekerüljön egy extra oszlop, melynek neve Actions
!
1 2 3 4 5 6 7 8 9 10 |
|
A táblázat body részében pedig adjuk meg a kuka ikont egy linken belül, melyet ha megnyom a felhasználó, akkor a delete URL-re megyünk:
1 2 3 4 5 6 7 |
|
Az svg ikont a Bootstrap Icons oldalról szereztem be.
Természetesen használható a <i class="bi bi-trash"></i>
forma is csak ekkor a header
fragment-be be kell illeszteni a megfelelő CSS-t, illetve más ikon forrás is alkalmazható, pl. Font Awesome.
Amit érdemes megfigyelni, hogy az alkalmazás context root-hoz megadott útvonalban, hogyan használhatok paramétereket: th:href="@{/contact/delete/{id}(id=${contact.id})}
.
Itt az eddig megszokott módon megy minden, aztán következik a {id}
az URL-ben, ami igencsak emlékeztet a controller-ben használt paraméter megadáshoz.
Ezután a fragment-eknél is látott paramétermegadási módot használhatjuk, azaz kerek zárójelek között megadjuk az összes paramétert név=érték
felsorolásban.
Figyelem
A th:href="@{/contact/delete/${contact.id}}"
nem fog működni!!!
Módosítás¶
A módosításnál néhány trükk elő fog még jönni.
Első körben nézzük megint csak a ContactRepository
-t, melyet a következő metódusokkal bővítettünk:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Először is biztosítunk egy lekérdezést, ami id alapján megkeres egy Contact-ot. A második a létező kontakt módosítása/updatelése a kapott adatokkal. Ebben az esetben az id-t használjuk fel, mint biztos pont.
A ContactService
továbbra is nagyon egyszerűen csak továbbítja a kéréseinket:
1 2 3 4 5 6 7 |
|
Itt kezdenek a dolgok egy kicsit érdekesebbé válni.
A ContactController
új metódusai:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
A kontaktok listájában a delete mellé egy kis szerkesztő ikont is elhelyezünk, melyre kattintva bejön a kontakt szerkesztő oldal, aminek nyilván biztosítani kell majd a meglévő adatokat, hiszen ezeket szeretné szerkeszteni a felhasználó.
Ezeket el is helyezzük a model
objektumban mint attribútum.
Ezután viszont nem egy új oldalra kalandozunk, hanem magára a létrehozó oldalra megyünk.
Mivel a kontaktot beállítottuk attribútumként és a template pontosan egy contact
nevű attribútummal dolgozik, így a meglévő értékeket ki is tudja olvasni a rendereléskor (azaz inicializálja, feltölti a HTML inputokat ezekkel az értékekkel).
POST kéréskor egyszerűen csak meghívjuk a service réteget, majd átirányítjuk a felhasználót a listázó oldalra.
Végül lássuk a home.html
ide vonatkozó részét!
1 2 3 4 5 |
|
Az ikon megint egy svg, de lényeg ismét a th:href
-ben található, ahol a törléshez hasonló módon adtuk meg a paraméterezést.
Van azonban még egy probléma: a create és az edit is a contact-create.html
-re visz, de akkor ott honnan tudjuk, hogy hova küldjünk kérést (új létrehozása, vagy létező mentése)?
Nézzük a trükköt:
1 |
|
Itt megvizsgáljuk, hogy az id-ja adott-e a contact
objektumnak.
Amennyiben az null
, akkor a /contact/create
-re megyünk, a másik esetben viszont a /contact/edit/{id}
URL-re küldjük a POST kérést.
Egyéb dolgokat is tehetünk ettől függővé. Például a form submit gombjának szövegét:
1 |
|
Form Validation¶
Jelenleg új kontakt létrehozása esetén nincs semmilyen jellegű input validáció.
A felhasználó, ha úgy tartja kedve akkor üresen hagyhatja a Name
mezőt, vagy helytelen emailt ad meg, stb.
Több opciónk is van az inputok validálására.
Az egyik, hogy a POST kéréshez tartozó controller metódusban validálunk és rengeteg if
-el mindent megvizsgálunk.
Ez érezhető, hogy nem túl célravezető.
Kliens oldalon próbálkozhatunk JavaScript használatával, de az csak kliens oldalon validálna (pl Postman-el bárki küldhetne érvénytelen form-data-t), ami megint csak nem túl jó megoldás (persze kombinálhatjuk szerver oldali validációval).
A Spring szerencsére támogatja a Java Bean Validation API-t.
Ez az API, mint látni fogjuk igen kényelmessé teszi a validációt, mivel a szükséges feltételeket, vagy szabályokat közvetlenül a model-jeink field-jeire adhatjuk meg annotáció segítségével.
A Spring spring-boot-starter-web
dependency automatikusan behúzza azokat a dependency-ket amik ehhez kellenek, így nincs semmi függőség, amivel foglalkoznunk kellene.
Tipp
Ha mégis olyan verzióba botlunk, ahol a szükséges függőségek nem lettek automatikusan behúzva, akkor adjuk hozzá a pom.xml
megfelelő részéhez a következőt:
1 2 3 4 |
|
Így az alkalmazásunkban mindösszesen a következőket kell tennünk:
- Alakítsuk át a
Contact
osztályunkat úgy, hogy a megfelelő validációkkal legyenek ellátva a szükséges adatok. - Meg kell adnunk azt a pontot ahol a validációt el szeretnénk végezni.
- Módosítanunk kell a view-t, hogy a validációs üzeneteket megjeleníthessük a felhasználó számára.
Contact osztály validációja¶
Kezdjük is a kóddal:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
A 6. sorban megadjuk, hogy a name
nem lehet null
, továbbá a 7. sor azt is kiköti, hogy legalább 3 hosszúnak kell lennie a névnek.
A message
részt a view-n fogjuk felhasználni, ugyanis ez fog megjelenni hibaüzenetként, amennyiben az előírást sérti a megadott érték.
Jelen pillanatban a telefonszámra semmilyen megkötést nem teszünk, viszont az email
-re ráaggattuk a @Email
annotációt, mely valid email felépítést vár el (pl. legyen benne kukac, előtte és utána is legalább egy karakter, stb.).
A két dátum típusú adattagra a @Past
és a @PastOrPresent
annotációkat helyeztük el, melyek rendre azt ellenőrzik majd, hogy a megadott dátum múltbéli, illetve múltbeli vagy éppen a jelen.
Miután ezzel megvagyunk, valahol meg is kell hívnunk magát a validálást.
Ezt a Controller-ben érdemes megtennünk, mégpedig a createContact
és az editContact
metódusokban:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
A két metódus egészen hasonló, de más service metódust hívnak.
Lényeges a @Valid
annotációt észrevenni, hiszen mindössze ennyi szükséges ahhoz, hogy a validáció megtörténjen.
A BindingResult
paramétert a rendszer automatikusan biztosítja a számunkra.
Ezen keresztül elkérhetjük, hogy volt-e hiba a validáció során.
Mivel a HTML oldalon szeretnénk kiíratni a hibaüzeneteket a megfelelő helyeken, így az a következőképpen módosul:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Ami érdekes ebben az a th:if="${#fields.hasErrors('email')}"
rész.
Itt a #fields
-et a rendszer automatikusan adja a számunkra, melytől megkérdezhetjük, hogy az aktuális field-en lépett-e fel hiba.
Amennyiben igen, akkor a th:errors="*{email}"
résszel a span
belsejébe bele is írjuk magát a hibaüzenetet, vagy üzeneteket.
A formázásokhoz létrehozhatunk saját CSS-eket, például a resources/static/css/style.css
állományt, melynek tartalma:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Ezt pedig a header
fragment-en belül tudjuk elhelyezni a megszokott módon:
1 |
|
Feladat
Próbáljuk ki, hogy az egyes validációs annotációk, illetve azok kombinációi milyen feltételeket szabnak meg - milyen adatokat fogad el a form-unk!
További anyagok¶
A gyakorlat anyagáról készült videó: