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.

This entry was posted in Uncategorized. Bookmark the permalink.

Leave a comment