How to implement ionic BehaviorSubject

The BehaviorSubject represents a value that changes over time, the real power of the BehaviorSubject, in this case, is that every subscriber will always get the initial or the last value that the subject emits. But the subject doesn’t return the current value on the Subscription. It triggers only on .next(value) call and return/output the value.

Ionic BehaviourSubject will return the initial value or the current value on the Subscription. An official document of rxjs defines BehaviorSubject as Requiring an initial value and emitting the current value to new subscribers.

Ionic behaviorsubject syntax

let subject = new Rx.Subject();
subject.next(1); //Subjects will not output this value

subject.subscribe({
  next: (v) => console.log('observer A: ' + v)
});
subject.next(1);  // Output new value 1 for 'observer A'
subject.next(2) // Output 2


let subject = new Rx.BehaviorSubject(0);  // 0 is the initial value
subject.subscribe({
  next: (v) => console.log('observerA: ' + v)  
// output initial value, then new values on `next` triggers
});

subject.next(1);  // Output new value 1 for 'observer A'
subject.next(2) // Output 2

Ionic BehaviorSubject example

BehaviorSubject in ionic or Ionic behaviorsubject
Ionic Behaviorsubject

Screenshot of our RxJS BehaviorSubject example to access local JSON data for CRUD operation in Ionic Angular.

Step 1: Create an ionic angular project to implement BehaviorSubject in Ionic

ionic start recipeApps blank --type=angular

Step 2: Create recipe model, in src/model/recipe.model.ts

export interface IRecipe {
    id?: string;
    title: string;
    imageUrl: string;
    ingredients: string;
}

Step 3: Generate recipe service
Let create and recipe service in a folder in src/services/

ionic generate service services/recipes

Add following code to perform of CRUD operation on _recipes behaviorSubject observable as

import { Injectable } from '@angular/core';
import { IRecipe } from 'src/models/recipe.model';
import { BehaviorSubject } from 'rxjs';
import { take, map, delay, tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class RecipesService {
  // tslint:disable-next-line: variable-name
  private _recipes = new BehaviorSubject<IRecipe[]>([
    {
      id: '1',
      title: 'Paneer Chilli',
      imageUrl: 'https://i.ytimg.com/vi/IF0AuPLRcko/maxresdefault.jpg',
      ingredients: 'Paneer, Onion, Masala'
    },
    {
      id: '2',
      title: 'Dal Makhani',
      imageUrl: 'https://www.indianhealthyrecipes.com/wp-content/uploads/2016/05/dal-makhani-recipe-3-500x375.jpg',
      ingredients: `2 tablespoon soaked overnight red kidney beans, 1 teaspoon red chilli powder, 4 tablespoon butter
      1 large chopped onion, 1/2 cup tomato puree, 1/2 cup fresh cream, 2 inch chopped ginger`
    },
  ]);

  get recipes() {
    return this._recipes.asObservable();
  }

  getRecipe(id: string) {
    return this.recipes.pipe(
      take(1),
      map(recipes => {
      return { ...recipes.find(recipe => recipe.id === id)};
    }));
  }

  addRecipe(data: IRecipe) {
    const copiedRecipes: IRecipe[] = this._recipes.getValue();
    data.id = (copiedRecipes.length + 1).toString();
    copiedRecipes.push(data);
    this._recipes.next(copiedRecipes);
  }

  deleteRecipe(id: string) {
    const data: IRecipe[] = this._recipes.getValue();
    this._recipes.next(data.filter((recipe: IRecipe) => recipe.id !== id ));
  }

  updateRecipe(data: IRecipe) {
    return this.recipes.pipe(
      take(1),
      delay(1000),
      tap(recipes => {
        const updateRecipeIndex = recipes.findIndex(recipe => recipe.id === data.id);
        const updatedRecipes = [...recipes];
        updatedRecipes[updateRecipeIndex] = data;
        this._recipes.next(updatedRecipes);
      })
    );
  }
}

Note:
1. this._recipes.asObservable()
The .asObservable() instance method inherited by the Subject class in RxJS to convert Subject to observable. The purpose of this is to prevent leaking the “observer side effects” of the instance of the Subject on other parts of the application.  To prevent this, it is best to convert Subjects to Observables so that the sequence is exposed in a read-only fashion.

2. getValue()
In the addRecipe and deleteRecipe method, we have used getValue() method, to get the current value of BehaviorSubject observable.

const copiedRecipes: IRecipe[] = this._recipes.getValue();
copiedRecipes.next(newArray); // throws an error

As you can see, when we tried to call .next() on the returned value, an error was thrown. This is because we successfully converted the Subject instance to an Observable instance,  shielding the calling context from the Subject implementation.

3. take(1) emits one and completes observable and unsubscribes it.

4. tap() It is used for side effects inside a stream. So this operator can perform some operation inside a stream and return the same observable as it was used on.

Step 3: Create an observer which subscribes to Observable
In recipe component will subscribe to our BehaviorSubject observable will perform an update and add operation. You can use the home page, as I have to remove the home page and create two more pages in src/pages with recipes and recipe-detail page. Add the following code in src/pages/recipes.page.ts

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { RecipesService } from 'src/services/recipes.service';
import { IRecipe } from 'src/models/recipe.model';
import { FormGroup, FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-recipes',
  templateUrl: './recipes.page.html',
  styleUrls: ['./recipes.page.scss'],
})
export class RecipesPage implements OnInit, OnDestroy {
  recipes: IRecipe[];
  form: FormGroup;
  addEditRecipe: boolean;
  editMode = false;
  private recipesSub: Subscription;

  constructor(
    private fb: FormBuilder,
    private recipeService: RecipesService) {
      this.form = this.fb.group({
        id: '',
        title: '',
        imageUrl: '',
        ingredients: ''
      });
  }

  ngOnInit() {
    this.recipesSub = this.recipeService.recipes.subscribe((recipes: IRecipe[]) => {
      this.recipes  = recipes;
    });
  }

  editRecipe(recipe: IRecipe) {
    this.editMode = true;
    this.form.patchValue(recipe);
  }

  update() {
    if (!this.editMode) { // Add new recipe
      this.recipeService.addRecipe(this.form.value);
      this.editMode = false;
      this.form.reset();
      this.addEditRecipe = false;
    } else {
      this.recipeService.updateRecipe(this.form.value)
      .subscribe(() => {
        this.editMode = false;
        this.form.reset();
        this.addEditRecipe = false;
      });
    }
  }

  ngOnDestroy() {
    if (this.recipesSub) {
      this.recipesSub.unsubscribe();
    }
  }

}

Add template for creating, edit and displaying recipes in src/pages/recipes.page.html

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Recipes</ion-title>
    <ion-buttons slot="primary" (click)="addEditRecipe=true">
      <ion-icon name="add"></ion-icon>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item *ngFor="let recipe of recipes">
      <ion-avatar slot="start">
        <ion-img [src]="recipe.imageUrl"></ion-img>
      </ion-avatar>
      <ion-label>{{ recipe.title }}</ion-label>
      <ion-button (click)="addEditRecipe=1; editRecipe(recipe, f)">Edit</ion-button>

      <ion-button [routerLink]="['/recipes', recipe.id]">View Detail</ion-button>
    </ion-item>
  </ion-list>

  <form [formGroup]="form" *ngIf="addEditRecipe" class="ion-padding">
    <ion-list >
      <ion-text *ngIf="!editMode" color="danger" class="ion-text-center"><h3>Add new Recipe</h3></ion-text>
      <ion-text *ngIf="editMode" color="danger" class="ion-text-center"><h3>Edit Recipe</h3></ion-text>
      <ion-item>
        <ion-label>Title*</ion-label>
        <ion-input formControlName="title" required></ion-input>
      </ion-item>
  
      <ion-item>
        <ion-label>Image Url*</ion-label>
        <ion-input formControlName="imageUrl" name="imageUrl" required></ion-input>
      </ion-item>
  
      <ion-item>
        <ion-label>Recipe</ion-label>
        <ion-textarea rows="3" cols="10" formControlName="ingredients" name="ingredients"></ion-textarea>
      </ion-item>
  
      <ion-button color="danger" size="small" 
      class="ion-float-right" (click)="update()" [disabled]="!form.valid">
        Submit
      </ion-button>
    </ion-list>
  </form>

</ion-content>

Step 4: Perform delete operation, we have a recipe-detail page that will allow us to perform delete operation and display detail of each recipe. Add the following code in src/pages/recipe-detail.page.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AlertController } from '@ionic/angular';
import { RecipesService } from 'src/services/recipes.service';
import { IRecipe } from 'src/models/recipe.model';


@Component({
  selector: 'app-recipe-detail',
  templateUrl: './recipe-detail.page.html',
  styleUrls: ['./recipe-detail.page.scss'],
})
export class RecipeDetailPage implements OnInit {
  recipe: IRecipe;

  constructor(
    private router: Router,
    private activeRoute: ActivatedRoute,
    private altCtrl: AlertController,
    private recipeService: RecipesService,
  ) { }

  ngOnInit() {
    this.activeRoute.paramMap.subscribe(paramMap => {
      if (!paramMap.has('id')) {
        return;
      }
      const id = paramMap.get('id');
      this.recipeService.getRecipe(id)
      .subscribe((recipe: IRecipe) => {
        this.recipe = recipe;
      });
    });
  }

  onDeleteRecipe() {
    this.altCtrl.create({
      header: 'Are you sure ?',
      message: 'Do you really want to delete recipe ?',
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel'
        },
        {
          text: 'Delete',
          handler: () => {
            this.recipeService.deleteRecipe(this.recipe.id);
            this.router.navigate(['/recipes']);
          }
        }
      ]
    }).then(altCtrl => {
      altCtrl.present();
    });
  }

}

Template for recipe-detail.page.html

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/recipes"></ion-back-button>
    </ion-buttons>
    <ion-title>{{ recipe.title }}</ion-title>
    <ion-buttons slot="primary">
      <ion-button (click)="onDeleteRecipe()" color="danger">
        <ion-icon name="trash" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-grid class="ion-no-padding">
    <ion-row>
      <ion-col class="ion-no-padding">
        <ion-img [src]="recipe.imageUrl"></ion-img>
      </ion-col>
    </ion-row>
    <ion-row>
      <ion-col>
        <h1 class="ion-text-center"> {{ recipe.title }} </h1>
      </ion-col>
    </ion-row>
    <ion-row>
      <ion-list>
        <ion-item>{{ recipe.ingredients }}</ion-item>
      </ion-list>
    </ion-row>
  </ion-grid>
</ion-content>

Related posts

Spread the love

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top