Flatten Data Structures
Angular Asynchronous RxJS SPA Typescript Visual Studio Code

How to Flatten Structures with RxJS Pipes and Maps

Welcome to today’s post.

In today’s post I will discuss how we can flatten structures emitted from Observables using the RxJS library. The RxJS library allows us to compose asynchronous and event-based applications using sequences of observables.

I will focus on piping and mapping of observables.

The operators that we will make use of within the RxJS library are the pipe() and map() operators.

Before I show how to use these operators achieve our intended purpose, I will explain what each operator does.

The Pipe() Operator and Transformational Operators

The pipe() operator takes an observable function and returns another observable as an output. The observable functions are applied in the order they are received from the observable source. The simplest way to understand the pipe operator is to think of it is a stream of marbles running down a runway and applying an action to each marble.

With each observable, we can apply any number of functions including transformational and filtering operators such as map(), filter(), tap() and so on.

For more detailed reference for the various functions refer here.

An example of how we can apply the pipe(map()..) operators is when  we wish to flatten a nested observable structure (also known as a higher-order observable).

The flattened structure can then used within a user interface or a tabular report without having to navigate through layers of objects in a structure, or to load more fields into memory than is necessary, which will be a cost overhead when loading the interface or report.

Before I show how to transform fields from two observables, I will show an example in the next section of how we combine the results from two observables into one result.

Combining Results from two Observables

In the example we have one observable input from a service that retrieves a specific book from a catalogue:

let getBook$ = this.apiService.getBook(this.id);

and another observable that retrieves the book reviews for the same record:

let getBookReviews$ = this.apiService.getBookReviews(this.id);

The data structure for a book review report are shown below:

import { BookReview } from "./BookReview";

export class BookReviewReport
{
    public id: number;
    public title: string;
    public author: string;
    public yearPublished: number;
    public genre: string;
    public edition: string;
    public isbn: string;
    public location: string;
    public reviews: BookReview[];
}

The detail property reviews is an array of BookReview structures that are as shown:

import { Book } from "./Book";

export class BookReview
{
    public id: number;
    public reviewer: string;
    public author: string;
    public title: string
    public heading: string;
    public comment: string;
    public rating: number;
    public dateReviewed: Date;
    public bookID: number;
    public book: Book;
}

Our report screen will be a header record and detail records for the review records.

When the initialize the form, we combine the resulting data from both observables as shown:

ngOnInit() {
    if (!this.activatedRoute)
        return;
    this.id = this.activatedRoute.snapshot.params["id"];

    let bookReviewReport = new BookReviewReport();
    bookReviewReport.reviews = [];

    let getBook$ = this.apiService.getBook(this.id);
    let getBookReviews$ = this.apiService.getBookReviews(this.id);

    this.subscription = combineLatest(
        [
            getBook$, 
            getBookReviews$
        ]
    )
    .subscribe(([book, reviews]) => {
        if (!book || !reviews)
            return of(null);
        console.log("number of reviews = " + reviews.length);
        console.log("book name = " + book['title']);        
  
        reviews.forEach(itm => 
        {
            bookReviewReport = { id: book.id,                     
                                 title: book.title,  
                                 author: book.author, 
                                 yearPublished: book.yearPublished,  
                                 genre: book.genre, 
                                 edition: book.edition, 
                                 isbn: book.isbn, 
                                 location: book.location, 
                                 reviews: reviews };                                     
        });
        this.bookReviewReport$.next(bookReviewReport);   
    });
}

We are using a combineLatest() join operator to combine the observables and construct a report instance which is emitted through the event handler:

bookReviewReport$: EventEmitter<BookReviewReport> = new EventEmitter();

For additional details on the CombineLatest() operator, refer to one of my previous posts where I showed how to combine the results from two observables.

One we have the combined data in one observable, I will show how to pipe and transform the observable into a flattened result.

Combining and Transforming Results from two Observables

In this section, I will show how to combine and transform data applying the pipe() and map() operators on the intermediate observable (from the previous section).

We will construct a pipe(map(..) structure to map the intermediate BookReviewReport observable to the respective declared variable instances within our report component that are shown below:

book: Book;
reviews: BookReview[] = [];

The mapping for the review data is shown below:

this.bookReviewReport$.pipe(
    map((r: BookReviewReport) => 
    {
        return (r != null) ? r.reviews: null; 
    })
).subscribe((r) => {
    this.reviews = r;
});

The mapping for the book header is shown below:

this.bookReviewReport$.pipe(
    map((r: BookReviewReport) => 
    {
        if (r == null)
            return null;
        let mappedBook: Book = new Book(); 
        mappedBook.id = r.id;
        mappedBook.author = r.author;
        mappedBook.edition = r.edition;
        mappedBook.genre = r.genre;
        mappedBook.isbn = r.isbn;
        mappedBook.location = r.location;
        mappedBook.title = r.title;
        mappedBook.yearPublished = r.yearPublished;
        return mappedBook; 
    })
    ).subscribe((r) => {
        this.book = r;
    });  
}

The data from the observables, which is now in object variables, can now be displayed in the interface.

Displaying the Flattened Data to the User Interface

Then we link the resulting data sources to the HTML view for the report.

For the report header this is done as shown:

<mat-card>
    …
    </mat-card-content>
  
    <mat-card-content *ngIf="book">
        <form>
            <div class="form-group">
                <label>Title:</label>
                <input readonly [(ngModel)]="book.title" name="title" 
                    class="form-control col-sm-6 input-sm" placeholder="Title" 
                    placement="right" ngbTooltip="Title">
            </div>
  
            <div class="form-group">
                <label for="location">Author:</label>
                <input readonly [(ngModel)]="book.author" name="author" 
                    class="form-control col-sm-4" placeholder="Author" 
                    placement="right" ngbTooltip="Author">
            </div>
  
            <div class="form-group">
                <label for="location">Year Published:</label>
                <input readonly [(ngModel)]="book.yearPublished" 
  			        name="yearPublished" 
                    class="form-control col-sm-4" placeholder="Year Published" 
                    placement="right" ngbTooltip="Year Published">
            </div>
   
            <div class="form-group">
                <label for="location">Location:</label>
                <input readonly [(ngModel)]="book.location" name="location" 
                    class="form-control col-sm-4" placeholder="Location" 
                    placement="right" ngbTooltip="Location">
            </div>
  
            <div class="form-group">
                <label for="genre">Genre:</label>
                <input readonly [(ngModel)]="book.genre" name="genre" 
                    class="form-control col-sm-4" placeholder="Genre" 
                    placement="right" ngbTooltip="Genre">
            </div>
        </form> 
    </mat-card-content>
</mat-card>  

For the review detail records, the HTML part of the interface is shown below:

<mat-card-content>
    <div class="comments-grid">
        <div style="display: flex; flex-wrap: wrap; border-style: solid; border-width: 1px; background-color: cadetblue;">
            <div style="flex: 0 40%;"><u><strong>Reviewer</strong></u></div>
            <div style="flex: 0 40%;"><u><strong>Comment</strong></u></div>
            <div style="flex: 0 20%;"><u><strong>&nbsp;</strong></u></div>
        </div>

        <mat-list *ngFor="let review of reviews">
            <div style="display: flex; flex-wrap: wrap; border-style: solid; 
border-width: 1px;">
                <div class="grid-row" style="flex: 0 40%;">{{review.title}}</div>
                <div class="grid-row" style="flex: 0 40%;">{{review.comment}}</div>
                <div class="grid-row" style="flex: 0 20%;">&nbsp;</div>
            </div> 
        </mat-list>
    </div>
</mat-card-content>

As we can see, the pipe(map(..)  RxJS operator combination has been used to map an array of detail records from an observable structure and also map a subset of fields from an observable structure.

If the data source for the report were an array, then we could also have applied an additional filter within our pipe to retrieve a subset of records for a drill down report. 

The resulting report should look something like the screen below:

We can see how we can take the rather abstract concept of nested observables and apply useful function operators to flatten out the structure to help us provide data for our UI form.

The most important thing to note before we apply the pipe() and map() operator on our observables is to understand the data structures we are extracting data from and what data fields we wish to select, extract and transform.

That is all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial