Angular 9 Validation ve Parent Child İlişkisi ile AG-Grid Data Binding Tutarlılığını Sağlamak

Selamlar,

Bu makalede, bir Angular projesinin birçok yerinde kullanılabilecek bileşenleri yani Child Componentleri, eskilerin tabiri ile UserControlerları, nasıl oluşuturabileceğimizi ve model binding bazında nasıl haberleşebileceğimizi tartışacağız.

Image Source: https://atomicdesign.bradfrost.com/images/content/atomic-design-process.png

Bu projede Angular 9 ve Child Component olarak ag-Grid bulunan bir sayfa kullanılmıştır. Bana göre, ag-Grid birçok bileşenin birleşiminden oluştuğu için Atomic Design‘a göre, bence Molecules’e karşılık gelmektedir. Bu sayfa, üzerinde kullanılan bileşenlerin complexity’sine göre değişkenlik göstermektedir. Bir projede listeleme, özellikle Admin sayfalarda vazgeçilmez bir bileşendir. O zaman gelin, bir kere yazıp her yerde kullanabileceğimiz bir grid’i kodlamaya başlıyalım.

Angular Projesi Yaratma

Bu projede Angular versiyonu olarak 9.1.1 kullanılmaktadır.

Aşağıdaki komut ile yeni Angular 9 projesi yaratılır.

Burada tek sayfalık yani single page, kurs kayıtlarının girileceği bir form yazılacaktır.

src/app/Models/course.ts: Kaydedilecek kurs model’i, aşağıdaki gibidir.

Kurs Formu Giriş Ekranı Template Validation: 

Pristine & Untouched & Dirty & Valid : 

Bu projede, Angular üzerinde Template-driven Form şeklinde bir sayfa oluşturulup validation işlemleri yapılacaktır. Ama öncesinde belli başlı terimlerin bilinmesinde fayda vardır.

  1. Pristine : Aslında saf olan bozulmamış anlamına gelmektedir. Dirty’nin tersidir. Kısaca, bir input’un içine bir karakter yazıldığı zaman, artık geri dönüşü yoktur. O artık, Dirty olmuştur. Yazılan karakter silinse dahi, Pristine değeri false döner.
  2. Untouched : Pristinden farklı olarak, içine herhangi bir şey yazılmamış ise dahi blur’unda yani inputdan ayrılınca, o artık Touched’dır. Untouched değeri, false döner.
  3. Dirty: İnput içine bir şey yazılmadan boş atlanır ise, Pristine özelliği bozulmaz ve Dirty false döner. Ancak input içine yazılan bir karakterden sonra, dönüş yoktur ve o artık Dirty’dir. Bu durumda dirty değeri true döner.
  4. Valid: İlgili html element için, her hangi bir zorunluluk getirilmiş ise mesela required validator directive‘i atanmış ise ve o alan boş geçilir ise Valid değeri false döner.

src/app/app.component.html: Kaydetme işleminin yapılacağı form aşağıdaki gibidir. Validation olarak, Template-Driven Forms kullanılmıştır.

  • <form class=”form-horizontal” #myForm=”ngForm” (ngSubmit)=”saveForm(myForm.form)”>” : Kaydetme işlemi yapılacak elementler <form> ile sarmalanır.
    • Bu örnekte form #myForm şeklinde tanımlanmıştır. Ve validation amaçlı “ngForm” sınıfı atanmıştır.
    • Formun submit edilmesi olayında (ngSubmit) event’i tetiklenecektir. Ve buna bağlı olarak saveForm(myForm.form) methodu, form parametresi ile çağrılacaktır.
  • Bu projede Css olarak bootstrap kullanılmıştır. Aşağıdaki komut ile bootstrap projeye eklenmektedir. Kullanılan style’lar, bootstrap’e aittir.

  • <input type=”text” id=”Name” name=”Name” [(ngModel)]=”newCourse.Name” #Name=”ngModel” required>” : Burada input alan [(ngModel)] ile çift yönlü olarak, newCourse modelinin Name alanına bağlanmıştır.
    • Form içinde kullanıldığı için #Name=”ngModel” şeklinde tanımlanması şarttır. 
    • Ayrıca required işaretlemesi ile zorunlu bir alan olduğu belirtmiştir. Form içinde kullanılan bu ngController’ın zorunluluk şartı sağlanmadıkça, Form asla valid yani geçerli olmayacaktır.
    • input alan’a, mutlaka bir name‘in atanması gerekmektedir. Bu şekilde html element’e erişilebilir.
  • <div *ngIf=”Name.touched && Name.errors”>” : Buradaki divin görünmesi için, Name alanına girilmiş ve bir tanımlanan kurallara uymayan bir durum (errors) var ise yani Valid değil ise gösterilsin koşulu konmuştur.
    • <div class=”alert alert-danger” *ngIf=”Name.errors.required”>İsim Alanı Zorunludur!</div>” : Bu kısım hataların yazıldığı bölümdür. Koşul olarak, Name input alanında error’lardan required hatası var ise bu hata yazılır.
  • <h4>{{ ‘Pristine:’+ Name.pristine }}, {{ ‘Untouched:’+ Name.untouched }}, {{ ‘Dirty:’+ Name.dirty }}, {{ ‘Valid:’+ Name.valid }}</h4>” : Örnek amaçlı Name alanı için, Pristine, Untouched, Dirty ve Valid state durumlarının gösterilmesi amaçlanmıştır.
  • <input type=”text” id=”Price” name=”Price” [(ngModel)]=”newCourse.Price” #Price=”ngModel” required numbersOnly>” :Name alanı için yapılan benzer tanımlamalar, Price alanı için de yapılmıştır. Buradaki tek fark, numbersOnly custom directive’i dir. Makalenin devamında ona da değinilecektir. Amaç, input alana sadece sayısal verinin girişinin sağlanmasıdır.
  • <input type=”text” id=”TotalHours” name=”TotalHours” [(ngModel)]=”newCourse.TotalHours” #TotalHours=”ngModel” required numbersOnly>” : Price icin yapılan tüm tanımlamalar, TotalHours için de yapılmıştır.
  • <input type=”submit” class=”btn btn-primary” value=”Kaydet” style=”width: 100px;” [disabled]=”!myForm.form.valid”>” : Burada tanımlanan Kaydet Button’u, #myForm.form üzerinde, ngModel olarak tanımlı tüm nesnelerin validation durumuna göre, valid state değeri değişir. Yani form üzerinde ngmodel olarak tanımlı elementlerin bir tanesi bile valid değil ise, #myForm.form.valid değeri false olur. Bu durumda Kaydet buttonuna tıklanamaz. Çünkü [disabled] =true değerini dönmektedir.
  • <input type=”button” class=”btn btn-warning” value=”Geri Al” style=”width: 100px;” (click)=”reverseBack()”>” : Burada, güncellenecek datanın ilk değeri “Shallow Copy”  :) ile saklanmakta ve istendiğinde yapılan güncelleme adımları geri alınarak ilk haline dönülebilmektedir. Tıklanma (click) eventinde çağrılan, “reverseBack()” methodu bu işe yaramaktadır.
  • Orginal :{{ orginalCourse | json}}<br>Updated : {{ newCourse | json}}” : Sayfanın sonuna yazılan güncellenecek datanın ilk ve son hali, json olarak test amaçlı ekrana basılır.

app/app.component.html :

Arka tarafta neler dönüyor :

app/onlyNumberDirective.ts: Sadece sayısal verinin, tanımlı input alanlara girilmesini sağlamak amacı ile yazılmış custom directivedir. Ayrca app.modules’de ==> “declarations: [.., OnlyNumberDirective]” altında da tanımlanması gerekmektedir.

app/app.component.ts : Bu bölümde, şimdilik sadece kaydetme fonksiyonu tanımlanacaktır.

  • courseList = [new Course(‘Angular’, 2400, 36, 1), new Course(‘.Net Core’, 4000, 200, 2) …” : Bu makalede, server side tarafa kod yazılmadan işlem yapılacak datalar, Dummy olarak oluşturulmuştur.
  • public saveForm(form: FormGroup) {” : Form’un submit durumunda “(ngSubmit)=”saveForm(myForm.form)” bu method, form parametresi ile çağrılmaktadır.
  • if (this.newCourse.Id == null) {” : Save methodu Insert ve Update olarak 2 göreve sahiptir. Eğer kaydedilecek model yani course’ın Id’si varsa, bu bir güncellemedir. Değilse yeni kayıt girişidir.
  • this.newCourse.Id = Math.max.apply(Math, this.courseList.map(function (course) { return course.Id; })) + 1” : Tüm courseList içinde, maximum Id değeri 1 arttırılıp, yeni insert edilecek course örnek amaçlı verilmiştir. Normal şartlar altında, bu Id değeri kullanılan DB tarafından örnek Sql-Oracle otomatik olarak verilebilir.
  • this.courseList.push(this.newCourse)“: Yeni kaydedilen course, courseList’e eklenir.
  • form.markAsUntouched()” : Burada amaç, yeni kayıt girildikten sonra form temizlenir. Form içindeki tüm elementlerin, touched stateleri temizlenmez ise touched durumları true kalacağı için, kaydetme işleminden sonra tüm hata mesajları sayfada görünecektir. Örneğin, yeni bir kayıt girildikten sonra “Name” alanı boşaltılacağı için ve touched state’i true olduğu için required hatası ekrana basılır. Bunu engellemek için form içindeki elementlerin stateleri, formun ilk yüklendiği haline geri döndürülür.
  • this.newCourse = new Course()” : Form üzerindeki tüm elementler model’e iki yönlü bağlandığı için, model sıfırlanınca tüm elementler yeni bir kayıt girişi için temizlenmiş olur.

Şimdi gelin, makalenin esas amca olan konuyu inceleyelim.

Child Control ag-Grid :

Burada amaç, projenin birçok yerinde kullanılabilecek bir nesneler topluluğunun, shared bir ortamda oluşturulması ve gereken her yerde kod tekrarına gidilmeden kullanılmasıdır.

Aşağıdaki komut ile ilgili ag-Grid community ücretsiz versiyonu, projeye yüklenir. Bu projede kullanılacak grid ag-grid’in ücretsiz versiyonu olan ag-grid-community’dir.

app/datagrid.component.html: Aşağıda görüldüğü gibi, tanımlanması gereken bazı propertyleri bulunmaktadır.

  • class=”ag-theme-blue”” : Görsel amaçlı, kendi sayfasından da görülebilecek bir tema seçilmiştir. İlgili thema, style.scss altına ayrıca tanımlanmalıdır.
  • [rowData]=”rowData”” : Grid’e basılacak data, bu property altında tanımlanır.
  • [columnDefs]=”columnDefs”” : Grid’e ait kolon başlıklarının tanımlandığı, propery’dir.
  • [pagination]=”true” [paginationAutoPageSize]=”true”” : Sayfalama açılmış ve sayfa başına data miktarı otomatik verilmiştir.
  • enableSorting enableFilter” : Kolona göre filitreleme ve sıralama özellikleri açılmıştır.
  • (selectionChanged)=”onSelectionChanged($event)”” : Yeni bir satır seçildiğinde, onSelectionChanged() methodu çağrılarak, seçilen kaydın parent’a gönderilip güncellenmesi sağlanmaktadır.
  • (gridReady)=”onGridReady($event)”” : Grid yüklendiği zaman, grid nesnesi ==>gridApi” nesnesine column’u da ==>gridColumnApi” nesnesine aktarılır.

app/style.scss : ag-grid için tanımlanması gereken stiller, buraya eklenir. Bu örnekte “ag-theme-blue” seçilmiştir.

@Input & @Output & EventEmitter:

app/datagrid.component.ts : Projenin istendiği bir yerinde kullanılacak bu grid, dinamik olmalıdır. Yani gösterilecek data, kolon isimleri ve seçim işleminden sonra çalıştırılacak method(), değişken olmalıdır.

  • @Input() rowData: []” : Bir child component’ın alacağı parametrik değerler “@Input()” ile tanımlanır. Bu örnekte, grid’de gösterilecek rowData[] parametrik olarak alınmıştır.
  • @Input() columnDefs: []” : Yine aynı şekilde, gösterilecek dataya ait kolon isimleri, parametrik olarak alınmıştır.
  • @Output() selectedData = new EventEmitter()” : Bir child component’ın dışarı ile etkileşimi, @Output() ile olmaktadır. Yani bu makalede, seçilen satır parrent component’daki “selectedDataevent‘ini tetiklemektedir.
  • selection = ‘single’” : Grid üzerinde seçim,  her seferinde tek satır ile sınırlandırılmıştır.
  • private gridApi; private gridColumnApi” : gridApi ==> ag-grid nesnesini, gridColumnApi ise grid üzerindeki kolonları temsil etmektedir.
  • onSelectionChanged(params) {” : Bir satır tıklandığı zaman, çağrılan methoddur.
  • var selectedRows = this.gridApi.getSelectedRows()” : Seçilen satır, gridApi nesnesinden alınır.
  • document.querySelector(‘#selectedRows’).innerHTML = selectedRows.length === 1 ? selectedRows[0].Id : ”” Test amaçlı seçilen satırın CourseID’si, selectedRows id’li bir span içine yazdırılır.
  • “this.selectedData.emit(selectedRows[0])” : İçinde açıldığı parent sayfanın selectedData eventi, selectedRows[0] yani seçilen satır’ın datası, paramtrik olarak verilerek tetiklenir.
  • onGridReady(params) { this.gridApi = params.api; this.gridColumnApi = params.columnApi; }” : Grid yüklendiği zaman, gridApi ve gridColumnApi nesneleri atanır.

Şimdi gelin isterseniz ilgili grid’i, app.component.html’e child olarak ekliyelim:

app/app.component.html (2):

Aşağıda görüldüğü gibi, ayrı bir component olarak yazılan “app-grid“, app.component sayfasına attach edilmiştir. İlgili component’ın her çağrılan sayfada çalışabilmesi için, o sayfaya özgü parametrelerin, datagrid component’a verilmesi gerekmektedir.

  • [rowData] : Gride verilecek, listelenen course datasıdır.
  • [columnDefs] : Grid’in kolonlarının başlıklarıdır.
  • (selectedData)=”getSelectedData($event)” : İlgili grid içinde bir satır seçilince, EventEmitter sayesinde, parent componenet’deki selectedData event’i tetiklenir.

Data Model Change Detection Child Component :

app/app.component.html (2): Geldik makalenin en can alıcı kısmına. Parent Component’de değişen modelin, Child componentde anlaşılması gerekmektedir.

  • public saveForm(form: FormGroup) //INSERT : this.updateCourseList(form)” : Makalenin başında tanımlanan Insert işlemindeki “newcourseList.push(this.newCourse)” satırı silinmiştir. Nedeni, bu değişimi Child Component datagrid’in fark etmemesidir. Kısacası yeni kayıt eklendiğinde ilgili data model change’in fark edilememesinden dolayı, altındaki grid’e eklenemez. Bunun çözümü, hem Insert hem Update’de kullanılan “updateCourseList()” methodundadır.
  • public saveForm(form: FormGroup) //UPDATE {“: Var olan bir kayıt güncellendiğinde çalıştırılan methoddur.
  • let course = this.courseList.filter(list => list.Id == this.newCourse.Id)” : Güncelleme işlemi yapılan Kayıt, Id değerinden filitrelenerek alınır.
  • var updateCourseIndex = this.courseList.indexOf(course[0])” : Güncelenen kaydın, courseList içindeki index’i bulunur.
  • if (updateCourseIndex > -1) { this.courseList.splice(updateCourseIndex, 1)” : Index bulunmuş ise, ilgili kayıt listeden çıkarılır.
  • “this.updateCourseList(form)” : Insert durumunda olduğu gibi, Update durumunda da ilgili method çağrılır.
  • updateCourseList(form) : Amaç “courseList[ ]” data güncellendiği zaman, bu değişimin @Input ile parametre olarak alan datagrid’in de anlamasıdır. Insert ve Update işlemlerinde aynı method, “form” parametresi ile çağrılır.
    • let newcourseList = this.courseList.slice(0)” : Öncelikle liste, DeepCopy şeklinde başka bir listeye kopyalanır.
    • newcourseList.push(this.newCourse)” : Yeni eklenen ya da güncelenen data, listeye eklenir.
    • “form.markAsUntouched()” : Yukarıda da bahsedildiği gibi, form’un tüm elementlerinin untouched durumları sıfırlanır.
    • “this.newCourse = new Course()”:  app.component.html’de tüm html elementler newCourse model’e bağlıdır. Bu şekilde tüm değerler sıfırlanır.
    • “this.courseList = newcourseList” : Child component’e, parametre olarak verilen model courseList’dir. Bu listeye yeni bir course eklendiği zaman, child component bu değişimin farkına varmamaktadır. Birçok farklı yolu olsa da ben bana en hızlı gelen bu yolu, courseList’e direk yeni bir list atayarak referans değerini değiştirmeyi ve bu sayede child component’in değişikliği fark etmesini sağladım. Aksi takdirde yeni eklenen course, courseList’e eklenmesine ragmen child component olan grid’de gözükmeyecektir.
  • “public getSelectedData(event) {” : Yukarıdaki büyük resme bakarsanız, grid üzerinde bir kayıt seçildiği zaman (selectedData) EventEmitter sayesinde tetiklenmektedir. Bunun sonucunda “getSelectedData()” methoduna, grid üzerinde seçilen course  datası parametrik olarak gönderilmektedir.
    • “this.newCourse = event” : Bu atamada seçilen kayıt app.component.html’deki formda, model binding durumundan dolayı, güncellenecek bir data şekilde gözükecektir.
    • this.orgianlCourse = { …this.newCourse }” : Shallow Copy :) yapılarak var olan listenin bir clone’u alınır. Amaç, üzerinde güncelleme işlem yapılan data’nın, istendiğinde değişimden önceki haline dönülmesini sağlamaktır.
  • public reverseBack() { if (this.orgianlCourse != null) { this.newCourse = { …this.orgianlCourse }; } }” Geri al buttonuna tıklandığında, güncellenen datanın child component grid’de tıklandığı andaki çekilen  ilk haline, “newCourse” modeli döndürür. Böylece yapılan işlemler, geri alınır.

Bu makalede Angular 9 üzerinde, bir form girişi yapılırken listeleme component’ini, moleküler yapıda ayrı bir sayfa olarak tasarladık. Böylece aynı listeleme component’ine ihtiyac duyan diyer sayfalar için, kod tekrarından kaçındık. Refactornig durumunda, örneğin grid’e default bir tarih kolonu eklendiğinde tek bir yerden kod değiştirip zamandan ve yönetilebilirlikten kazandık. Bunun için tüm sayfalarda ortak kullanılan ihtiyaçlar, ilgili listeleme component’ına parametre olarak verildi. Ayrıca Parent-Child componentlarda, data binding tutarlılığı ve değişimin farkedilmesi gibi durumlar ele alındı. Form girişlerinde Template Validation ile veri girişi kontrolü yapılırken aynı zamanda numeric alanlar için custom directive yazıldı.

Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.

Source Code: https://github.com/borakasmer/ParentChildOnAngular9

Source : 

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir