behaviorsubject api endpoint unrelated

Create Ionic App that uses BehaviorSubject and Shared Service
Nande Konst, Monday, December 10, 2018

Recently I found myself in the situation of having two unrelated components in an Ionic application that needed to make use of the same data coming from an API endpoint. There are multiple ways of letting two unrelated components listen to the same information, but today I would like to focus on solving this kind of problem by creating a shared service using the BehaviorSubject.

The idea of a shared service is that both components can subscribe to the latest value of the data using a BehaviorSubject which is emitted by the service. This way both components can use the same data without needing to be updated.

In order to demonstrate this behavior, we will create a small application in Ionic. You can find a demo here. If you want to use the repo and play with it you can find it here. The application will show a list of home schooling teachers. In the settings you can set your location and the list will be updated accordingly. In this example we will use Jexia as a platform to store our data in and generate our API endpoints. You will need to register in order to use their service. If you don’t have an account yet you can create one here:

Set up Jexia

After you have created your Jexia account, create a new project, add a dataset and add the following fields. You can add as many fields as you want.

  • name
  • subject
  • location
  • price

If you don’t know how to create a project and a dataset in Jexia, please follow the docs. Create an API key and add Policies like described here.

Now fill your dataset with some data. You can use the Data Console to do this. If you don’t know how to do this, find the instruction here.

Create an Ionic App

After you’ve set up everything in Jexia, create an Ionic app by running the following command:

$ ionic start myApp tabs --type=angular

Install the Jexia SDK and the Angular module to interact with your data like this:

$ npm install ng-jexia jexia-sdk-js

Go to your app.module.tsand import the

ngJexiaModule: import { NgJexiaModule, DataOperationsModule } from ‘@jexia/ng-jexia’.

In the imports section you need to initialize the module. The module will take care of authenticating the dataset, so the only thing you will need to do is provide your projectIDwhich you can find under settings in the Jexia dashboard, and thekey and secret of your API key.

imports: [
...
NgJexiaModule.initialize({
 projectID:'<your-project-id>'
 key: '<your-key>',
 secret:'<your-secret>',
 providers: [
 DataOperationsModule,
 ],
 })
]

Creating dataservice

With our connection to the datastore all being set up, it is time to create a service so we can start interacting with our data. In your app create a folder “services”, cd into it and create a file named jexia-data.service.ts.You can do this using the cli command

$ ionic generate service jexia-data.

Now import Datasetand fieldfrom the jexia-sdk-jsand DataOperationsfrom ng-jexia

import { Dataset } from 'jexia-sdk-js/api/dataops/dataset';
import { field } from 'jexia-sdk-js/api/dataops/filteringApi';
import { DataOperations } from '@jexia/ng-jexia';

We will also need Observableand BehaviorSubjectfrom rxjs

import { Observable, BehaviorSubject } from 'rxjs';

What I really like in Typescript is its type checking nature. This is why I like to use interfaces when it comes to working with objects. So let’s create two interfaces which we will need later on. Create another folder in the root which is called interfaces, create a file called ITeacher.tsand another one calledILocation.tsThere is a convention that names of interfaces always start with a capital I, that’s why we name it ITeacherand ILocation.The interface will look like this:

ITeacher.ts

export interface ITeacher {
   id: string,
   updated_at: Date,
   name: string,
   subject: string,
   yrs_of_exp: number,
   location: string,
   price: number,
   available: boolean
}

Ilocation.ts

export interface ILocation{
  city: string
}

Import the interfaces in your service like this:

import { ITeacher } from '../interfaces/ITeacher';
import { ILocation } from '../interfaces/ILocation';

Now we are ready to start coding! First add the Jexia DataOperationsmodule to the constructor so we can start using it:

constructor(public dataOperations: DataOperations) {   }

The first thing we want to do is fetch our data. Later, we want to display the data in our component, but first we have to fetch the data from our Jexia data store. We can do this by creating a property of the type Dataset with anITeacherinterface and set it to the name of your dataset, which is teachers.

teacherdataset: Dataset<ITeacher> = this.dataOperations.dataset<ITeacher>('teachers');

Now we need to select it and execute it. We can do this by creating another property called teachers.This property is a Promiseand holds an array of the type ITeacher. Jexia’s SDK is Promise based, so it’s good to understand that every time we are interacting with data we have to deal with Promises. Thisteachersproperty we can later call in our components to fetch our JSON with records coming from the Jexia backend.

teachers: Promise<ITeacher[]> = this.teacherdataset.select().execute();

In the service we want to create two functions. The first function called

getAllTeachers()

will emit the latest value of the entire dataset, which we will show by default, or when the settings field is left blank.

Now we need to create and initialize the BehaviorSubject, which will make sure components can subscribe always to the current value:

teacherSource: BehaviorSubject<ITeacher[]> = new BehaviorSubject<ITeacher[]>([])

The BehaviorSubject has the characteristic that it stores the “current” value. Every component that consumes the service and subscribes to it, will always get the last emitted value from the BehaviorSubject. A BehaviorSubject can be both Observable and Observer. In this case, we only care about the Observable part and give other components of the app the ability to subscribe to our Observable. We can return only the Observable part with the help of the function:

asObservable()

Create another property which will hold the current value:

currentMessage: Observable<ITeacher[]> = this.teacherSource.asObservable();

Now we can create the function that will get all the teachers for us:

getAllTeachers(){
  this.teachers.then(data => {
    this.teacherSource.next(data);
  })
}

This function will emit the latest value using the method

next()

We will call this function later by default in our home component when the currentMessage has not yet been filled.

The second function is

updateTeachers(location: ILocation)

It has one argument, the location. This function receives a location from the settings component. Jexia comes with handy filtering functions to filter your data. When a users sets a location in the settings component we want to filter our set of records and only show the relevant ones to the user. In this case the BehaviorSubject needs to emit the current value to the home component based on the settings which just have been set by the user. Here we see the big benefit of our shared service and the behavior subject. One of the components performs an actions on the data and the other component receives the current value in order to show the most recent data directly on the screen. First let’s create the filter. Use the field operator from the jexia sdk, which let you perform a query on the dataset.

updateTeachers(location: ILocation): void{
let filterCondition = field("location").isEqualTo(location.city)
this.teacherdataset.select().where(filterCondition).execute().then(data => {
     //sending out the messagestream
     this.teacherSource.next(data)
   });
}

Now we can execute the query and let the BehaviorSubject emit the value.

The components

That’s it for our service. Let’s start working on the components that will have to display our data. When you are generating an Ionic app using the tabs argument, the app will create a home, contact and tabs component by default. We will need the home component and a settings component. These will be the components that will interact with our data. The tabs component is our navigation, so we will also need this component. Delete the contact and all its reference in tabs.module.ts and tabs.router.module.ts component and generate a new component called settings by using the cli command:

$ ionic generate component settings

Go to the tabs component and add the settings route:

<ion-tabs>
<ion-tab label="Home" icon="home" href="/tabs/(home:home)">
   <ion-router-outlet name="home"></ion-router-outlet>
 </ion-tab>
  <ion-tab label="Settings" icon="cog" href="/tabs/(settings:settings)">
   <ion-router-outlet name="settings"></ion-router-outlet>
 </ion-tab>
</ion-tabs>

The Home Tab (home.ts)

In the home tab we want to show all the teachers by default, when there is no location set in the settings tab. When a location has been set in the settings component we will show the current value of the BehaviorSubject which we will store in a property which we call currentTeachers. Let’s see how we can get to our records from the backend. First import the JexiaDataServiceand the ITeacherinterface.

import { JexiaDataService } from '../services/jexia-data.service';
import { ITeacher } from '../interfaces/ITeacher';
import { ILocation } from '../interfaces/ILocation';

Don’t forget to provide the JexiaDataService in the constructor:

constructor(private jexiaDataService: JexiaDataService) {}

First create a property in the component called currentTeachers. This will be an ITeacher array. In this property we will show the current state of our records:

currentTeachers: ITeacher[];

Now we will create a function which will always show the nearest teacher, or when the BehaviorSubject is still empty, which it is by default, we will show all teachers by calling getAllTeachers()from the shared service.

Create a new functionshowNearestTeacher()

The first thing we want to do is subscribe to the current value of our behavior subject:

showNearestTeacher(){
this.jexiaDataService.currentMessage.subscribe(message => this.currentTeachers = message)
}

Since the currentMessagewill be an empty array initially, we can check on this and call getAllTeachers() to show all the teachers by default. We will also need to check if the local storage is empty or not.

Import Storage and provide it to the constructor:

import { Storage } from '@ionic/storage';
constructor(private jexiaDataService: JexiaDataService, private storage: Storage) {}

Now get back to the function. If there is a value set in local storage, we will need this value to update the recordset:

showNearestTeacher(){
  this.jexiaDataService.currentMessage.subscribe(message =>   this.currentTeachers = message
 
  //check if there is a value in local storage
   this.storage.get('location').then((val) => {
      //if the currentTeachers has not yet been filled and local storage is empty, use the entire dataset with records
   if((this.currentTeachers == undefined || this.currentTeachers.length == 0) && (val == undefined)) {
     this.jexiaDataService.getAllTeachers();
   } else {
     this.location = JSON.parse(val);
     this.jexiaDataService.updateTeachers(this.location)
   } 
 })  
}

Call the function in ngOnInit(), which means the function will be called when the component is initialized:

ngOnInit(){    
 this.showNearestTeacher();    
}

Displaying the information (home.html)

Ionic comes with handy UI components that you can use to display the data. We will display our data in a grid of cards like this:

<ion-header>
 <ion-toolbar>
   <ion-title>Home</ion-title>
 </ion-toolbar>
</ion-header>
<ion-content padding class="home">
 <ion-grid >
  
       <div class="current-teachers" *ngIf="currentTeachers">
         <ion-card  *ngFor="let teacher of currentTeachers">
           <ion-item>
               <ion-card-title>{{teacher.name}}</ion-card-title>
               <ion-button fill="outline" slot="end">View</ion-button>
           </ion-item>
           <ion-card-content>
                   <ion-item>
                     <ion-icon name="pin" slot="start"></ion-icon>
                     <ion-label> {{teacher.location}} </ion-label>
                   </ion-item>
                   <ion-item>
                      <ion-icon name="ios-cube" slot="start"></ion-icon>
                     <ion-label>
                       {{teacher.subject}}
                     </ion-label>
                   </ion-item>
                   <ion-item>
                     <ion-icon name="logo-euro" slot="start"></ion-icon>
                     <ion-label>
                         {{teacher.price}}
                     </ion-label>
                   </ion-item>
                   <ion-item>
                       <ion-icon name="ios-paper" slot="start"></ion-icon>
                         <ion-label>
                              {{teacher.yrs_of_exp}}
                         </ion-label>
                       </ion-item>
                   <ion-item>
                     <ion-icon name="ios-clock" slot="start"></ion-icon>
                       <ion-label>
                           {{teacher.available}}
                       </ion-label>
                     </ion-item>
           </ion-card-content>
         </ion-card>
          <p *ngIf="currentTeachers == 0" >There are no teachers in this region</p>
       </div>
 </ion-grid>    
</ion-content>

If the currentTeachersproperty is filled we loop through it. If the result is 0, which means that we couldn’t find the location, we will send a message to the user that there were no teachers found.

The Settings Tab (settings.ts)

In the settings component, the user can set his location. When the location has been saved, the records will be filtered based on it. The home component subsequently will show us the updated version of the data.

Our settings page has one input field where the location can be typed in. Then there is a button where this location can be saved.

The first thing we want to do is save our settings when we filled out the location field and memorize the value of it by putting it in local storage. When we don’t set a location we want to show all available teachers and clear the local storage. If there has a new location been set we need to update the data.

OnInit we will check if the local storage is filled. Create a functiongetLocationLocalStorage()and call it on init.

ngOnInit(){
  this.getLocationLocalStorage();
}
getLocationLocalStorage(){
 this.storage.get('location').then(val => {
  if(val! = undefined) {
    let location = JSON.parse(val);
    this.city = location.city;
  } 
 }
}

Now create asaveSettings()function. We need to create this function async because of the message from Angular Material we will show when the settings are updated.

We will first create an object with a city field which will be filled by the city property. If we didn’t fill in the location the city field in the object will be empty. In that case we want to display all the teachers and clear the local storage. We will also display a message that says that our settings have been saved to default. If the location was filled out by the user we want to update our dataset by calling updateTeacher()where we will send the location as the filtering argument. updateTeacher(location)is now emitting the new value where the home component will subscribe to. Also we want to store the new location to the local storage and display a message that our new settings have been saved.

async saveSettings(){
  let location = {city: this.city} as ILocation
 
   if(location.city == ""){
     this.storage.clear();
     this.jexiaDataService.getAllTeachers();
     const toast = await this.toastController.create({
       message: 'Your settings have been saved to default.',
       duration: 2000
     });
     toast.present(); 
   } else {
     this.jexiaDataService.updateTeachers(location);
     this.setLocationLocalStorage(location)
    
     const toast = await this.toastController.create({
       message: 'Your settings have been saved.',
       duration: 2000
     });
     toast.present(); 
   }
 }

Create a function setLocationLocaStorageand pass the location as argument:

setLocationLocalStorage(location: ILocation){
 this.storage.set('location', JSON.stringify(location))
}

Displaying the settings form (settings.html)

On submit we call saveSettings(). The input has a ngModelassigned to the city property to get the value of the input field.

<ion-header>
 <ion-toolbar>
   <ion-title>settings</ion-title>
 </ion-toolbar>
</ion-header>
<ion-content padding>
 <ion-grid>
   <ion-row>
     <ion-col>
       <ion-list>
         <form (ngSubmit)="saveSettings()">
           <ion-item>
             <ion-label fixed>
               City
             </ion-label>
             <ion-input [(ngModel)]="city" name="city" type="text"></ion-input>
           </ion-item>
             <ion-button expand="full" type="submit" color="primary" fill="outline">Save Settings</ion-button>
         </form>
       </ion-list>
     </ion-col>
   </ion-row>
 </ion-grid>
</ion-content>

Now you know how to create an application with a shared service that emits the last value using a BehaviorSubject and have two unrelated components use the same data. It’s a very simple but useful rxjs pattern.



Also published on Medium.

Tags: , ,

Categorised in: ,

This post was written by Nande Konst

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.