Angular 6’da Kullanıcı Giriş Ekranında Undo Redo İşlemler
Selamlar,
Bu makalede, Angular 6 ile yazılacak bir iş başvuru web uygulamasında, UNDO – REDO işlemler nasıl yapılır, hep beraber inceleyeceğiz. Bu sistemin sadece Angular için değil, aynı zamanda farklı teknolojiler için de kolaylıkla implemente edilebileceği akıllardan çıkarılmamalıdır.
Bilgisayarda en çok sevdiğim şey, genelde yapılan bir hatanın kolayca geri alınabilmesidir. Örneğin tıpta ya da İnşaat Müh., böyle bir şey söz konusu bile değildir. Bu makalede, her yapılan değişiklikten sonra ilgili model, bir dizide tutulacaktır. Daha sonra istenen durumda, ilgli dizi içerisinde ileri geri hareket edilerek, geçerli indexdeki model alınacaktır. Böyle bir açıklama ile, akıllara gelen ilk design pattern “Memento”‘dan başka bir şey değildir. Memento Design Pattern hakkında daha detaylı bilgiye buradan erişebilirsiniz.
Alttaki komut ile Angular 6 projesi oluşturulur.
1 |
ng new UndoRedo |
Bu örnekte bir iş bavurusu formu oluşturulacaktır.
Model/Person.ts: Başvuru yapacak kişinin view model’i aşağıdaki gibidir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
export class Person{ private _Name : string; public get Name() : string { return this._Name; } public set Name(v : string) { this._Name = v; } private _Surname : string; public get Surname() : string { return this._Surname; } public set Surname(v : string) { this._Surname = v; } private _BirthDate : Date; public get BirthDate() : Date { return this._BirthDate; } public set BirthDate(v : Date) { this._BirthDate = v; } private _IsMale : boolean; public get IsMale() : boolean { return this._IsMale; } public set IsMale(v : boolean) { this._IsMale = v; } private _Salary : number[]; public get Salary() : number[] { return this._Salary; } public set Salary(v : number[]) { this._Salary = v; } } |
src/app/app.component.ts:
- “model: Person” : Sayfada kullanılacak model tipi “Person”‘dır.
- “tr” : Sayfada PrimeNG kullanılmıştır. Takvimde [locale]=”tr” attribute’ü ile kullanılacak dil tanımlanmıştır.
- “isModelChange” : Input alanlardan ayrılındığında yani (blur)’da, datanın değişip değişmediği kontrol edilmişitr.
- “changedModel” : Değişen datanın, UserControl(navigate.component)’a gönderilerek, container dizisinde saklanacağı “person” model değişkenidir.
- “constructor(public service: cloneService)” : Sayfa yüklenirken kullanılacak servis, “dependency injection” ile constructor’da sayfaya eklenir.
- “public ngOnInit() { }”: İlgili method, Angular tarafında bir page load methodu gibidir. Sayfa ilk yüklenirken, örnek amaçlı dummy data oluşturulur ve “changedModel“, yani değişen data değişkenine atanır. Bu da “navigate.component” UserController’ında bir history listesine eklenir.
- “this.tr= { }” : PrimeNGkütüphanesi ile kullanılacak takvim yani “<p-calendar” controlünün türkçe dil desteği buradaki tanımlama ile yapılır.
- “checkModel()” : İlgili modelde değişiklik olmuş ise, bir sonraki adımda anlatılacak service ile değişen model’in deep copy’si alınır.
- isModelChange: View Model’de değişiklik olup olmadığına bakar.
- isCheck: Bazı html elementlerde model’in değişip değişmediğine bakılmasına gerek yoktur. Yazının devamında detaylıca anlatılacaktır.
- getCurrentData(): Undo, Redo ile gelinen geçerli index’deki Person data, ilgili model’e atanır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import { Component, OnInit } from '@angular/core'; import { Person } from '../model/person'; import { cloneService } from './cloneService'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { model: Person; tr: any; isModelChange: boolean = false; changedModel: Person; constructor(public service: cloneService) { } public ngOnInit() { this.model = new Person(); this.model.Name = "Bora"; this.model.Surname = "Kasmer"; this.model.BirthDate = new Date(1978, 6, 3); this.model.IsMale = true; this.model.Salary = [5000, 7000]; this.changedModel = this.service.cloneModel(this.model); //------------------------- this.tr = { firstDayOfWeek: 0, dayNames: ["Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"], dayNamesShort: ["Paz", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt"], dayNamesMin: ["Pz", "Pt", "Sl", "Çr", "Pr", "Cm", "Ct"], monthNames: ["Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Augstos", "Eylül", "Ekim", "Kasım", "Aralık"], monthNamesShort: ["Ock", "Şbt", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Ekm", "Ksm", "Arl"], today: 'Bugün', clear: 'Temizle' }; } public checkModel(isCheck: boolean = false) { if (this.isModelChange || isCheck) { this.changedModel = this.service.cloneModel(this.model); this.isModelChange = false; } } public getCurrentData(event) { this.model = event; } } |
“npm install –save lodash” Yandaki komut ile Lodash projeye yüklenir. Peki neden mi? Az sonra :)
cloneService.ts: Amac, bir “Person[]” dizinin deep copy’sini almaktır. Yani referance değerleri tamamen farklı, yeni bir eşleniğini oluşturmaktır. Bu işlemin yapılabileceği 3. party kütüphanelerden biri de Lodash’dir. Peki neden? Çünkü değişime uğrayan herbir model, kendine özgüdür. Kopyalanan ve ilgili diziye atılan herbir modelin, var olan sistemden bağımsız olmaması durumunda, hali hazırda çalışılan model üzerindeki değişimlerin tümü, bu kopyalanan modelleri etkileyecekti. İlgili deep copy işlemi projenin farklı birçok yerinde yapıldığı için, kod kalabalığından kurtulma amaçlı servis haline getilirilmiştir. Ayrıca Generic tipte bir değişken beklediği için, kendisine gönderilen tüm data modelleri clonelayabilmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { Injectable } from '@angular/core'; import { Person } from '../model/person'; import * as _ from "lodash"; @Injectable() export class cloneService { constructor() { } public cloneModel<T>(oldData: T): T { return _.cloneDeep(oldData); } } |
app.component.html: Örnek amaçlı başvuru formunun girildiği ekrandır. Css olarak bootstrap kullanılmıştır. Kullanılan Html elementler PrimeNG’ye aittir.
- “<navigate-console [modelPerson]=”changedModel” (getdata)=”getCurrentData($event)”></navigate-console>” : Tüm sayfalarda kullanılacak, Undo-Redo işlemlerinin yapıldığı UserControl’dür. İlerde detaylıca incelenecektir.
- “<input id=”float-input” type=”text” size=”30″ pInputText [(ngModel)]=”model.Name” (ngModelChange)=”isModelChange=true” (blur)=”checkModel()”>”: İsmin yazıldığı input alandır.
- [(ngModel)]=”model.Name” : “User.Name” alanına 2 yönlü şekilde bağlanılmıştır.
- (ngModelChange)=”isModelChange=true” : İlgili Name property’si değiştiği zaman, bu değişken’e true değeri atılır. Bu şekilde değişen data Undo-Redo’da, değişen data listesine atanır.
- (blur)=”checkModel()” : Input alanlarda, her tuşa basıldığında değişen data history listesine atılmaması amacı ile, input alandan sadece ayrılınca(blur), değişen ilgili model, “checkModel()” methoduna gönderilip servis yardımı ile clone’u alınır.
- “<input type=”checkbox” [(ngModel)]=”model.IsMale” (ngModelChange)=”checkModel(true)”/>” : Cinsiyet checkBox’ı [(ngModel)] ile “IsMale” propertysine bağlanmıştır. (ngModelChange) eventinde, kısaca checkbox tıklandığı zaman, model değişir ve ilgili event tetiklenir. “checkModel(true)” methodu çağrılırken “IsCheck=true” atanarak, modelin değişip değişmediğine bakılmadan doğrudan servis yardımı ile clone’u alınır.
- “<p-calendar [(ngModel)]=”model.BirthDate” (ngModelChange)=”checkModel(true)” [inline]=”true” showOtherMonths=”true” yearNavigator=”true” yearRange=”1960:2020″ monthNavigator=”true” showTime=”true” hourFormat=”24″ showButtonBar=”true” [locale]=”tr”></p-calendar>”: Bu component, PrimeNG açık kaynak kodlu Angular kütüphanesine aittir.
- Not: PrimeNG kurmak için yandaki komutların çalıştırılması gerekmektedir. “npm install primeng –save“, “npm install primeicons –save“. PrimeNG Angular, React gibi birçok javascript kütüphanesine component yazan bir türk firmasıdır. Hakkında daha detaylı bilgiye buradan erişebilirsiniz.
- “[(ngModel)]=”model.BirthDate”” :BirthDate alanına 2 yönlü bind işlemi yapılır.
- “(ngModelChange)=”checkModel(true)””: Yeni bir tarih seçildiğinde geçerli model değiştirilmiş olunur. Bu durumda model değişmiş mi diye bakılmadan “checkModel()” methodu ile “deep copy”‘si alınır. Diğer propertyler’i PrimeNG sayfasından incelenebilir.
- “<p-slider [(ngModel)]=”model.Salary” [range]=”true” [step]=”1000″ [min]=”1000″ [max]=”20000″ (onSlideEnd)=”checkModel(true)”></p-slider>” : Bu da gene PrimeNG’ye ait bir controllerdır. Amaç belli bir aralıkta maaş değeri seçmektir.
- “[(ngModel)]=”model.Salary”” : Salary property’sine bağlanmıştır.
- “(onSlideEnd)=”checkModel(true)”” : Herbir slide yani yana kaydırma hareketinden ziyade, kaydırma işlemi bittikten sonra “checkModel()” methodu çağrılıp, değişen model’in service yardımı ile clone()’lanmaktadır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
<div class="container"> <div class="jumbotron"> <h2>İş Başvuru Kayıt Ekranı</h2> </div> <navigate-console [modelPerson]="changedModel" (getdata)="getCurrentData($event)"></navigate-console> <!-- <img src="/assets/Images/undo.png"> <img src="/assets/Images/redo.png"> --> <table class="table" *ngIf="model"> <tbody> <tr> <td> <label for="float-input">Username</label> </td> <td> <span class="ui-float-label"> <input id="float-input" type="text" size="30" pInputText [(ngModel)]="model.Name" (ngModelChange)="isModelChange=true" (blur)="checkModel()"> </span> </td> </tr> <tr> <td> <label for="float-input">Soyadınız:</label> </td> <td> <input id="float-input" type="text" size="30" pInputText [(ngModel)]="model.Surname" (ngModelChange)="isModelChange=true" (blur)="checkModel()"> </td> </tr> <tr> <td> <label for="float-input">Erkek mi?:</label> </td> <td> <input type="checkbox" [(ngModel)]="model.IsMale" (ngModelChange)="checkModel(true)"/> </td> </tr> <tr> <td> <label for="float-input">Doğum Tarihi:</label> </td> <td> <p-calendar [(ngModel)]="model.BirthDate" (ngModelChange)="checkModel(true)" [inline]="true" showOtherMonths="true" yearNavigator="true" yearRange="1960:2020" monthNavigator="true" showTime="true" hourFormat="24" showButtonBar="true" [locale]="tr"></p-calendar> </td> </tr> <tr> <td> <label for="float-input">Maaş Aralığ:</label> </td> <td> <h3>[{{this.model.Salary[0] + '-' + this.model.Salary[1]}}]</h3> <p-slider [(ngModel)]="model.Salary" [range]="true" [step]="1000" [min]="1000" [max]="20000" (onSlideEnd)="checkModel(true)"></p-slider> </td> </tr> </tbody> </table> </div> |
Şimdi sıra geldi esas işin yani, Undo-Redo işlemlerinin yapıldığı navigate.component.ts‘e.
navigate.component.ts: İstenen herhangi bir sayfaya rahatlıkla implemente edilebilmesi amacı ile, UserControl olarak oluşturulmuştur. Memento Design Patterninin kullanıldığı yapıda, ileri geri button durumları, geçerli Index numarası ve değişen model’in tüm listesi bu component üzerinde bulunmaktadır. Böylece hangi sayfa üzerine konursa konsun, ileri geri button aktifliği, bir önceki veya bir sonraki kayda erişim, ilgili sayfa üzerindeki model geçmişi gibi yapıların tamamı bulunduğu sayfadan bağımsız bir controller üzerinde bulunacak ve böylece kod kalabalığından da kaçınılacaktır.
<navigate-console [modelPerson]=”changedModel” (getdata)=”getCurrentData($event)”></navigate-console>
- Constructor’da : KeyValueDiffers ve cloneService providerları dependency injection ile sınıfa dahil edilmişlerdir.
- KeyValueDiffers: Service’e @input olarak gelen complex tiplerin, mesela Person modelinin herhangi bir property’sinin değişip değişmediğinin anlaşılması için kullanılır. Klasik @input, property olarak tanımlandığında, basit string veya number gibi tiplerin değişimi algılanabilir.
- cloneService: İlgili user controller’a gelen modelin deep copy’sinin alınması için kullanılmıştır.
- “container: Person[] = []” : Değişen model history’nin saklanacağı dizidir.
- “currentIndex: number = 0” : Dizi içerisinde bulunulan Index.
- Not: En büyük indexli eleman, dizideki ilk elemandır. Yukarıda görüldüğü gibi, son elemanın Index’i 0’dır.
- “@Input() modelPerson: Person”: User Control’a değişken olarak bulunduğu sayfadan aktarılan değişen person model’i.
- “ngDoCheck()” : Gönderilen model’in değişip değişmediğine bakılır ve değişmiş ise person history dizisine(container)’a atılır.
- “@Output() getdata” : Undo veya Redo işleminden sonra, gelinen o andaki indexdeki model ile birlikte tetiklenen event’dir. Böylece User Control’ün bulunduğu sayfadaki model, bu seçilen model ile değiştirilir.
- “undoClick()” : Bir önceki yapılan işleme dönülür.
- “var getIndex = this.container.length – 1 – this.currentIndex” : currentIndex bir arttırılarak, tersten Person[]container’da, bir gerideki değişen eski modelin indexi alınır.
- “var data = this.service.cloneModel(this.container[getIndex])” : User Control’ün bulunduğu sayfaya gönderilecek bir önceki modelin, deep copy’si alınır.
- “this.getdata.emit(data)” : getdata event’i, clone alınan data ile tetiklenir. Bu da app.component sayfasındaki “getCurrentData()” methodunu, ilgili data ile çağırmakta ve bir önceki kayıt, var olan person model ile değiştirilmektedir. Bu şekilde ilgili modele 2 way binding ile bağlı olan PrimeNG controllerlar, artık bir önceki yapılan değişikliği göstermektedirler.
- “redoClick()” : Bir sonraki yapılan işleme gidilir.
-
“this.currentIndex–” : Index 1 azaltılır.
-
“var getIndex = this.container.length – 1 – this.currentIndex” : Tersten Person[]container’da, bir sonraki kaydın indexi alınır.
-
“var data = this.service.cloneModel(this.container[getIndex])”: User Control’ün bulunduğu sayfaya gönderilecek bir sonraki modelin, deep copy’si alınır.
- “this.getdata.emit(data)”: getdata event’i, clone alınan data ile tetiklenir. Bu da app.component sayfasındaki “getCurrentData()” methodunu, ilgili data ile çağırmakta ve bir sonraki kayıt, var olan person model ile değiştirilmektedir. Bu şekilde ilgili modele 2 way binding ile bağlı olan PrimeNG controllerlar, artık bir sonraki yapılan değişikliği göstermektedirler.
-
- “pushContainer()” : Amaç, değişime uğruyan modeli history amaçlı Person[] container’a atmaktır.
- “if (this.currentIndex > 0 && this.currentIndex < this.container.length – 1) {” : Undo -Redo adımlarında gezinirken, klasik sistemlerden bağımsız olarak, araya yeni bir değişen Person model’i Insert edilebilmektedir. Yani 1,2,3,4 indexli bir dizide 2 ile 3. index arasında iken, yeni bir Person model konulup, 3=>4 ve 4=>5 olarak ötelenir.
-
“this.container.push(per)”: Son indexde duruluyor ise, değişen person kaydı listeye doğrudan atılır.
- “public canUndo(): boolean {” : Undo(Geri) butonunun aktifliğini belirlenir.
- “return (this.container.length < 2 || this.currentIndex >= this.container.length – 1)”: Eğer 1’den fazla kayıt yok ise, ya da son index yani ilk kayıtta bulunuluyor ise, Geriye Dönüş buttonu(Undo) pasif hale getirilir.
-
“public canRedo(): boolean {” : Redo(İleri) butonunun aktifliğini belirlenir.
-
“return this.currentIndex<=0” : Eğer son kayıtta ise, İleri buttonu(Redo) pasif hale getirilir.
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
import { Component, OnInit, Input, Output, EventEmitter, KeyValueDiffers } from '@angular/core'; import { Person } from '../model/person'; import { cloneService } from './cloneService'; @Component({ selector: 'navigate-console', templateUrl: 'navigate.component.html' }) export class NavigateComponent implements OnInit { constructor(private differs: KeyValueDiffers, public service: cloneService) { this.differ = differs.find({}).create(); } container: Person[] = []; currentIndex: number = 0; @Input() modelPerson: Person; differ: any; ngDoCheck() { var changes = this.differ.diff(this.modelPerson); if (changes) { //console.log("Input Data:"+JSON.stringify(this.modelPerson)); this.pushContainer(this.modelPerson); } } @Output() getdata: EventEmitter<Person> = new EventEmitter<Person>(); ngOnInit() { } public undoClick() { this.currentIndex++; var getIndex = this.container.length - 1 - this.currentIndex; if (getIndex >= 0) { var data = this.service.cloneModel(this.container[getIndex]); this.getdata.emit(data); } } public redoClick() { this.currentIndex--; var getIndex = this.container.length - 1 - this.currentIndex; if (getIndex >= 0) { var data = this.service.cloneModel(this.container[getIndex]); this.getdata.emit(data); } } public pushContainer(per: Person) { var data = new Person(); data=this.service.cloneModel(per); if (this.currentIndex > 0 && this.currentIndex < this.container.length - 1) { var getIndex = this.container.length - 1 - this.currentIndex; this.container.splice(getIndex, 0, data); } else { this.container.push(data); } //console.log("Container:" + JSON.stringify(this.container)); } public canUndo(): boolean { return (this.container.length < 2 || this.currentIndex >= this.container.length - 1); } public canRedo(): boolean { return this.currentIndex<=0; } } |
navigate.component.html: Tüm sayfalarda kullanılabilecek, datanın bir önceki veya bir sonraki halinin alınabileceği UserControl.
- Undo (Geri) buttonu “canUndo()” methodu ile aktiflik propery’si tanımlanmıştır.
- Undo (click) eventinde bir önceki kayda gidilecek, yukarıda anlatılan “undoClick()” methodu çalıştırılır.
- Redo (ileri) buttonu “canRedo()” methodu ile aktiflik propery’si tanımlanmıştır.
- Redo (click) eventinde bir sonraki kayda gidilecek, yukarıda anlatılan “redoClick()” methodu çalıştırılır.
1 2 3 4 5 6 7 |
<button pButton type="button" [disabled]="canUndo()" label="UNDO" (click)="undoClick()" class="ui-button-rounded ui-button-success"> <span class="glyphicon glyphicon-backward"></span> </button> <button pButton type="button" [disabled]="canRedo()" label="REDO" (click)="redoClick()" class="ui-button-rounded ui-button-warning"> <span class="glyphicon glyphicon-forward"></span> </button> |
Geldik bir makalenin daha sonuna. Bu makalede memento design pattern kullanılarak, işlem yapılan data üzerinde geri ve ileri hareket edilmiş ve tarihçesi tutulmuştur. Bu işlem Angular 6 üzerinde yapılsa da, farklı bir yapı üzerinde de kolaylıkla uygulanabilirdi. İlgili Undo-Redo buttonları, User Control haline getirilip tüm bussines tek bir yerde tutulduğu için, kullanıldığı sayfalara minimum kod yükü getirmiştir. Ayrıca değişen model’in clone’u, ayrı bir serviste generic tipte bir parametre kullanılarak alındığı için, tüm model ve sayfalara hızlıca implemente edilebilmiştir. Bu sistemde, modelde oluşan her değişik durumunda, üzerinde çalışılan modelin tamamı bir container list’e [] atılmıştır. Siz bu projeyi performans anlamında bir adım daha öteye götürerek, ilgili modelde sadece değişen property’i bir container list []’e tutabilirsiniz. Böylece ilgili container listesinin, client’ın rem’inde daha az yer kaplamasını ve daha hızlı çalışmasını sağlamış olursunuz. Tabi bu durumda, undo redo işlemlerinde değişen propertylerin, var olan modele implemente edilmesi gerekmektedir.
Yeni bir makalede görüşmek üzere hoşçakalın.
Source Code : https://github.com/borakasmer/UndoRedoOperationsonAngular6
Source :
Teşekkürler hocam güzel bir yazı
İşte bu !
Teşekkürler
Teşekkürler :)
Merhaba bora bey, yazı serileriniz için çok teşekkür ederim. Anlattığınız konudan bağımsız bir sorum olacak. 5-6 user type barındıran sistemim için angular tarafında sayfalarda gösterilmesi gereken alanlar, toollar farklı olabiliyor. Bunu basit bir şekilde sayfalara anguların if directivleriyle çözüyorum,fakat günün sonunda sadece bir sayfa için bile onlarca if directivi ile karşılaşabiliuorum. Bu probleme daha iyi bir yaklaşımınız var mıdır?
Şimdiden teşekkür ederim.
Merhaba,
Teşekkürler video için. Veritabanından gelen datayı dinamik olarak forum oluşturan bir video yapabilir misiniz ? Örneğin; veritabanında Adı(textbox) , cinsiyeti (checkbox), resmi (photo) , uyrugu (combobox) şeklinde veri olduğunu düşünelim. Html bu dataya göre oluşacak ve gerekli insert update işlemlerini yapacak. Bu konuda kısa da olsa video yapabilirseniz çok sevinirim. Şimdiden teşekkürler.
Bora Bey Visual Studio ASP.NET mvc projelerimizde angularjs(yani ilk versiyonu) mi kullanmalıyız. Ya da diğer angular versiyonlarını kolayca kullanabilmemiz mümkün mü örneğin angular 6 yı.
Selamlar Kemal,
Teknolojide bir klasik vardır. Her zaman son versiyon, en iyisidir. Kısaca Angular 6’dan başlayabilirsiniz.
İyi çalışmalar.
Bora Bey Angular6 ‘yı Visual Studio Ortamında(örneğin açtığımız bir mvc projesinde) kullanabilir miyiz?Kullanabiliyorsak nasıl entegre edebiliriz. Siz hep visual studio code üzerinde core projelerinizde kullanmışsınız.