Angular - Formlardaki Kaydedilmemiş Değişiklikleri Algıla

Form doldurduğunuz bir sayfadan yanlışlıkla çıktığınızda doldurduğunuz tüm değerleri kaybedersiniz. Hele ki doldurulması gereken alan çok fazlaysa baya can sıkıcı bir durum olur. Böyle durumların önüne geçebilmek için Angular'ın canDeactivate özelliğinden yararlanabiliriz. Eğer form'da herhangi bir değişiklik varsa ve sayfa değiştirilmek isteniyorsa kullanıcıdan onay alarak bu işlemi yaptırabiliriz.


Basit bir örnek yapalım;

Filmlerin listelendiği ve film ekleme işlemi yapabildiğimiz bir projemiz olsun. Eklemeyi yaptığımız sayfada inputlardan herhangi birine bir şey yazılmışsa ve menüden başka sayfaya tıklanmışsa kullanıcıya confirm() ile uyarı verip, onaylarsa sayfa değişikliği gerçekleşsin, onaylamazsa doldurduğu hiçbir değerini kaybetmeden aynı sayfada kalsın.


1-) Projemizi oluşturalım;

ng new detect-unsaved-changes-in-forms


2-) Componentlerimizi oluşturalım ve route düzenlememizi yapalım;

ng g c components/home
ng g c components/movies
ng g c components/add-movie
ng g c components/categories
ng g c components/add-category


app-routing.module.ts;

const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
    pathMatch: 'full'
  },
  {
    path: 'movies',
    component: MoviesComponent
  },
  {
    path: 'add-movie',
    component: AddMovieComponent
  },
  {
    path: 'categories',
    component: CategoriesComponent
  },
  {
    path: 'add-category',
    component: AddCategoryComponent
  }
]


3-) add-movie componentimiz için formu oluşturalım.

add-movie.component.ts;

import { FormGroup, FormBuilder, Validators } from '@angular/forms';

movieForm: FormGroup;

constructor(
    private fb: FormBuilder
) { }

ngOnInit() {
    this.movieForm = this.fb.group({
      name: ['', Validators.required],
      year: ['', Validators.required],
      image: ['', Validators.required]
    });
    this.movieForm.valueChanges.subscribe( e => this.isDirty = true );
}

addMovie() {
    console.log(this.movieForm.value);
}


add-movie.component.html;

<form [formGroup]="movieForm">
  <div class="form-group">
    <input type="text" formControlName="name">
    <label>Movie Name</label>
  </div>
  <div class="form-group">
    <input type="text" formControlName="year">
    <label>Year</label>
  </div>
  <div class="form-group">
    <input type="text" formControlName="image">
    <label>Image Link</label>
  </div>
  <button type="button" (click)="addMovie()">Add Movie</button>
</form>


4-) Şimdi DirtyComponent interface oluşturacağız. Bu interface'i add-movie componentimizde implement edeceğiz.

ng g models/dirty-component


dirty-component.ts;

import {Observable} from 'rxjs';


export declare interface DirtyComponent {
  canDeactivate: () => boolean | Observable<boolean>;
}


5-) AddMovieComponent içerisinde oluşturduğumuz interface'i kullanalım. Nasıl yapıyoruz bunu;


add-movie.component.ts i açıyoruz. export class AddMovieComponent implements OnInit kısmına bir virgül koyup DirtyComponent yazıyoruz (yukarıda import etmeyi de unutmuyoruz). Daha sonra bu interface'de şart koştuğumuz canDeactive methodunu ekliyoruz.


Tabi bu method içerisinden true ya da false değer döndürmemiz gerekiyor. Bunun içinde bir değişken tanımlayacağız isDirty olsun adı. İlk değerine de false vereceğiz. Ve canDeactivate methodu içerisinde bu değişkeni döndüreceğiz, yani return this.isDirty;


Daha sonra movieForm içerisinde bir değişiklik olursa isDirty değerini true yapacağız. Bu değişikliği de ReactiveForm'ların valueChanges özelliği ile. Yani şu şekilde değişikliği yakalayıp isDirty değerini true olarak değiştireceğiz;

this.movieForm.valueChanges.subscribe( e => this.isDirty = true );


Bunları yaptığımızda add-movie.component.ts dosyamız şu şekilde olacak;

import { DirtyComponent } from '../../models/dirty-component';

export class AddMovieComponent implements OnInit, DirtyComponent {


  movieForm: FormGroup;
  isDirty = false;


  constructor(
    private fb: FormBuilder
  ) { }


  ngOnInit() {
    this.movieForm = this.fb.group({
      name: ['', Validators.required],
      year: ['', Validators.required],
      image: ['', Validators.required]
    });
    this.movieForm.valueChanges.subscribe( e => this.isDirty = true );
  }


  canDeactivate() {
    return this.isDirty;
  }


  addMovie() {
    console.log(this.movieForm.value);
  }


}


6-) Eğer formda değişiklik var ve sayfadan çıkmak istiyorsak, kullanıcıdan onay aldıracağımız DirtyCheck guard'ı oluşturalım.

ng g guards/dirty-check

komutu çalıştırdıktan sonra gelen seçeneklerden canDeactivate'i seçelim, eğer bu seçenek yoksa canActivate'i seçebilirsiniz. İçeriğini değiştirip son haline kavuştururuz.


Bu guard içerisinde canDeactivate() methodunda DirtyComponent'in değerine göre confirm() (onay alma) çalıştıracağız.

Yani şöyle methodumuz şu şekilde olacak;

canDeactivate(
    component: DirtyComponent,
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    if (component.canDeactivate()) {
      return confirm('There are changes you have made to the page. If you quit, you will lose your changes.');
    } else {
      return true;
    }
}


Tüm dosya ise şu şekilde olacak: dirty-check.guard.ts;

import { Injectable } from '@angular/core';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { DirtyComponent } from '../models/dirty-component';


@Injectable({
  providedIn: 'root'
})
export class DirtyCheckGuard implements CanDeactivate<DirtyComponent> {


  canDeactivate(
    component: DirtyComponent,
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    if (component.canDeactivate()) {
      return confirm('There are changes you have made to the page. If you quit, you will lose your changes.');
    } else {
      return true;
    }
  }


}


Buradaki olay şu;

Guard içerisindeki canDeactivate methoduna gelen DirtyComponent'in değeri true ise confirm ile bir uyarı verecek ve kullanıcı Tamam derse bu sayfaya gidecek, Hayır derse sayfadan çıkmamış olacak. Eğer false ise direkt o sayfaya gidebilecek.


Tabi şuan yukarıdaki olayların hiçbirini çalıştırmadık. Çalışabilmesi için son bir adımımız var. Routing içerisinde 'add-movie' rotasına bunu tanımlamak.


7-) AppRoutingModule'de tanımlamamızı yapalım;


Yukarıda yazdığımız işlemler hangi sayfalarımızda geçerli olsun istiyorsak routes içinde o elamana canDeactivate: [] tanımlamasını yapmamız gerekiyor. Yani şu şekilde;

{
    path: 'add-movie',
    component: AddMovieComponent
}

olan tanımlamayı şununla değiştireceğiz;

{
    path: 'add-movie',
    component: AddMovieComponent,
    canDeactivate: [DirtyCheckGuard]
}


Hepsi bu kadar. Basit bir şekilde anlatmaya çalıştım.

Yaptığımız örneğin canlı canlı test edebileceğiniz linkini de buraya bırakayım: https://stackblitz.com/edit/detect-unsaved-changes-in-forms


Umarım faydalı olur.

Mehmet Sert

HTML, CSS, Javascript ve Angular konularında tecrübe edinmiş ve Bursa'da Frontend Developer olarak bir firmada görev almaktayım. Yeni teknolojilere meraklıyım, öğrendiklerimi uygulayarak ve anlatarak pekiştirmeyi seviyorum....

"Angular - Formlardaki Kaydedilmemiş Değişiklikleri Algıla" için 6 yorum yapıldı.
İ.Ç
İBRAHİM ÇOBAN 18 Nisan 2020

Merhabalar Yukarıdaki konu ile ilgisi değil fakat Angular ile bir bilgiye ihtiyacım var. Angular 9 ile küçük bir proje yapıyorum. Projede iki sayfa (master->detail) var. İlk sayfada veri tabanında arama sonucunda gelen isim listesi var ve buna tıklandığında yeni sayfada adres telefon gibi bilgiler geliyor. İkinci sayfadaki bilgilerden sonra geri dönüldüğünde sayfa tekrar yükleniyor ve aramayı tekrar yapmam gerekiyor. Şimdilik arama değerini ve listesini global bir değişkenlere atadım ve bu değişkenlerde veri varsa bunları gösteriyorum. Yani Ionic framework deki gibi sayfaları push pop yapabiliyor muyuz? Paylaşımlarınız devamı dileği ile... Şimdiden çok teşekkürler.

Mehmet Sert (Yazı sahibi)19 Nisan 2020

Merhaba İbrahim Çoban, Aslında olması gereken çözümü yapmışsın yani servis içindeki bir değişkene apiden gelen listeyi atıp, ilk önce o değişkeni kontrol edip ona göre apiden çağırmak veya değişkenden kullanmak doğru bir çözüm. Aynı şekilde sessionStorage i'de kullanabilirsin. Ionic push pop'u bilmiyorum o yüzden onunla ilgili bir şey diyemem ama senin kullandığın yöntem yanlış bir yöntem değil. Ancak senin için küçük bir araştırma yaptım ve bu konu ile ilgili sanırım cache yöntemi var. Http isteği yaptığımız yerde shareReplay ile buna bir çözüm getirmişler gibi duruyor. Denemediğim için net ifadeler kullanamıyorum ama deneyip yazacağım. Benimde işimi görecektir bu yöntem. Anahtar kelimemiz shareReplay. Umarım seninde işini görür.

Ü.A
Ümid Aydemir 20 Nisan 2020

Merhabalar , sayfa yenilemesi için de window:beforeunload kullanıyorum çıkan popup'ı customize edebiliyor muyuz?

Mehmet Sert (Yazı sahibi)21 Nisan 2020

Merhaba Ümid Aydemir, Ben confirm() yerine Material Dialog denemesi yaptım ancak çalışmadı bende çok üstüne durmadım o yüzden klasik confirm ile bıraktım.

İ.Ç
İBRAHİM ÇOBAN 21 Nisan 2020

Merhaba Mehmet Bey Cevap için teşekkürler. Belki eksik oldu, Ionic deki router için. Ionic belki mobile first olduğu için sayfaları stack de tutuyor. NavController.push(page) ->stack sayfa ekler ve gösterir. NavController.pop() <-stackde sayfa kaldırır ve geri döner. Döndüğünde de sayfa yani component tekrar create edilmez. Bunu angular da nasıl yapabiliriz.

Mehmet Sert (Yazı sahibi)23 Nisan 2020

Tekrardan merhaba İbrahim Çoban, Detay sayfasına gidip geri geldiğinde componentin tekrar oluşmaması nasıl olur bilemedim. Sayfa değiştiği için html de tekrar yüklenecektir bir yolu varsa şuan için bilmiyorum bu yüzden yardımcı olamayacağım kusura bakmayın ama öğrenirsem anlatırım mutlaka, sizde öğrenirseniz burada veya başka bir yerde bunun anlatımını yaparsanız çok memnun olurum. Teşekkürler.

Yorum yap * E-posta adresiniz yayınlanmayacak.