SE-LT žodynas (35)

Dar šiek tiek patobulinsime sistemą.

Automatinis sukūrimo datos įrašymas

Įgyvendinkime funkcionalumą, kad sukūrus naują žodį būtų automatiškai užpildomas jo kūrimo datos laukas (word.date). Šis laukas iš tikrųjų turi būti valdomas sistemos, o vartotojui juo neturėtų reikėti rūpintis.

Pakeitimas šiuo atveju reikalingas backende. Žodžio esybės crud servise WordCrudService reikėtų kuriant žodį įgyvendinti tokius veiksmus:

// WordCrudService.java

if (word.getDate() == null) {
    word.setDate(LocalDateTime.now());
}

Žodžio sukūrimo data date užpildoma einamąja data veiksmu word.setDate(LocalDateTime.now()). Šis veiksmas bus įdedamas į crud serviso save() metodą, kuris kviečiamas tiek kuriant, tiek keičiant žodį; kai žodis keičiamas, kūrimo data neturi būti perrašoma vėlesne, todėl įdėta tikrinimo sąlyga — užpildyti kūrimo datą tik, jei ji dar nebuvo užpildyta (kuriamas naujas žodis).

Taigi backende galutiniai pakeitimai atrodys taip:

// WordCrudService.java

public Word save(Word entity) {
    if (entity.getDate() == null) {
        entity.setDate(LocalDateTime.now());
    }
    return wordRepo.save(entity);
}

Panaikinti žalią brūkšnelį po menu juosta

Panaikinkime žalią juostelę, kuri skiria meniu juostą nuo žodžių sąrašo komponento. Ši juostelę užsilikusi nuo pirmos aplikacijos versijos:

Tam reikia iš pagrindinio aplikacijos komponento išimti spalvos atributą background-color, priskirtą CSS klasei menu-bar:

/* app.component.css */

.menu-bar {
  grid-area: header;
  /* background-color: rgb(198, 239, 206); */
}

Dabar aplikacija atrodo taip:

Gerai įsižiūrėjus matyti, kad žalios juostelės nebėra, tačiau vietoj jos yra tarpas tarp meniu juostos ir žodžių sąrašo komponento. Taip yra dėl to, kad meniu juosta užima visą jai skirtą plotą, įskaitant ir tą tarpą, tačiau melyna teminė juosta užima tik tam tikrą fiksuotą aukštį, nustatomą pagal meniu juostoje esančius mygtukus-valdiklius.

Praplėskime meniu juostą atitinkančios CSS srities aukštį:

/* app.component.css */

.outer {
  height: 100%;
  display: grid;
  /* grid-template-rows: 1fr 8fr 8fr; */
  grid-template-rows: 1fr 10fr 10fr;
  grid-template-columns: 2fr 2fr;
  grid-template-areas:
    "header header"
    "main main"
    "main main";
}

Dabar meniu juostai tenka mažiau vietos palyginti su bendru žodžių sąrašu: santykis 1:\left(8+8\right)=1:16 pakeistas nauju santykiu 1:\left(10+10\right)=1:20. Kitaip sakant, meniu juostai tenkanti dalis sumažėjo nuo \frac{1}{17} iki \frac{1}{21}.

Aplikacija atrodo taip:

Perklausimo dialogo trinant žodį patobulinimai

Trinant žodį rodomas perklausimo dialogas atrodo taip:

Suvienodinkime mygtukų Cancel stilių — tegu jis būna toks pat, kaip šoninėse juostose esančių Clear / Cancel.

<!-- dialog.component.html -->

<mat-dialog-content>Are you sure you want to delete the word <b>{{ word }}</b>?</mat-dialog-content>
<mat-dialog-actions class="button-container">
    <button mat-stroked-button color="primary" mat-dialog-close>CANCEL</button>
    <button mat-raised-button color="primary" [mat-dialog-close]="true">CONFIRM</button>
</mat-dialog-actions>

Čia pakeitėme mygtuko CANCEL stilių iš mat-raised-button į mat-stroked-button, taip pat pridėjome spalvos atributą color="primary". Mygtuko stilius šiek tiek pakinta:

Galiausiai padarykime, kad perklausimo dialogas nebūtų uždaromas pele paspaudus už jo ribų, nes tai šiek tiek nepatogu vartotojui. Tam reikia kode, atidarančiame dialogą (komponente WordDetailsComponent), įdėti specialų dialogo atvėrimo nustatymą disableClose:

// word-details.component.ts

onDelete(): void {
  const dialogRef = this.dialog.open(DialogComponent, {
    autoFocus: false,
    disableClose: true,
    data: { word: this.selectedWord.word }
  });
  ...
}

Dabar dialogą galima uždaryti tik paspaudus kurį nors jo mygtuką.

Posted in Uncategorized | Leave a comment

SE-LT žodynas (34)

Didelis sekantis žingsnis – iškelti paieškos kriterijų komponentą SearchCriteriaComponent SC iš dabartinės padėties ir įdėti jį į bendros lentelės komponentą DatatableComponent B. Taip kaip paieškos kriterijų komponentas taps ne toks nepriklausomas, kaip dabar, o priklausys nuo jį apimančio komponento DatatableComponent.

Išėmimas iš esamos vietos

Kol kas SC visada matomas aplikacijoje:

Taip yra dėl to, kad paieškos komponentas įdėtas tiesiai į pagrindinį komponentą <app-root>, taigi jis tame pačiame lygyje kaip ir meniu komponentas bei bendro sąrašo komponentas.

Išimkime SC iš dabartinės vietos:

// app.component.html

<div class="outer">
  <div class="menu-bar">
    <dict-menu-bar></dict-menu-bar>
  </div>
  <!-- div class="search-criteria">
    <dict-search-criteria></dict-search-criteria>
  </div -->
  <div class="datatable">
    <router-outlet></router-outlet>
  </div>
</div>

Taip pat panaikinkime susijusius CSS stilius ir CSS grid apibrėžimus:

/* app.component.css */

.outer {
  height: 100%;
  display: grid;
  grid-template-rows: 1fr 8fr 8fr;
  grid-template-columns: /*1fr*/ 2fr 2fr;
  grid-template-areas:
    /*"header*/ "header header"
    /*"sidebar*/ "main main"
    /*"sidebar*/ "main main";
}

.menu-bar {
  grid-area: header;
  background-color: rgb(198, 239, 206);
}

/* .search-criteria {
  grid-area: sidebar;
  background-color: rgb(255, 204, 153);
} */

.datatable {
  grid-area: main;
}

Kadangi panaikinama šoninė juosta, kurioje būdavo SC komponentas, supaprastėja ir CSS grid aprašai: iš trijų stulpelių lieka tik du, panaikinamas pirmasis stulpelis, atitikęs šoninę juostą.

Paieškos komponentas dabar neberodomas nei iki paspaudžiant, nei paspaudus Search:

Įdėjimas į bendro sąrašo komponentą

Įdėkime paieškos komponentą į bendro sąrašo komponentą:

<!-- datatable.component.html -->

<mat-drawer-container class="container" autosize>
    <mat-drawer class="mat-elevation-z4 drawer" mode="side" position="start" opened>
        <dict-search-criteria></dict-search-criteria>
    </mat-drawer>
    <mat-drawer class="mat-elevation-z4 drawer" mode="side" position="end" [opened]="isOpen">
        <dict-word-details [selectedEntityId]="selectedEntityId"
                           (sidebarClosed)="onSidebarClosed()" 
                           (editModeOn)="onEditModeChange($event)"
                           (resetList)="onListReset()">
        </dict-word-details>
    </mat-drawer>
    ...
</mat-drawer-container>

Komponentas įdedamas ne tiesiai, o per konteinerį mat-drawer, kuris turi panašius atributus, kaip ir seniai esantis detalusis komponentas, tik reikšmė position = start, o ne position = end, kadangi paieškos juosta turi būti rodoma kairėje, o ne dešinėje. Taip pat fiksuota atributo opened reikšmė reiškia, kad paieškos juosta visada išskleista. Prie juostos išskleidimo (susskleidimo) funkcionalumo bus grįžta vėliau.

Taigi dabar vaizdas toks:

Paieškos komponento elementų vidinis išdėstymas

Toliau pagerinkime patį kairiosios juostos elementų išdėstymą.

Papildykime juostą pavadinimo eilute, kurios kairėje pusėje bus parašyta Search, o dešinėje — suskleidimo mygtukas <. Bendras vaizdas bus panašus į dešiniosios juostos — detaliojo komponento — pavadinimo eilutės vaizdą.

Juostą sudarys dvi CSS grid sritys: header ir fields, kuriose bus išdėstytos pavadinimo dalys ir paieškos valdymo laukeliai. Sritys išdėstytos vertikaliai.

Nuo juostos šonų paliekamos 25px paraštės, nuo viršaus – mažesnė, 10px, paraštė. Įvedame atitinkamus elementus:

/* search-criteria.component.css */

.outer {
    display: grid;
    grid-template-areas:
        "header"
        "fields";
}

.header {
    grid-area: header;
}

.fields-container {
    grid-area: fields;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    column-gap: 25px;
    padding: 25px 25px 0 25px;
}

Čia pridėjome CSS konteinerį outer, naują sritį header, o CSS klasei fields-container priskyrėme įvestąjį CSS srities pavadinimą fields.

Papildome komponento HTML failą:

<!-- search-criteria.component.html -->

<div class="outer">
    <div class="header">Search</div>
    <div class="fields-container">
        <mat-form-field appearance="outline">
            <mat-label>Word</mat-label>
            <input matInput [(ngModel)]="query.word" />
        </mat-form-field>
        <mat-form-field class="form-field" appearance="outline">
            <mat-label>Translation</mat-label>
            <input matInput [(ngModel)]="query.translation" />
        </mat-form-field>
        <mat-form-field class="form-field" appearance="outline">
            <mat-label>Collection Name</mat-label>
            <input matInput [(ngModel)]="query.collectionName" />
        </mat-form-field>
    </div>
</div>
...

Sukurkime pavadinimo tekstui atskirą CSS klasę ir ją pritaikykime, kad pavadinimas būtų didelis:

/* search-criteria.component.css */

.title {
    font-weight: 600;
    font-size: x-large;
    align-self: center;
}
<!-- search-criteria.component.html -->

<div class="outer">
    <div class="header">
        <div class="title">Search</div>
    </div>
...
</div>

Matyti toks vaizdas:

CSS klasei-sričiai header trūksta paraščių (jos taikomos tik srityje fields arba, kitaip sakant, CSS klasėje fields-container ). Pritaikykime:

/* search-criteria.component.css */

.header {
    grid-area: header;
    padding: 10px 25px 0 25px;
}

Dabar vaizdas toks:

Taigi juostos pavadinimas atvaizduojamas jau gerai. Toliau įdėkime juostos suskleidimo mygtuką — rodyklę <.

Panašiai, kaip padaryta detaliojo komponento atveju, CSS header paverskime flex konteineriu:

/* search-criteria.component.css */

.header {
    grid-area: header;
    display: flex;
    flex-direction: row;
    padding: 10px 25px 0 25px;
}

...

.push-right {
    margin-left: auto;
}

Taip sukūrėme CSS klasę push-right, kuri pastums rodyklės mygtuką į dešinę juostos pusę. Papildome HTML failą į header sritį įdėdami ir rodyklės mygtuką:

<!-- search-criteria.component.html -->

<div class="outer">
    <div class="header">
        <div class="title">Search</div>
        <button class="push-right" mat-icon-button>
            <mat-icon>chevron_left</mat-icon>
        </button>
    </div>
...
</div>

Dabar vaizdas toks:

Taigi pavadinimo eilutė atvaizduojama tinkamai.

Suskleidimo (išskleidimo) funkcionalumas

Įgyvendinkime galimybę suskleisti (išskleisti) šoninę juostą. Tam reikia, kad juosta aptiktų, kada buvo paspaustas rodyklės mygtukas ir siųstų signalą tėviniam komponentui, kuris suskleistų juostą.

Padarome standartinius pakeitimus: į šoninės juostos komponentą įdedame emiterį ir jį aktyvuojame iš metodo, kuris vykdomas paspaudus rodyklės mygtuką:

<!-- search-criteria.component.html -->

<div class="outer">
    <div class="header">
        <div class="title">Search</div>
        <button class="push-right" mat-icon-button (click)="onSearchbarClosed()">
            <mat-icon>chevron_left</mat-icon>
        </button>
    </div>
...
</div>
// search-criteria.component.ts

export class SearchCriteriaComponent {

  @Output()
  searchbarClosed: EventEmitter<void> = new EventEmitter<void>();

  ...

  onSearchbarClosed(): void {
    this.searchbarClosed.emit();
  }
}

Padarome pakeitimus tėviniame komponente. Įvedame naują kintamąjį, kuris žino, ar paieškos juosta šiuo metu išskleista:

// search-criteria.component.ts

export class DatatableComponent implements OnInit {
    ...

    isOpen = false;
    searchbarOpen = true;

    ...
}

Šiam kintamajam priskirkime reikšmę false, kai iš paieškos komponento ateina signalas, kad buvo paspaustas rodyklės mygtukas suskleisti juostą. Reikšmę priskirsime supaprastintai, tiesiai HTML faile:

<!-- datatable.component.html -->

<mat-drawer-container class="container" autosize>
    <mat-drawer class="mat-elevation-z4 drawer" mode="side" position="start" [opened]="searchbarOpen">
        <dict-search-criteria (searchbarClosed)="searchbarOpen = false"></dict-search-criteria>
    </mat-drawer>
    ...
</mat-drawer-container>

Taip pat šoninės juostos parametras opened dabar nebe turi fiksuotą reikšmę true, o priklauso nuo įvesto kintamojo searchbarOpen reikšmės: juosta išskleista tik, jei kintamasis lygus true.

Galima palyginti šoninės juostos vaizdus, kai ši išskleista ir suskleista:

Kol kas nėra galimybės juostos išskleisti — jai susiskleidus nėra jokio valdiklio, kuris vėl galėtų ją išskleisti. Bendro sąrašo komponente DatatableComponent reikia naujo valdymo elemento.

Esamą CSS klasę content paverskime CSS grid sričių konteineriu.

/* datatable.component.css */

.content {
    display: grid;
    grid-template-rows: 1fr 14fr;
    grid-template-columns: 1fr 44fr;
    grid-template-areas: 
        "arrow header"
        "arrow datatable";
    padding: 25px 25px 0 25px;
}

.table-header {
    grid-area: header;
    display: flex;
    flex-direction: row;
    margin-bottom: 10px;
}

.table {
    grid-area: datatable;
}

.arrow {
    grid-area: arrow;
}

Taip pat sukūrėme CSS klases naujai įvestoms sritims arrow, datatable, buvusiai klasei table-header priskyrėme srities pavadinimą header.

Dabar sričių išdėstymas yra toks:

Komponento HTML faile apgaubkime elementą <div class="mat-elevation-z4"> (pačią duomenų lentelę) naujai įvesta sritimi table/datatable; sukurkime rodyklę-mygtuką, apgautą sritimi arrow; lentelės pavadinimas lieka, kaip buvęs, nes jo CSS klasei table-header buvo priskirta sritis header:

<!-- datatable.component.html -->

<mat-drawer-content class="content">
    <div class="arrow">
        <button mat-icon-button>
            <mat-icon>chevron_right</mat-icon>
        </button>
    </div>
    <div class="table-header">
        <h1 class="title">Words</h1>
            <button mat-raised-button color="primary" class="new-button" [disabled]="editMode" (click)="onCreate()">CREATE NEW</button>
    </div>
    <div class="table">
        <div class="mat-elevation-z4">
            <!-- lentelės elementai, puslapiuoklis ir t.t. -->
        </div>
    </div>
</mat-drawer-content>

Dabar bendras vaizdas toks:

Mygtukas atvaizduojamas, tačiau kol kas jis neveikia. Jį paspaudus komponentas DatatableComponent turėtų priskirti juostos valdymo kintamajam searchbarOpen reikšmę true, nes juosta išskleidžiama:

<!-- datatable.component.html -->

<mat-drawer-content class="content">
    <div class="arrow">
        <button mat-icon-button (click)="searchbarOpen = true">
            <mat-icon>chevron_right</mat-icon>
        </button>
    </div>
    ...
</mat-drawer-content>

Dabar juostos valdymo mygtukas suskleistoje būsenoje veikia:

Paspaudus mygtuką juostoje ši suskleidžiama, o paspaudus mygtuką lentelei iš kairės juosta išskleidžiama.

Patobulinimai

Funkcionalumas veikia, tačiau galima šiek tiek patobulinti valdymo elementų lygiavimą.

  • mygtuką-rodyklę > juostoje pastumkime dešinėn, kad dešinioji paraštė būtų ne didesnė už paraštę, kad nuo juostos krašto skiriančią žemiau esančius mygtukus. Tam reikia panaikinti srities header vidinę dešiniąją paraštę, kad rodyklė nebebūtų nuo srities krašto stumiama kairėn:
/* search-criteria.component.css */

.header {
    grid-area: header;
    display: flex;
    flex-direction: row;
    /* padding: 10px 25px 0 25px; */
    padding: 10px 0 0 25px;
}
  • mygtukas-rodyklė < (bendrame sąraše) turi būti rodomas tik tada, kai juosta suskleista, ir paslepiamas, kai juosta išskleista. Pritaikykime sąlygą šio mygtuko atvaizdavimui:
<!-- datatable.component.html -->

<mat-drawer-content class="content">
    <div class="arrow" *ngIf="!searchbarOpen">
        <button mat-icon-button (click)="searchbarOpen = true">
            <mat-icon>chevron_right</mat-icon>
        </button>
    </div>
    ...
</mat-drawer-content>
  • šiek tiek pagerinkime išdėstymą bendro sąrašo komponente. Iš CSS klasės content išimkime padding atributą; įdėkime tą atributą į klases table-header, table, arrow nurodytomis reikšmėmis:
/* datatable.component.css */

.content {
    display: grid;
    grid-template-rows: 1fr 14fr;
    grid-template-columns: 1fr 44fr;
    grid-template-areas: 
        "arrow header"
        "arrow datatable";
    /* padding: 25px 25px 0 25px; */
}

.table-header {
    grid-area: header;
    display: flex;
    flex-direction: row;
    padding: 25px 25px 0 0;
    /* margin-bottom: 10px; */
}

.table {
    grid-area: datatable;
    padding: 0 25px 0 0;
}

.arrow {
    grid-area: arrow;
    padding: 10px 0 0 0;
}

Čia buvo išimtas ir atributas margin-bottom iš klasės table-header (nebereikalingas). Atributą padding perkėlėme iš CSS sričių konteinerio į atskiras jame esančias sritis, kad galėtume paraštes reguliuoti atskirai.

Dabar bendro sąrašo vaizdas toks:

Juostos išskleidimo rodyklė > yra maždaug viename lygyje su bendro sąrašo lentelės pavadinimu Words, be to, atstumai tarp GUI elementų atrodo tvarkingai.

  • paieškos juosta, kaip puslapiuoklis ir kai kurie kiti valdymo elementai, redagavimo režime turi būti išjungta. Tam reikia paieškos juostai iš bendro sąrašo komponento perduoti reikšmę editMode ir priklausomai nuos jos reikšmės įjungti arba išjungti paieškos juostos valdiklius:
 // search-criteria.component.ts

export class SearchCriteriaComponent {
  ...

  @Input()
  editMode = false;
  ...
}
<!-- search-criteria.component.html -->

<div class="outer">
    ...
    <div class="fields-container">
        <mat-form-field appearance="outline">
            <mat-label>Word</mat-label>
            <input matInput [disabled]="editMode" [(ngModel)]="query.word" />
        </mat-form-field>
        <mat-form-field class="form-field" appearance="outline">
            <mat-label>Translation</mat-label>
            <input matInput [disabled]="editMode" [(ngModel)]="query.translation" />
        </mat-form-field>
        <mat-form-field class="form-field" appearance="outline">
            <mat-label>Collection Name</mat-label>
            <input matInput [disabled]="editMode" [(ngModel)]="query.collectionName" />
        </mat-form-field>
    </div>
</div>
<div class="fields-container">
    <button mat-raised-button color="primary" class="search-button" [disabled]="editMode" (click)="onClear()">CLEAR</button>
    <button mat-raised-button color="primary" class="search-button" [disabled]="editMode" (click)="onSearch()">SEARCH</button>
</div>

Iš bendro sąrašo komponento perduodama editMode reikšmė:

<!-- datatable.component.html -->

<dict-search-criteria [editMode]="editMode"
                      (searchbarClosed)="searchbarOpen = false">
</dict-search-criteria>
  • pakeiskime atšaukimo mygtukų Cancel stilių paieškos ir detaliojoje juostoje, kad jie būtų mažiau akcentuoti, negu veiksmo mygtukai Search / Save. Tegu šių mygtukų stilius būna mat-stroked-button (ne mat-raised-button), o spalva color="primary":
<!-- search-criteria.component.html, atitinkamai ir 
     word-details.component.html -->

<div class="fields-container">
    <button mat-stroked-button color="primary" class="search-button" [disabled]="editMode" (click)="onClear()">CLEAR</button>
    <button mat-raised-button color="primary" class="search-button" [disabled]="editMode" (click)="onSearch()">SEARCH</button>
</div>

Dabar vaizdas toks:

***

Šiuo patobulinimu paieškos komponento perkėlimo iš pagrindinio aplikacijos komponento AppComponent į bendro sąrašo komponentą DatatableComponent pertvarkymas baigiamas.

Posted in Uncategorized | Leave a comment

SE-LT žodynas (33)

Pataisymai ir patobulinimai

Prieš darydami tolesnius esminius pakeitimus sistemoje (fronte) patobulinkime esamą funkcionalumą ir pataisykime kai kurias klaidas.

Šoninės juostos valdymo elementų išjungimas redagavimo režime

Šiuo metu yra įmanoma pakliūti į „šacho“ situaciją, kai vartotojas nebegali daryti jokių veiksmų: paspaudus Edit šoninėje juostoje įjungiamas redagavimo režimas ir bendrame sąraše nebegalima paspausti ant kitų žodžių. Tačiau vis dar galima suskleisti šoninę juostą; taip padarius juosta užsidaro (su ja ir mygtukas Cancel, galintis išjungti redagavimo režimą) ir sistemoje nebegalima nei atlikti kokius veiksmus, nei išeiti iš redagavimo režimo:

Vienintelė išeitis dabar – perkrauti puslapį.

Kad to būtų išvengta, kai redagavimo režimas įjungtas, rodyklė juostai susskleisti turi tapti neaktyvi; taip pat neaktyvus turi tapti mygtukas, kurį paspaudus parodoma meniu parinktys Edit / Cancel:

Įvedame papildomas sąlygas detaliojo komponento HTML faile:

<!-- word-details.component.html -->

<button mat-icon-button [disabled]="editMode" (click)="onSidebarClose()">
    <mat-icon>chevron_right</mat-icon>
</button>
<div class="title">{{ selectedWord.word ? capitalize(selectedWord.word) : 'New word' }}</div>
<button class="push-right" mat-icon-button [disabled]="editMode" [mat-menu-trigger-for]="detailActions">
    <mat-icon>more_vertical</mat-icon>
</button>
<mat-menu #detailActions="matMenu">
    <button mat-menu-item (click)="onEdit()">Edit</button>
    <button mat-menu-item (click)="onDelete()">Delete</button>
</mat-menu>

Pridėjus sąlygas [disabled]="editMode" minėti valdymo elementai tampa neaktyvūs redagavimo režime.

Tokį patį pakeitimą įveskime ir puslapiuokliui — neturi būti galima (nereikalinga) redagavimo režime vaikščioti per puslapius bendrame sąraše:

Tam tereikia pridėti tokią pat sąlygą prie puslapiuoklio:

<!-- datatable.component.html -->

<mat-paginator  [disabled]="editMode"
                [pageSizeOptions]="[5, 10, 20]"
                [pageSize]="pageSize"
                [pageIndex]="pageNo"
                [length]="total"
                (page)="onPageEvent($event)">
</mat-paginator>

Bendrame sąraše pasirinkto žodžio papilkinimas redaguojant

Pasirinkus žodį bendrame sąraše, visa eilutė pažymima melsvai. Kadangi redagavimo režime kai kurie valdymo elementai (kurių nebegalima paspausti) tampa nebeaktyvūs, vartotojui būtų aiškiau, jei ir pasirinkto žodžio eilutė pakeistų spalvą (pvz., papilkėtų) taip parodydama, kad kito pasirinkti nebegalima, kol neišeisima iš redagavimo režimo:

Šiam rezultatui pasiekti reikia įvesti papildomą sąlygą bendro sąrašo komponento HTML faile, kur eilutei suteikiama spalva:

<!-- datatable.component.html -->

<mat-row [ngStyle]="{ 'background-color': 
    word.id === selectedId ? 
        editMode === false ? '#C1D0E9' : '#DADBDD'
        : null }" *matRowDef="let word; columns: displayColumns" (click)="onRowSelect(word)">
</mat-row>

Čia pirma patikrinama, ar apskritai reikia nuspalvinti einamąją eilutę, jei taip — ar esama peržiūros (ne redagavimo) režime? Jei taip, spalvinama kaip anksčiau, melynai (#C1D0E9), jei ne — spalvinama pilkai (#DADBDD).

Detalusis komponentas iš naujo atsisiunčia žodį spaudinėjant tą patį žodį bendrame sąraše

Dabar pakartotinai paspaudus ant to paties žodžio bendrame sąraše (pasirinkus jau pasirinktą žodį), detalusis komponentas iš naujo siunčiasi tą patį žodį:

Tai nėra reikalinga. Šis bugas pataisomas detaliajam komponente, vietoje, kurioje vykdoma R operacija, įterpiant papildomą sąlygą — naujas iš bendro sąrašo atėjęs žodžio id turi skirtis nuo esamo. Tai reikš, kad buvo pasirinktas kitas žodis ir jį reikia atsisiųsti:

// word-details.component.ts

ngOnInit(): void {
  this.selectedEntityId.subscribe(id => {
    if (id) {
      if (id !== this.selectedWord?.id) {
        this.crudService.get(id).subscribe(data => this.selectedWord = data);
      }
    } else {
      this.selectedWord = {
        translations: [] as Translation[]
      } as Word;
      this.setEditMode(true);
    }
  });
}

Taigi dabar žodis bus atsisiunčiamas ne visada, o tik, jei tai kito, negu iki šiol buvo pasirinktas, žodžio id. (Taip pat įdėta apsauga nuo null — iš pat pradžių žodis nebūna pasirinktas.)

Posted in Uncategorized | Leave a comment

SE-LT žodynas (32)

Pertvarkysime atsakomybių paskirstymą tarp bendro žodžių sąrašo ir detaliojo komponentų. Kitaip sakant, pakeisime, kaip perduodami duomenys tarp šių dviejų komponentų.

Dabar struktūrinė duomenų perdavimo schema tarp bendro žodžių sąrašo (tėvinio komponento P) ir detaliojo komponento (vaikinio komponento C) atrodo taip:

Yra netgi septyni duomenų srautai (rodyklės): komponentui C komponentas P perduoda pasirinktą žodį ir redagavimo režimo būseną; komponentas C komponentui P perduoda pranešimus (įvykius) apie tai, kad žodis buvo sėkmingai išsaugotas į duomenų bazę; iš jos ištrintas; pradėtas redaguoti; redagavimas atšauktas; uždaryta šoninė juosta.

Šioje schemoje esama tam tikrų netobulumų. Redagavimo režimą valdo C, o ne P: vartotojas spaudžia Edit arba Cancel detaliajame komponente, o bendro sąrašo komponentas tiesiog apie tai turi irgi žinoti. Taigi būtų logiškiau editMode ne perdavinėti iš P į C, o atvirkščiai.

Dabar P tvarko CRUD operacijas su žodžiais gavęs signalą iš C, kad buvo paspaustas vienas ar kitas mygtukas. Tačiau vėlgi būtų logiškiau, kad tas operacijas tvarkytų C ir tik pasiųstų signalą į P, kai operacija baigiama, kad P galėtų atlikti reikalingus veiksmus iš savo pusės: užverti šoninę juostą, atnaujinti žodžių sąrašą, išjungti redagavimo režimą.

Perkėlus CRUD logiką į komponentą C, duomenų perdavimo schema galėtų supaprastėti iki tokios:

Parametro editMode valdymas atiduotas komponentui C, o P tik gauna signalus apie to parametro pasikeitimą. Tai reikalinga, kadangi kartais redagavimo režimą P turi išsijungti (įsijungti) detaliajame komponente neatlikus jokių CRUD operacijų, pvz., kai vartotojas komponente C paspaudžia Cancel arba Edit.

Kai atliekamos CRUD operacijos, naudojamas subjektas-emiteris listReset, pagal kurį P žino, kad buvo užbaigta CRUD operacija ir reikia atnaujinti žodžių sąrašą (kreipiantis į crud servisą), užverti šoninę juostą ir tuo pačiu išjungti redagavimo režimą.

Pagaliau pats komponentas C negali savęs užverti, todėl tai turi padaryti P. Bet P turi žinoti, kada vartotojas paspaudė šoninės juostos suskleidimo mygtuką (o tai nepriklauso nuo CRUD operacijų); tam paliktas emiteris sidebarClosed.

Galiausiai, kadangi C perima CRUD operacijų valdymą iš P, komponentas C turi turėti crud servisą ir kviesti jo atitinkamus metodus get, post, put, delete. Detalusis komponentas C dirba visada tik su vienu konkrečiu žodžiu-esybe, todėl jam reikia žinoti jo id. Kai norima trinti arba redaguoti esamą žodį, jo id turi ateiti iš P (pagal vartotojo pasirinktą eilutę). Todėl schemoje paliktas duomuo apie pasirinktą žodį, tik perduodamas nebe visas žodis selectedWord, o jo id — selectedId. Jei kuriamas naujas žodis, iš P perduodama null id, pagal ką C atpažįsta, kad bus kuriamas naujas žodis.

Taigi duomenų perdavimo schema perkėlus dalį atsakomybių iš P į C gerokai supaprastėja.

Pasiruošimas

Pirmiausia reikia šiek tiek paruošti kodą būsimiems pakeitimams. Dabar komponentas C pats kvies žodžių crud serviso metodus kurti, redaguoti, trinti žodžiams, tačiau jis turėtų ir atsisiųsti naujausią žodžio versiją iš duomenų bazės pats, o ne gauti ją (ar jos kopiją) iš bendro žodžių sąrašo (komponento P). Todėl reikia sudaryti galimybę crud servisui fronte ir backe atlikti read operaciją vienam žodžiui, kurio id iš anksto žinomas.

Frontende papildomas žodžių crud servisas:

// word-crud.service.ts

get(id: number): Observable<Word> {
  return this.http.get<Word>('http://localhost:8080/api/' + id);
}

// post, put, delete metodai parašyti anksčiau

Crud servisas įterpiamas į komponentą C (per konstruktorių):

// word-details.component.ts

constructor(private crudService: WordCrudService, private dialog: MatDialog) { }

Tuo tarpu backende papildomas kontroleris WordController nauju metodu findById:

@GetMapping("/api/{id}")
public WordDto findById(@PathVariable Long id) {
	Word word = crudService.findById(id);
	if (word == null) {
		return null;
	}

	return mapper.toDto(word);
}

Šie papildymai sudaro galimybę gauti ir priimti konkretų žodį žinant jo id (arba jei toks nerastas, grąžinama null). Žinoma, būtų buvę galima pasinaudoti paieškos servisu ir nustatyti paieškos kriterijų pagal id (id būtų naujas paieškos kriterijus), tačiau pagal prasmę šiuo atveju vykdoma ne laisva paieška, o reikalaujama žodžio konkrečiu id, kuris būtinai turi būti grąžintas (tik anomaliu atveju grįžta null), todėl reikiamo žodžio „pasiėmimas“ pagal id realizuotas ne kaip paieška, o kaip atskira operacija findById.

Kodas paruoštas esminiams pakeitimams.

Atsakomybių perdavimas iš P į C

R operacija

Bendro sąrašo komponentas P detaliajam komponentui C tik praneš, kada vartotojas pažymėjo konkretų žodį sąraše (kartu perduodamas to žodžio id), o nuo to momento viską turėtų tvarkyti detalusis komponentas — atsisiųsti naujausią žodžio versiją iš duomenų bazės, įjungti redagavimo režimą ir t. t. Taigi P turi perduoti duomenis komponentui C ne nuolat (kaip buvo iki šiol [selectedWord]="selectedWord"), o „impulsiškai“ (kaip įvykį).

Komponentuose P ir C sukuriame subjektą-emiterį (C pusėje — su direktyva @Input()):

// datatable.component.ts

selectedEntityId: Subject<number> = new Subject<number>();

// word-details.component.ts

@Input()
selectedEntityId: Subject<number> = new Subject<number>();

ir perduodame jį komponentui C:

<!-- datatable.component.html -->

<dict-word-details [selectedEntityId]="selectedEntityId"
                   [selectedWord]="selectedWord"
                   [editMode]="editMode"
                   (sidebarClosed)="onSidebarClosed()" 
                   (editingCancelled)="onCancelEdit()"
                   (wordSaved)="onWordSave($event)"
                   (editingStarted)="onEditing($event)"
                   (wordDeleted)="onWordDelete($event)">
</dict-word-details>

Komponente C apibrėžiamas priimamas subjektas-emiteris ir kokie veiksmai atliekami reaguojant į juo gaunamą id. C turi įsiminti reikiamus veiksmus, kai yra inicializuojamas, vadinasi, jis turi implementuoti metodą OnInit ir veiksmus aprašyti jame:

export class WordDetailsComponent implements OnInit {

  @Input()
  selectedEntityId: Subject<number>;

  ngOnInit(): void {
    this.selectedEntityId.subscribe(id => {
          this.crudService.get(id).subscribe(data => this.selectedWord = data);
    });
  }
}

Inicializuojant komponentą C susiejamas veiksmas 8 eilutėje su subjektu-emiteriu: kai tik P praneša, kad pažymėtas žodis bendrame sąraše, kurio identifikatorius id, C turi atsinešti tą žodį iš duomenų bazės kreipdamasis į crud serviso metodą get(), kurį parašėme ruošdamiesi pakeitimams.

Gautas žodis priskiriamas kintamajam selectedWord. Šis kintamasis jau nebeperduodamas iš P, todėl jam galima nuimti direktyvą @Input(), o komponento P faile išimti duomenų siejimo eilutę [selectedWord]="selectedWord". Dabar vartotojui pažymėjus eilutę bendrame žodžių sąraše komponentas C gaus signalą apie pažymėto žodžio id, jį atsisiųs iš db ir juo užpildys duomenų laukus.

P signalą perduoda šitaip:

// datatable.component.ts

onRowSelect(word: Word): void {
  if (this.editMode) {
    return;
  }

  this.selectedId = word.id;
  // this.selectedWord = word;
  this.isOpen = true;
  this.selectedEntityId.next(word.id);
}

Eilutė this.selectedWord = word tampa nebereikalinga, kadangi pats žodis nebeperduodamas (o jo ir nebereikia įsiminti), bet atsiranda nauja eilutė, kurioje subjektas-emiteris selectedEntityId siunčia signalą apie pasirinkto žodžio id.

C operacija

Kūrimo atveju vartotojas spaudžia mygtuką Create New, o kuriamas žodis id dar neturi. Tokiu atveju komponentas P gali perduoti id == null, ką C gali interpretuoti kaip norą kurti naują žodį.

// datatable.component.ts

onCreate(): void {
  /* this.selectedWord = {
    translations: [] as Translation[]
  } as Word; */
  this.selectedId = null;
  this.isOpen = true;
  this.editMode = true;
  this.selectedEntityId.next(null);
}

Iškomentuotos eilutės tampa nebereikalingos, nes šie veiksmai bus perkelti į komponentą C. Tačiau atsiranda nauja eilutė, kuria subjektas-emiteris perduoda komponentui C null id, vadinasi, norima kurti naują žodį.

Komponente C pakeičiamas metodas ngOnInit() papildant nauja sąlyga:

export class WordDetailsComponent implements OnInit {

  @Input()
  selectedEntityId: Subject<number>;

  ngOnInit(): void {
    this.selectedEntityId.subscribe(id => {
      if (id) {
        this.crudService.get(id).subscribe(data => this.selectedWord = data);
      } else {
        this.selectedWord = {
          translations: [] as Translation[]
        } as Word;
      }
    });
  }
}

Jei komponente C subjektas-emiteris priėmė nenulinį id, veiksmai atliekami kaip anksčiau (crud servisu atsinešamas žodis tokiu id iš db ir priskiriamas kintamajam selectedWord). Tačiau jei id == null, kuriamas naujas žodis ir tada kintamajam selectedWord priskiriamas naujo žodžio šablonas (logika perkelta iš metodo onCreate() komponente P).

D operacija

Anksčiau trynimas buvo inicijuojamas C, apie tai siunčiamas signalas wordDeleted komponentui P, kuris kreipdavosi į crud serviso delete() metodą ir žodį ištrindavo. Dabar darbas su crud servisu vyksta komponente C

Sąveikos su dialogu iki vartotojui teigiamai atsakant į dialogo klausimą logika lieka ta pati. Toliau užuot siuntus signalą wordDeleted pats C kviečia crud servico delete() metodą.

export class WordDetailsComponent implements OnInit {

  onDelete(): void {
    const dialogRef = this.dialog.open(DialogComponent, {
      autoFocus: false,
      data: { word: this.selectedWord.word }
    });
    dialogRef.afterClosed().subscribe(answer => {
      if (answer) {
        // this.wordDeleted.emit(this.selectedWord);
        this.crudService.delete(this.selectedWord.id).subscribe(data => {});
      }
    });
  }
}

Emiteris wordDeleted tampa nebereikalingas, taip pat galima ištrinti duomenų siejimo eilutę (wordDeleted)="onWordDelete($event)" komponento P HTML faile.

U operacija

Panašus pertvarkymas reikalingas komponente C redaguojant žodį.

Anksčiau C siųsdavo signalą wordSaved komponentui P, kuris pagal tai, ar gauto žodžio id null, ar ne, atsirinkdavo, kurį crud serviso metodą kviesti — post() ar put(). Dabar ši logika įkeliama tiesiai į komponentą C, o joks signalas nebesiunčiamas:

export class WordDetailsComponent implements OnInit {

  onSave(): void {
    // this.wordSaved.emit(this.selectedWord);
    if (!this.selectedWord.id) {
      this.crudService.post(this.selectedWord).subscribe(data => {});
    } else {
      this.crudService.put(this.selectedWord).subscribe(data => {});
    }
  }
}

Emiteris wordSaved tampa nebereikalingas, taip pat galima ištrinti duomenų siejimo eilutę (wordSaved)="onWordSave($event)" komponento P HTML faile.

Veiksmai po sėkmingos CUD operacijos

Sėkmingai atlikus CUD operacijas komponente C reikia atnaujinti žodžių sąrašą komponente P ir atlikti kitus veiksmus. Komponentas P turi žinoti, kada operacija buvo sėkmingai užbaigta, apie tai turi pranešti komponentas C. Tam naudojamas emiteris listReset, kuris įvedamas pertvarkytoje duomenų perdavimo schemoje:

export class WordDetailsComponent implements OnInit {

  @Output()
  resetList: EventEmitter<void> = new EventEmitter<void>();
}

C aktyvuoja po kiekvienos CUD operacijos:

export class WordDetailsComponent implements OnInit {

  onSave(): void {
    if (!this.selectedWord.id) {
      this.crudService.post(this.selectedWord).subscribe(data => {
        this.resetList.emit();
      });
    } else {
      this.crudService.put(this.selectedWord).subscribe(data => {
        this.resetList.emit();
      });
    }
  }

  onDelete(): void {
    const dialogRef = this.dialog.open(DialogComponent, {
      autoFocus: false,
      data: { word: this.selectedWord.word }
    });
    dialogRef.afterClosed().subscribe(answer => {
      if (answer) {
        this.crudService.delete(this.selectedWord.id).subscribe(data => {
          this.resetList.emit();
        });
      }
    });
  }
}

Tuo tarpu P pagauna signalą:

<!-- datatable.component.html -->

<dict-word-details [selectedEntityId]="selectedId"
                   [editMode]="editMode"
                   (sidebarClosed)="onSidebarClosed()" 
                   (editingCancelled)="onCancelEdit()"
                   (editingStarted)="onEditing($event)"
                   (resetList)="onListReset()">
</dict-word-details>

ir atlieka veiksmus, kuriuos jau buvome aprašę metode reset() (metodas pervardinamas į onListReset()):

// datatable.component.ts

// reset(): void {
onListReset(): void {
  this.selectedId = null;
  this.isOpen = false;
  this.editMode = false;
  this.search({});
}

Taigi C atlieka veiksmus su žodžiais per crud servisą ir praneša P, kai veiksmai baigiami, tada P nužymi pasirinktą bendro sąrašo eilutę, uždaro šoninę juostą, išjungia redagavimo režimą, atnaujina žodžių sąrašą.

Informaciniai pranešimai

Komponentas P išsaugant arba ištrinant žodį atlikdavo veiksmus, kuriuos vaizduoja šis fragmentas:

// datatable.component.ts

this.crudService.post(word).subscribe(data => {
      this.reset();
      this.snackBar.open('Data saved', '', { duration: 3000 });
});

Sėkmingai pasibaigus CUD operacijai būdavo atnaujinamas žodžių sąrašas ir parodomas informacinis pranešimas. Palyginkime su veiksmais C komponente:

// word-details.component.ts

this.crudService.post(this.selectedWord).subscribe(data => {
      this.resetList.emit();
});

Veiksmo reset() funkciją dabar atlieka metodas onListReset() komponente P, kuris kviečiamas gavus signalą iš emiterio resetList. Tačiau trūksta informacinio pranešimo. Jį gali atvzaiduoti pats komponentas C. Įterpkime į jį snackBar elementą:

export class WordDetailsComponent implements OnInit {

  constructor(private crudService: WordCrudService, private dialog: MatDialog, private snackBar: MatSnackBar) { }

}

Iškelkime pranešimų rodymo sakinius iš komponento P į atitinkamas vietas C:

export class WordDetailsComponent implements OnInit {

  onSave(): void {
    if (!this.selectedWord.id) {
      this.crudService.post(this.selectedWord).subscribe(data => {
        this.resetList.emit();
        this.snackBar.open('Data saved', '', { duration: 3000 });
      });
    } else {
      this.crudService.put(this.selectedWord).subscribe(data => {
        this.resetList.emit();
        this.snackBar.open('Data saved', '', { duration: 3000 });
      });
    }
  }

  onDelete(): void {
    const dialogRef = this.dialog.open(DialogComponent, {
      autoFocus: false,
      data: { word: this.selectedWord.word }
    });
    dialogRef.afterClosed().subscribe(answer => {
      if (answer) {
        this.crudService.delete(this.selectedWord.id).subscribe(data => {
          this.resetList.emit();
          this.snackBar.open('Data deleted', '', { duration: 3000 });
        });
      }
    });
  }
}

Metodai onWordSave(), onWordDelete() komponente P pasidaro nebereikalingi, juos galima ištrinti.

Redagavimo režimas

Kol kas redagavimo režimą vis dar valdo P ir perduoda jo reikšmę C: [editMode]="editMode". Perkelkime valdymą komponentui C.

Nutraukime šį ryšį komponente C nuimdami direktyvą @Input() nuo editMode ir priskirdami jam pradinę reikšmę false. Iš pat pradžių redagavimo režimas niekada nebūna įjungtas. Taip pat galima pašalinti duomenų siejimo eilutę [editMode]="editMode".

Dabar editMode reikšme turi rūpintis pats komponentas C. Jis nustato reikšmę į true, kai

  • vartotojas paspaudė Edit
  • pradedamas kurti naujas žodis

ir į false, kai

  • vartotojas paspaudė Cancel
  • užbaigta CU operacija (trinant redagavimo režimas neįjungiamas)
export class WordDetailsComponent implements OnInit {

  ngOnInit(): void {
    this.selectedEntityId.subscribe(id => {
      if (id) {
        this.crudService.get(id).subscribe(data => this.selectedWord = data);
      } else {
        this.selectedWord = {
          translations: [] as Translation[]
        } as Word;
        this.editMode = true;
      }
    });
  }

  onSave(): void {
    if (!this.selectedWord.id) {
      this.crudService.post(this.selectedWord).subscribe(data => {
        this.resetList.emit();
        this.editMode = false;
        this.snackBar.open('Data saved', '', { duration: 3000 });
      });
    } else {
      this.crudService.put(this.selectedWord).subscribe(data => {
        this.resetList.emit();
        this.editMode = false;
        this.snackBar.open('Data saved', '', { duration: 3000 });
      });
    }
  }

  onCancel(): void {
    this.editMode = false;
    this.editingCancelled.emit();
  }

  onEdit(): void {
    this.editMode = true;
    this.editingStarted.emit(this.selectedWord);
  }
}

Komponentui P reikia žinoti apie redagavimo režimo pasikeitimą, kad galėtų leisti (uždrausti) vartotojui rinktis kitą žodį. Aišku, kad redagavimo režimas išjungiamas po CUD operacijų, tačiau jis kaitaliojasi ir spaudant mygtukus Edit / Cancel, o tada jokios CUD operacijos nevyksta. Taigi komponentas C neišvengiamai turi perdavinėti P informaciją apie redagavimo režimo pasikeitimą.

Visa logika, kuria P pats keičia editMode reikšmę, turi būti pašalinta ir P turi priklausyti tik nuo C pranešimų apie redagavimo režimo pasikeitimą. Kol kas yra du emiteriai, editingCancelled ir editingStarted, kurie komponentui P praneša apie redagavimo režimo pasikeitimą. Šie emiteriai panaikinami vietoje jų įvedant naują boolean tipo editModeOn emiterį, kuris praneš apie naujausią redagavimo režimo būseną (įjungta arba išjungta):

export class WordDetailsComponent implements OnInit {

  /* @Output()
  editingCancelled: EventEmitter<void> = new EventEmitter<void>();

  @Output()
  editingStarted: EventEmitter<Word> = new EventEmitter<Word>(); */

  @Output()
  editModeOn: EventEmitter<boolean> = new EventEmitter<boolean>();

}

Atitinkamai panaikinamos duomenų siejimo eilutės (editingCancelled)="onCancelEdit()", (editingStarted)="onEditing($event)" ir vietoje jų įvedama (editModeOn)="onEditModeChange($event)". Atitinkamame metode komponentas P tiesiog priskiria savo lokaliai editMode kopijai reikšmę, gautą iš C.

Komponentas C visur, kur nustato editMode reikšmę, turi ją atnaujinti ir komponentui P per emiterį. Kadangi dabar visada poromis eina du veiksmai, pvz.,

this.editMode = true;
this.editModeOn.emit(true);

patogu aprašyti vieną metodą jiems abiem sugrupuoti:

setEditMode(value: boolean): void {
  this.editMode = value;
  this.editModeOn.emit(value);
}

Dabar šis metodas kviečiamas komponente C ten, kur keičiama editMode reikšmė:

export class WordDetailsComponent implements OnInit {

  @Output()
  editModeOn: EventEmitter<boolean> = new EventEmitter<boolean>();
  
  ngOnInit(): void {
    this.selectedEntityId.subscribe(id => {
      if (id) {
        this.crudService.get(id).subscribe(data => this.selectedWord = data);
      } else {
        this.selectedWord = {
          translations: [] as Translation[]
        } as Word;
        this.setEditMode(true);
      }
    });
  }

  onCancel(): void {
    this.setEditMode(false);
  }

  onSave(): void {
    if (!this.selectedWord.id) {
      this.crudService.post(this.selectedWord).subscribe(data => {
        this.resetList.emit();
        this.setEditMode(false);
        this.snackBar.open('Data saved', '', { duration: 3000 });
      });
    } else {
      this.crudService.put(this.selectedWord).subscribe(data => {
        this.resetList.emit();
        this.setEditMode(false);
        this.snackBar.open('Data saved', '', { duration: 3000 });
      });
    }
  }

  onEdit(): void {
    this.setEditMode(true);
  }
}

Komponente P pašaliname visi priskyrimo sakiniai, kur editMode priskiriama reikšmė (išskyrus, žinoma, metodą onEditModeChange()). Metodai onCancelEdit(), onEditing() nebereikalingi, kadangi juos atitinkantys emiteriai buvo pašalinti.

Tuo baigiamas sąveikos tarp komponentų P ir C pertvarkymas nekeičiant funkcionalumo.

Posted in Uncategorized | Leave a comment