WageSum Angular UI

“Bonum vinum hedera non indiget”

A latin mondás annyit jelent, hogy “Jó bornak nem kell cégér”. Ezt az egyébként bölcs mondást a marketingesek sokszor cáfolták már, így most inkább az utóbbiakra hallgatok. A backend oldali fejlesztők, tervezők, architektek egyik legnagyobb rákfenéje, hogy sokszor úgy definiálnak API-kat, hogy bár funkcionálisan megfelelő, de gyakorlatban nehezen használható végeredményt szülnek meg.

Ezt elkerülendő, néha nem árt átülni a másik oldalra is, a grafikus felületet készítők szemszögéből is vizsgálni a végeredményt. Emellett így könnyebb “eladni” a backend terméket cégen belül is.

Wagesum Angular UI

Wagesum Angular UI github-on

Ennek örömére készítettem a WageSum microservice-nek egy Angular UI-t. Miért épp Angular? Vannak más framework-ok is, de ez az egyik legelterjedtebb (oké a react még inkább az, a flutter pedig nagyon pörög, arról is érdemes majd pár szót írni), felépítése hasonlít a Spring-hez, komoly támogatás van mögötte. TypeScript nyelvet használ, ami a típusosságot kellően komolyan veszi. Sok-sok cikk született már erről, nem fejtegetem bővebben.

Személyes ok még az is, hogy még 2019-ben készítettem egy pet projectet, amit máig (2022. december) lényegében néhány npm upgrade paranccsal, különösebb változás nélkül lehetett naprakészen tartani. Ezt azért kevés UI framework mondhatja el magáról.

Természetesen egy igazi UI specialista nagyon sok hibát, rossz gyakorlatot, elavult elemet tudna találni. Jöhetnek a pull requestek. :)

Fejlesztési napló

1. Project alapok

Kezdésnek fel kell tennünk a NodeJS aktuális verzióját, majd telepíthetjük az Angular CLI csomagot is. Mindkettőt időről időre frissíteni kell, de az npm upgrade és az npm audit fix (esetleg –force kapcsolóval) parancsok gyakori futtatása segít rendben tartani a kódot security szempontból is.

npm install -g @angular/cli

ng new wage-sum-angular-ui

Itt pár kérdésre válaszolni kell. Routing-ot szeretnénk használni, CSS helyett SASS-t választottam, de igazán nem használtam semmi extrát. Már ebben az állapotban el lehet indítani az alkalmazást a ng serve -o paranccsal.

Lehet a kedvenc IntelliJ Idea-t is használni, de a webes kiegészítő már fizetős, így az ingyenes Visual Studio Code néhány pluginnal ( Angular Language Service, Sass extensions) elegendő a legtöbb igény kielégítésére.

Mivel nem vagyok jó designer, de azért szeretem, ha esztétikusan néz ki egy felület, ezért aztán a legismertebb design könyvtárat, a Material design Angular komponenseit használom. Mint később látható, ezzel nagyon kényelmes elérni a legtöbb funkcionalitást.

 ng add @angular/material
 
    ? Choose a prebuilt theme name, or "custom" for a custom theme: Deep Purple/Amber  [ Preview: https://material.angular.io?theme=deeppurple-amber ]
    ? Set up global Angular Material typography styles? No
    ? Include the Angular animations module? Include, but disable animations

    Please consider manually setting up the Roboto font.

# szeretne roboto font-ot is használni. Legyen akkor boldog:
npm install typeface-roboto --save

A generált HTML oldalt csupaszítsuk le, hogy csak az ‘App is running’ felirat maradjon, majd a Material tesztelése végett tegyünk be egy mini kapcsolót. Ez persze pirosan világít majd, mivel még app.module.ts fájlban be kell húzni a MatSlideToggleModule modult is. Ezek után már látszani fog a kis csúszókapcsoló.

<mat-slide-toggle>Toggle me!</mat-slide-toggle> 

Annak érdekében, hogy még az ng test futására a Unit tesztek is túléljenek, bizonyos tesztelő importokat a teszt app.component.spec.ts fájlban is fel kell venni.

import { MatSlideToggleModule } from '@angular/material/slide-toggle';

...
imports: [
        RouterTestingModule,
        MatSlideToggleModule
      ],
...

Remek, akkor már az alapokat le is tettük.

2. OpenAPI generation

A WageSum szerver oldalán már a kezdetekben definiáltuk a WageSum OpenAPI leírót. Ebből elég könnyedén lehetséges TypeScript/Angular klienst generáltatni. Első lehetőség a rendes openapi docker image használata:

docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate -i https://raw.githubusercontent.com/lsmhun/wage-sum-server/main/api/wagesum-openapi.yaml  -g typescript-angular -o /local/build

Amennyiben nem szeretnénk külső generálót, használhatjuk erre npm csomagot is, ahogy ebben a leírásban is szerepel.

npm add @openapitools/openapi-generator-cl

Itt kicsit lamentáltam, hogy on-the-fly generálás történjen-e, vagy inkább snapshotok legyenek. Az életben nem szereti senki, ha a production build alatt változik meg az API definíciója. Amúgy erre (is) szolgálnak a contract tesztek.

De most egyelőre ennek alkalmazásnak a generált kódja elfér a build könyvtárban, csak vegyük fel a .gitignore fájlba is.

A convention over configuration nevében az npm esetén a package.json-ban lehet felvenni script{ ... } definíciókat, ahol ha a pre vagy post prefixet alkalmazod, akkor előtte/utána fog lefutni a kért parancs. Így felvettem ide a generálást, hogy mindig megtörténjen npm build/serve/test előtt. Ez eléggé lassít, de legtöbbször úgyis az ng serve -o lesz futtatva.

...
 "generate:api": "openapi-generator-cli generate -g typescript-angular -i https://raw.githubusercontent.com/lsmhun/wage-sum-server/main/api/wagesum-openapi.yaml -o ./build/openapi",
    "prestart": "npm run generate:api",
    "start": "ng serve",
...

Ezek után már fel is vehetünk egy példa dolgozót a main komponensbe:

import { Emp } from 'build/openapi/model/emp';
...
exampleEmp: Emp = {
    userName: "man1",
    firstName: "first",
    lastName: "last",
    empId: 2,
    mgrId: 1,
    status: Emp.StatusEnum.Active,
    type: Emp.TypeEnum.Manager
};

3. Váz kialakítása, routing

Idáig eljutottunk, minden szuper, már sok minden megvan, de igazán még semmi. Hozzuk létre akkor az alap komponenseinket. Lényegében az egész egy CRUD alkalmazás, aminek létezik dolgozói és fizetés kezelő REST interfésze.

Pár nézetünk lesz csak:

  • fa nézet (emp-tree)
  • dolgozó form (emp-details)
  • 404 oldal (page-not-found)
  • fejléc (head-nav) - nem kell routing hozza
  • bejelentkező oldal (most ez még nem kell)

Ezekhez hozzuk létre a component eket.

ng generate component emp-tree
ng generate component emp-details
ng generate component emp-tree
ng generate component head-nav

Szükségünk lesz navigációra, amit legegyszerűbben az Angular routing megoldásával oldhatunk meg. Az AppRoutingModule-ba vegyük fel az alap routing célokat.

 const routes: Routes = [
  { path: 'emp-tree', title: 'Employee hierarchy', component: EmpTreeComponent },
  { path: 'emp/:id', title: 'Employee details', component: EmpDetailsComponent },
  { path: '',   redirectTo: '/emp-tree', pathMatch: 'full' }, // now redirect to `emp-tree`
  { path: '**', title: 'Page not found', component: PageNotFoundComponent },
];

A head-nav -ba vegyünk fel két linket elsőre, majd az app.component.html-ben az eddigi szöveget cseréljuk ki csak ennyire:

<app-head-nav></app-head-nav>
<router-outlet></router-outlet>

Remek, a routing már működik is, bár az alap két tesztünket elrontottuk. A hibaüzenetek alapján ezt gyorsan javíthatjuk is. Sok minden még nem történik, de már tudunk váltogatni oldalak között.

Azért, hogy Material-hoz tartozó elemek egy helyen legyenek gyűjtve, ezért ezeket a material.modules.ts-ben definiáljuk. Ezek után az app.module.ts-ben már csak ezt a “gyűjtő” modult kell behúzni és már használhatjuk is a komponenseket. Jogos felvetés, hogy ha így teszünk, akkor sok felesleges komponens is bekerül. Szerencsére, amikor a végső shuffle zajlik majd, akkor van olyan okos a fordító, hogy a nem használt elemeket kiszedi a végeredményből.

Először a app-head-nav nál érdemes kipróbálni, hogy megy-e. Aztán jöhet a többi. A következő az dolgozói adat form lehet. Elsőre nem a szépség a fő cél, csak látsszon valami, de már material design-nal.

Wagesum Angular UI - employee form

Amikor routing már működni kezd, akkor szeretnénk például az empId paramétert megkapni. Erre a @Input annotáció szolgál, de null-safety kapcsán kicsit enyhíthető a szigor, mint ahogy itt javasolják is. A tsconfig.json fájlban “compilerOptions”: {“strictPropertyInitialization”: false, ..} segítségével áthidalhatjuk a kérdést.

5. Környezetek, fa nézet, dolgozó editor

A fa nézet létrehozásához egy az egyben a Tree with dynamic data mintát vettem át, csak a statikus repository helyett már az OpenApi generált service-eket használatam, amint az emp-tree.component.ts-ben látható is.

Ezen a ponton már elkerülhetetlen valódi HTTP kérések küldése. A WageSum szerver futtatása nem fogyasztott sokat, plusz ott is előkerült pár bug.

A környezetek definiálása nem megkerülhető ezen a ponton, mivel fejlesztéshez tökéletesen elég a 127.0.0.1 cím, de production már mást kérhet. Erre build guide ad mintát, és ettől kezdve ng serve --configuration=development -o paranccsal tudjuk futtatni az alkalmazást.

A CORS miatt szükség proxy beállításra is. Ehhez én ezt a cikket használtam mankónak. Mivel a környezetek között már felvettük a BASE_PATH értéket a kívánságunknak megfelelően, de ezt még az app.module.ts -ben a providerek közé fel kell venni, hogy a generált module ennek megfelelő URL-re hívjon tovább.

  providers: [{ provide: BASE_PATH, useValue: environment.API_BASE_PATH }],

Ha Chrome-ban a debug oldalt nézzük, már látható, hogy a kérések ettől kezdve a /api címre fognak beérkezni, amit a proxy modul fog továbbítani a tényleges, BASE_PATH-ban beállított cím felé.

Wagesum Angular UI - employee details

A dolgozói form miatt a FormsModule-ra is szükségünk lesz, de egyébként itt is maradtam a legegyszerűbb form megoldásnál. Validációból kéne jóval több, de az alap látható.

Két érdekes hibába futottam bele. Az egyik, hogy ha már ki van választva egy dolgozónk, akkor sajnos nem veszi fel a routing változást (például az új dolgozó felvételére kattintva). Erre megoldásként feliratkoztam már a constuctorban a route változásokra, ahogy itt és itt javasolják.

A másik érdekesség az animációknál ért. Mivel egy-egy kérés akár sokáig is futhat, ezért ilyenkor egy “loading” ikont érdemes kirakni, hogy a felhasználó tudja, hogy működünk, csak várunk a backend válaszára. Erre vannak szép mat-progress-spinner elemek, csak sajnos nem forogtak. Aztán jött a megvilágosodás, hogy a kezdet kezdetén NoopAnimationsModule lett kiválasztva, amit le kell cserélni BrowserAnimationModule-ra, ha mozgást is szeretnénk látni.

6. Fizetéshez kapcsolódó elemek

Mostanra már működnek a dolgozói CRUD funkciók, most meg kéne mutatni a a fizukat és a bértömeget is. Végülis ezért született az egész.

Ehhez egy nagyon egyszerű, minimalista komponens is elég, amit az emp-tree.component.html-be beillesztve már láthatjuk is az eredményt. Az dolgozói formhoz képest annyi a különbség, hogy itt átadjuk az empId paramétert a @Input -tal, nem a routingból érkezik.

Emellett kell a fizetés update-hez is egy kisebb fizetés változtató komponens, ami majdnem ugyanaz, mint a dolgozói adatok változtatása, csak a salary REST interfészt hívogatja.

7. Tesztek fixálása

Amint észre lehetett venni, a TDD kicsit háttérbe szorult. Legalább a minimális unit teszteket újra életre kéne lehelni, hogy később akár bővíteni lehessen.

Ennek érdekében pár teendőnk azért akad.

Az egyik, hogy a UsedMaterialModules üt be kell húzni a tesztekbe is. Ez persze lustaság, mert nem mindegyik module-t használjuk, élesben nyilván mindegyik tesztnél csak az abban a komponensben használt elemeket importáljuk, ezzel gyorsabb is lesz.. Ahol használjük, ott ne felejtsük el a FormsModule és BrowserAnimationsModule elemeket sem.

A másik sarkalatos pont a mocking and dependency injection. A legtöbb példa a emp-details.component.spec.ts esetén megfigyelhető.

Egyik első lényeges lépés a HttpClientTestingModule import. Ez nagyon okos kis tesztelő module, megoldja a hianyzó HttpClient dependecy injektálást.

Mivel az Angular alapból az RxJS library segítségével éri el az aszinkron és reaktív működést, ezért érdemes ennek jobban is utánaolvasni. A tesztek esetén is Observable<> definíciókra lesz szükségünk.

Az egyes hívásokat ezek után a spyOn() metódussal, fluent formában lehet a választ emulálni.

    // given salService
    const salService = TestBed.inject(SalService);
    const httpEventSal: HttpEvent<string> = new HttpResponse<string>({ body: "123" });
    spyOn(salService, 'getSalByEmpId').and.returnValue(of(httpEventSal));

A sorrend fontos: előbb kell legyen az instance _TestBed.configureTestingModule()_l és csak utána lehet injektálni, meg spyOn()-t használni.

Wagesum Angular UI - karma test

Tegyünk fel egy lint -et is, ami segít kitakarítani a kódot. Amikor először próbáljuk meg futtatni, akkor felkínál ESLint-et, de bármelyik másik is választható.

ng lint --fix

A fejlesztés során is automatizáltan érdemes ezeket alkalmazni. Most csak utólagos takarításra használtam.

8. Dockerizálás

A konténerek használata minden microservice-nek része. Angular alkalmazás dockerizálása témában rengeteg példa elérhető. Az alkalmazott eset előbb az npm build segítségével legyártja a dist könyvtárba a production ready kódot, majd egy ngix konténerhez hozzáadja.

Rengeteg ideig fut, kihajtja napjaink csúcsgépeit is. Helyette lehet lokálisan buildelni egy npm run build --prod paranccsal egy normál kimenetet és ehhez gyártani egy másik Dockerfile-t.

docker build -t lsmaster/wage-sum-angular-ui:0.0.1 .