Implementasi Reactive Form – 6

Pada modul ini kita akan mengaktifkan beberapa fitur:

  • Delete recipe pada form Recipe List.
  • Pada form Edit Recipe, menambahkan navigasi pada button cancel, save dan delete ingredient.
  • Menampilkan image preview pada Edit Recipe.

Fungsi Delete Recipe

Buka file recipe.service.ts, kita tambahkan method untuk handling delete.

import { Injectable } from "@angular/core";
//tambahkan library subject jika IDE tidak otomatis menambahkannya
import { Subject } from "rxjs";
import { Ingredient } from "../shared/ingredient.model";
import { ShoppingListService } from "../shopping-list/shopping-list.service";
import { Recipe } from "./recipe.model";

@Injectable()
export class RecipeService{
    //tambahkan subject untuk perubahan recipe
    recipeChange = new Subject<Recipe[]>();

    private recipes: Recipe[] = [
        new Recipe(
            'Oreo Dorayaki 1', 
            'Doriyaki dengan bahan utama Oreo', 
            'https://i.ytimg.com/vi/nJGmdqb892Y/maxresdefault.jpg',
            [
                new Ingredient('Oreo', 1),
                new Ingredient('Susu', 1),
            ]),
        new Recipe(
            'Oreo Dorayaki 2', 
            'Doriyaki dengan bahan utama Oreo 2', 
            'https://i.ytimg.com/vi/nJGmdqb892Y/maxresdefault.jpg',
            [
                new Ingredient('Oreo', 1),
                new Ingredient('Gula', 1),
            ])
      ];
    
    constructor (private shoppingListServ: ShoppingListService){}

    getRecipes(){
        return this.recipes.slice();
    }

    getRecipe(idx : number){
        return this.recipes[idx];
    }

    addIngToShopList(ingredients : Ingredient[]){
        this.shoppingListServ.addIngredients(ingredients);
    }

    addRecipe(recipe: Recipe){
        this.recipes.push(recipe);
        this.recipeChange.next(this.recipes.slice());
    }

    updRecipe(idx: number, updRecipe: Recipe){
        this.recipes[idx] = updRecipe;
        this.recipeChange.next(this.recipes.slice());
    }

    // tambahkan method delete
    delRecipe(idx: number){
        this.recipes.splice(idx, 1);
        this.recipeChange.next(this.recipes.slice());
    }
}

Kemudian buka file recipe-detail.component.html, pada delete button tambahkan code berikut

<div class="row">
    <div class="col-xs-12">
        <img [src]="recipe.imgPath" alt="{{recipe.name}}" class="img-fluid" style="max-height:240px;">
    </div>
</div>
<div class="row">
    <div class="col-xs-12">
        <h1>{{recipe.name}}</h1>
    </div>
</div>
<div class="row">
    <div class="col-xs-12">
        <div class="dropdown" appDropDown>
            <a class="btn btn-primary dropdown-toggle" role="button" data-toggle="dropdown">Manage Recipe</a>
            <div class="dropdown-menu">
              <a class="dropdown-item" (click)="onAddShoppingList()" style="cursor:pointer;">Add Shopping List</a>
              <a class="dropdown-item" style="cursor:pointer;" (click)="onEditRecipe()">Edit Recipe</a>
              <a class="dropdown-item" style="cursor:pointer;" (click)="onDeleteRecipe()">Delete Recipe</a>
            </div>
          </div>        
    </div>
</div>
<div class="row">
    <div class="col-xs-12">{{recipe.description}}</div>
</div>
<div class="row">
    <div class="col-xs-12">
        <ul class="list-group">
            <li class="list-group-item" *ngFor="let ingredient of recipe.ingredients">
                {{ingredient.name}} - {{ingredient.amount}}
            </li>
        </ul>
    </div>
</div>

Kemudian buka file recipe-detail.component.ts, buat method untuk handling event click diatas.

Method akan menggunakan method yang ditambahkan pada file recipe service diatas, kemudian melakukan navigasi ke path /recipe.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Recipe } from '../recipe.model';
import { RecipeService } from '../recipe.service';

@Component({
  selector: 'app-recipe-detail',
  templateUrl: './recipe-detail.component.html',
  styleUrls: ['./recipe-detail.component.css']
})
export class RecipeDetailComponent implements OnInit {
  recipe! : Recipe;
  id! : number;

  constructor(private recipeServ : RecipeService, 
    private route: ActivatedRoute,
    private router: Router
    ) { }

  
  ngOnInit(): void {
    this.route.params.subscribe(
      (params: Params) =>{ 
        this.id = +params['id'];
        this.recipe = this.recipeServ.getRecipe(this.id);
      }
    );
  }

  onAddShoppingList(){
    this.recipeServ.addIngToShopList(this.recipe.ingredients);
  }

  onEditRecipe(){
    this.router.navigate(['edit'], {relativeTo: this.route});
    //this.router.navigate(['../', this.id, 'edit'], {relativeTo: this.route}  );
  }

  //method untuk handling click delete.
  onDeleteRecipe(){
    this.recipeServ.delRecipe(this.id);
    this.router.navigate(['/recipes']);
  }
}

Fungsi Navigasi pada Button Cancel, Save dan delete ingredient

Buka file recipe-edit.component.html, lalu tambahkan event listener untuk button cancel dan button delete ingredient (X).

<div class="row">
    <div class="col-xs-12">
        <form [formGroup]="recipeForm" (ngSubmit)="onSubmit()">
            <div class="row">
                <div class="col-12">
                    <!-- disabled button jika validasi gagal -->
                    <button type="submit" class="btn btn-success" [disabled]="recipeForm.invalid">Save</button>
                    <button type="submit" class="btn btn-danger" (click)="onCancel()">Cancel</button>
                </div>
            </div>
            <div class="row">
                <div class="col-12">
                    <div class="form-group">
                        <label for="name">Name</label>
                        <input type="text" class="form-control" id="name" formControlName="name">
                    </div>
                </div>
            </div>
            <div class="row">
                <div class="col-12">
                    <div class="form-group">
                        <label for="imgPath">Image URL</label>
                        <input type="text" class="form-control" id="imgPath" formControlName="imgPath">
                    </div>
                </div>
            </div>
            <div class="row">
                <div class="col-12">
                    <img src="" class="img-fluid.">
                </div>
            </div>            
            <div class="row">
                <div class="col-12">
                    <div class="form-group">
                        <label for="description">Description</label>
                        <textarea class="form-control" id="description" rows="5" formControlName="description"></textarea>
                    </div>
                </div>
            </div>
            <div class="row mt-2">
                <div class="col-12" formArrayName="ingredients">
                    <div class="row" 
                    *ngFor="let ingCtrl of ingredientsCtrl; let i = index"
                    [formGroupName]="i">
                        <div class="col-8">
                            <input type="text" class="form-control" formControlName="name">
                        </div>
                        <div class="col-2">
                            <input type="number" class="form-control" formControlName="amount">
                        </div>
                        <div class="col-2">
                            <button type="button" class="btn btn-danger" (click)="onIngredientDel(i)">X</button>
                        </div>                        
                    </div>
                    <hr>
                    <div class="row">
                        <div class="col-12">
                            <button type="button" class="btn btn-success" (click)="onAddIngredient()">Add</button>
                        </div>
                    </div>
                </div>
            </div>
        </form>
    </div>
</div>

Kemudian buka file recipe-edit.component.ts, tambahkan method untuk handling event click diatas, kemudian gunakan juga method tersebut pada button save.

Karena kita akan melakukan navigasi, untuk itu kita perlu menambahkan library Router. Kemudian inject router pada constructor method. Router akan kita gunakan pada method onCancel().

Ditambahkan juga method untuk handling ingredient delete.

import { ThrowStmt } from '@angular/compiler';
import { Component, OnInit } from '@angular/core';
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
//tambahkan library Router jika IDE tidak otomatis.
import { ActivatedRoute, Params, Router } from '@angular/router';
import { RecipeService } from '../recipe.service';

@Component({
  selector: 'app-recipe-edit',
  templateUrl: './recipe-edit.component.html',
  styleUrls: ['./recipe-edit.component.css']
})
export class RecipeEditComponent implements OnInit {
  id! : number;
  editMode = false;
  recipeForm! : FormGroup;

  //tambahkan router untuk melakukan navigasi saat button cancel ditekan.
  constructor(private route: ActivatedRoute, private recipeSrv : RecipeService, private router: Router) { }

  ngOnInit(): void {
    this.route.params.subscribe(
      (params : Params) =>{
        this.id = +params['id'];
        this.editMode = params['id'] != null;
        this.initForm();
      }
    );
  }

  private initForm(){
    let recipeNm = '';
    let recipeImg = '';
    let recipeDesc = '';
    let recipeIngredients = new FormArray([]);

    if (this.editMode){
      const recipe = this.recipeSrv.getRecipe(this.id);
      recipeNm = recipe.name;
      recipeImg = recipe.imgPath;
      recipeDesc = recipe.description;
      if (recipe['ingredients']){
        for (let ing of recipe.ingredients){
          recipeIngredients.push(
            new FormGroup({
              'name' : new FormControl(ing.name, Validators.required),
              'amount' : new FormControl(ing.amount, [Validators.required, Validators.pattern(/^[1-9]+[0-9]*$/)])
            })
          );
        }
      }
    }
    this.recipeForm = new FormGroup({
      'name': new FormControl(recipeNm, Validators.required),
      'imgPath': new FormControl(recipeImg, Validators.required),
      'description': new FormControl(recipeDesc, Validators.required),
      'ingredients': recipeIngredients
    });
  }

  
  onAddIngredient(){
    (<FormArray>this.recipeForm.get('ingredients')).push(
      new FormGroup({
        'name': new FormControl(),
        'amount' : new FormControl()
      })
    );
  }

  //method handling delete ingredient.
  onIngredientDel(idx: number){
    (<FormArray>this.recipeForm.get('ingredients')).removeAt(idx);
  }

  onSubmit(){
    if (this.editMode){
      this.recipeSrv.updRecipe(this.id, this.recipeForm.value);
    }else{
      this.recipeSrv.addRecipe(this.recipeForm.value);
    }
    this.onCancel();
  }

  // handling click button cancel
  onCancel() {
    this.router.navigate(['../'], {relativeTo: this.route});
  }

  get ingredientsCtrl(){
    return (<FormArray>this.recipeForm.get('ingredients')).controls;
  }

}

Menampilkan Image Preview

Buka file recipe-edit.component.html, tambahkan local reference pada input type imagePath, kemudian gunakan reference tersebut untuk menampilkan image preview. (lihat code dengan commentar).

Perhatian, jika Anda mengcopy code modul sebelumnya, ada typo pada class img-fluid. Pada modul sebelumnya tertulis “img-fluid.”, seharusnya tanpa titik.

<div class="row">
    <div class="col-xs-12">
        <form [formGroup]="recipeForm" (ngSubmit)="onSubmit()">
            <div class="row">
                <div class="col-12">
                    <button type="submit" class="btn btn-success" [disabled]="recipeForm.invalid">Save</button>
                    <button type="submit" class="btn btn-danger" (click)="onCancel()">Cancel</button>
                </div>
            </div>
            <div class="row">
                <div class="col-12">
                    <div class="form-group">
                        <label for="name">Name</label>
                        <input type="text" class="form-control" id="name" formControlName="name">
                    </div>
                </div>
            </div>
            <div class="row">
                <div class="col-12">
                    <div class="form-group">
                        <label for="imgPath">Image URL</label>
                        <!-- tambahkan local reference -->
                        <input type="text" class="form-control" id="imgPath" formControlName="imgPath" #imgPath>
                    </div>
                </div>
            </div>
            <div class="row">
                <div class="col-12">
                    <!-- gunakan local reference untuk mendapatkan image path -->
                    <img [src]="imgPath.value" class="img-fluid">
                </div>
            </div>            
            <div class="row">
                <div class="col-12">
                    <div class="form-group">
                        <label for="description">Description</label>
                        <textarea class="form-control" id="description" rows="5" formControlName="description"></textarea>
                    </div>
                </div>
            </div>
            <div class="row mt-2">
                <div class="col-12" formArrayName="ingredients">
                    <div class="row" 
                    *ngFor="let ingCtrl of ingredientsCtrl; let i = index"
                    [formGroupName]="i">
                        <div class="col-8">
                            <input type="text" class="form-control" formControlName="name">
                        </div>
                        <div class="col-2">
                            <input type="number" class="form-control" formControlName="amount">
                        </div>
                        <div class="col-2">
                            <button class="btn btn-danger">X</button>
                        </div>                        
                    </div>
                    <hr>
                    <div class="row">
                        <div class="col-12">
                            <button type="button" class="btn btn-success" (click)="onAddIngredient()">Add</button>
                        </div>
                    </div>
                </div>
            </div>
        </form>
    </div>
</div>

Jika Anda test fungsi button-button diatas, sudah berfungsi sesuai ekspektasi.

Sampai disini kita sudah berhasil mengimplementasikan reactive form, ada sedikit bug yang terjadi, yaitu recipe yang ditambahkan, bila kita melakukan navigasi, akan hilang.

Akan kita bahas pada modul berikutnya.

Sharing is caring:

Leave a Comment