Angular 5 HTTP Form Inputs & RxJS
Angular 5 Form Inputs
Whatever framework you are using, forms are a key component of any website or web application. They provide us with one of the most important aspects of user interaction. We need to give the user a clear understanding of what information we require, while also validating his/her data in the process.
If we want to provide a real-time interface for the user that reacts to his/her inputs and selections, we can use RxJS Observables and Angular form controls.
This tutorial is based on my application Beer Name Finder which you can view here:
The application takes user input, sends off a HTTP request while the user is typing and retrieves a new dataset based on their current search term in real-time.
The user can also change the beer category through a drop down selector in the same way to get a similar user interface update.
The full repository for Beer Name Finder is available here.
I think this is one of the coolest things about modern UI manipulation when combined with modern frontend frameworks. And surprisingly, it is not overly difficult to get running once it is broken down into different segments. I will detail how to go about that in this post.
And if you enjoy this dashboard, please consider giving it an upvote on Product Hunt here.
On a side note, if your looking to expand your Angular and Typescript knowledge I highly recommend this Angular & TypeScript book by Yakov Fain.
So, let’s begin.
Populate an Angular Input
Let’s say we want to populate a select dropdown field from a HTTP call. I used this process for my Beer Application so I’ll be using that as an example.
To start, I retrieved a list of beer categories from the BreweryDB API and applied them to a select dropdown field.
In our search.component.ts
I declared the main variable types used:
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { FormControl } from '@angular/forms'; import { BeerService } from '../beer.service'; import { Beer } from '../beer'; import { moveIn } from '../animations'; import { ChangeDetectorRef } from '@angular/core'; export class SearchComponent implements OnInit { term = new FormControl(); beers: Array; categories: {}; currentCategory: String; }
The FormControl
is the most notable declaration here:
term = new FormControl();
It tracks the value and validation status of an individual form control e.g input field.
It is one of the three fundamental building blocks of Angular forms, along with FormGroup and FormArray.
I declared an array of type Beer from an imported TypeScript definition:
beers: Array;
This maps our desired beer properties from our HTTP request to a beer object.
My imported type definition for Beer looked like this:
export class Beer { _id: string title: string; url: string; description: string; rating: number; abv: string; labels:{ } style: { category: { id: number, name: string } } }
I then initialised my data service (beer.service) in the constructor:
constructor(private _beerService: BeerService) { }
I used a method in _beerService
to retrieve the beer categories to populate the select dropdown field.
In my lifecycle hook ngOnInit
I then invoke two custom functions:
ngOnInit() { this.initializeDropdowns() this.listenForInput(); }
The first initialization of the dropdown contains this functionality:
initializeDropdowns() { this._beerService.getBeers() .subscribe((res: Beer[]) => { this.beers = res; this._beerService.getCategories().then(res => { this.categories = res; }) }) }
I got an initial dataset of beers from our getBeers()
function I have covered this function extensively in another post here:
After this, I invoked the getCategories()
method from the beerservice
file which contains the following functionality:
getCategories() { return new Promise((resolve, reject) => { this._http.get(this._baseUrl + "/categories?key=" + this.apiKey) .subscribe((res) => { this.categories = res['data']; if (this.categories) { let categories = Array.from(new Set(this.categories)); resolve(categories); } else { reject("Categories Undefined") } }) }) }
This is yet another HTTP request similar to all others in the service.
this._baseUrl
refers to the BreweryDB API endpoint:
private _baseUrl: string = "https://api.brewerydb.com/v2/"
The only unusual thing in the getCategories
method is the Array.from(new Set(this.categories))
This could have been done a number of ways to get a new array object from an iterable but I used the Array.from() method here but this is just personal preference.
So now my categories
object will now contain data from our request that look something like this:
If we now examine our markup for the form we can see this:
Just concentrating on the select
field for the moment:
We can see that the *ngFor
directive has iterated through our retrieved categories array. At this point, I navigated and selected a category option.
This next section is possibly the most interesting thing about creating a reactive input in Angular 5. It uses RxJS methods to track data from an input field and allows you to manipulate that data from that input field as an Observable.
We now are going to look at the method listenForInput()
:
listenForInput() { this.term.valueChanges .debounceTime(400) .distinctUntilChanged() .subscribe(term => this.searchTerm(term, this.currentCategory)), function (error) { console.log("Error happened" + error) }, function () { console.log("the subscription is completed") } }
So starting with:
this.term.valueChanges
This term value is a reference to our FormControl
declared earlier in the file:
term = new FormControl();
The debounce time:
.debounceTime(400)
Waits for a new change in the input field from the user every 400 milliseconds. If a change occurs before 400 milliseconds from the previous change it discards those values.
The distinct method:
.distinctUntilChanged()
Ensures that there is a new value in the input field before declaring that theres been a change.
Once this criteria is met, we are finally able to subscribe to the Observable:
.subscribe(term => this.searchTerm(term, this.currentCategory)), function (error) { console.log("Error happened" + error) }, function () { console.log("the subscription is completed") }
From here I have invoked:
this.searchTerm(term, this.currentCategory)
This is just another http call to retrieve a list of beers using the parameters term
and this.currentCategory
which will filter the response to the desired dataset.
searchTerm(search, category) { this._beerService.searchBeer(search, category) }
I’ll show you this searchTerm method as well:
searchBeer(term, cat) { this.spinnerService.show(); if(term && term.length > 0 ) { return new Promise((resolve, reject) => { return this._http.get(this._searchUrl + `${term}` + this.apiKey) .subscribe(res => { this.searchResults = res['data']; this.spinnerService.hide(); if (cat !== "All" && cat !== undefined) { const filteredByCat = this.searchResults.filter(beer => (beer.style != undefined && beer.style.category.name === cat)) this.beerAnnouncedSource.next(filteredByCat); } else { this.beerAnnouncedSource.next(this.searchResults); this.result = this.searchResults; } this.searchEnabled = true; resolve(this.beer); }) }); } this.spinnerService.hide(); return(this.beer) }
This seems quite complicated but its actually not doing much more different than any other of our HTTP calls in the service.
We are using the term from our input field in the api request:
return this._http.get(this._searchUrl + `${term}` + this.apiKey)
We are making sure that the category property exists in the our respoinse:
if (cat !== "All" && cat !== undefined) { const filteredByCat = this.searchResults.filter( beer => (beer.style != undefined && beer.style.category.name === cat)) this.beerAnnouncedSource.next(filteredByCat); }
But most importantly, we are assigning this new information to our beer observable source by using .next()
.
this.beerAnnouncedSource.next(filteredByCat);
This beer observable is being subscribed to elsewhere in the application so the UI will automatically get refreshed which is very handy.
And when we have selected a new value currentCat(selectedCategory.value)
will be called:
(change)="currentCat(selectedCategory.value)"
This is just one example of using RxJS and Observables to track a users interaction with a form element and have the UI refresh accordingly.
On a side note, Observables in general should be used when there is more than one stream of data. This is when they are most effective. But its fun to demonstrate their power in an application such as this.
The full repository for Beer Name Finder is available here.