2. gyakorlat¶
Ezen a gyakorlaton folytatjuk a Contacts alkalmazásunk megvalósítását.
Összegezzük, hogy hol is tartunk:
- Van egy
Contactmodell 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-contacttemplate-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:classth: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.Iterableinterfészt - objektumok, amik implementálják a
java.util.Mapinterfé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
Contactosztá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ó:
