Flatten Data Structures
Angular Asynchronous RxJS SPA Typescript Visual 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 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).

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();

We then construct a pipe(map(..)  to map the BookReviewReport observable to the respective variable instances within our report component:

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;
    });  
}

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, this 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 extract or transform.

That is all for today’s post.

I hope you found this port useful and informative.

Social media & sharing icons powered by UltimatelySocial