Developer Blog

Tipps und Tricks für Entwickler und IT-Interessierte

Angular I18N

Angular | Working with I18N

In this post, you will learn how to get started with Angular I18n using ngx-translate, the internationalization (i18n) library for Angular. We will cover the following topics:

  • setup new angular app
  • install required dependencies
  • add bootstrap as ui framework
  • create app with demo page and translation services

This will be the final result (click to show video). Source code for this post is on GitHub.

Setup new Angular app

➜ ng new app
➜ cd app

Add required modules

➜ npm install @ngx-translate/core @ngx-translate/http-loader rxjs --save

Add Bootstrap as UI framework

We need the libraries for Bootstrap and flag icons. Download the required files into your asset/vendor/bootstrap/5.3.1 folder:

Flags Icons need a CSS file with the corresponding images for the flags, so we use the archive from GitHub.

Download and extract the archive into the folder assets/vendor/flag-icons

Current folder structure

This is our current folder structure:

Setup Application

We choose the following structure for the HTML architecture.

  • index.html contains the required css and js files. Also, the <app-root> component, which loads our app
  • app.component.html contains the main structure of every page. This includes header, navigation, place for main content and footer
  • The main content is inserted via the <router-outlet>

index.html

We will add the corresponding file in the main index.html in our project.

<!doctype html>
<html lang="en" class="h-100">

<head>
  <meta charset="utf-8">
  <title>I18N</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">

  <link rel="stylesheet" href="/assets/vendor/bootstrap.min.css">
  <link rel="stylesheet" href="/assets/vendor/flag-icons.min.css">

  <link rel="stylesheet" href="/assets/vendor/bootstrap/5.1.3/css/bootstrap.min.css">
  <link rel="stylesheet" href="/assets/vendor/flag-icons/css/flag-icons.min.css">

  <link rel="stylesheet" href="/assets/css/default.css">

</head>

<body class="d-flex flex-column h-100">
  <app-root class="h-100"></app-root>

  <script src="assets/vendor/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
</body>

</html>

app.component.html

We borrow the main structure from the bootstrap example ‘Sticky Footer with Navbar‘ with some changes in the navigation bar.

<header>
    <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-primary">
        <div class="container-fluid">
            <a class="navbar-brand" href="#">
                <img src="assets/img/logo-angular.png" height="40px" width="auto">
            </a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" 
                data-bs-target="#navbarCollapse"
                aria-controls="navbarCollapse" 
                aria-expanded="false"     
                aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarCollapse">
                <ul class="navbar-nav me-auto mb-2 mb-md-0">
                    <li class="nav-item">
                        <a class="nav-link active" aria-current="page" href="#">Home</a>
                    </li>
                </ul>

                <div class="btn-group dropstart">
                    <button class="btn btn-primary dropdown-toggle" type="button" 
                        id="dropdownFlags"
                        data-bs-toggle="dropdown" aria-expanded="false">
                        <span class="flag-icon flag-icon-{{currentlang}}"></span>
                    </button>
                    <ul class="dropdown-menu">
                        <li *ngFor="let lang of languages" [value]="lang" 
                            (click)="useLanguage(lang)">
                            <a class="dropdown-item"
                                [ngClass]="{'active': currentlang == lang}">
                                <span class="flag-icon flag-icon-{{lang}}"></span>
                                &nbsp;
                                {{lang | uppercase}}
                            </a>
                        </li>
                    </ul>
                </div>
            </div>
        </div>
    </nav>
</header>
<main class="flex-shrink-0">
    <div class="container">
        <router-outlet></router-outlet>
    </div>
</main>
<footer class="footer mt-auto py-3 bg-dark">
    <div class="container">
        <span class="text-muted">(C) Ralph Göstenmeier</span>
    </div>
</footer>

app.component.ts

import { Component } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';

import defaultLanguage from '../assets/i18n/de.json';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss'],
})
export class AppComponent {
    title = 'I18N';

    languages = ['us', 'de', 'fr', 'sp'];
    currentlang = 'us';

    constructor(private translate: TranslateService) {
        this.currentlang = 'de';
        translate.setTranslation(this.currentlang, defaultLanguage);
        translate.setDefaultLang(this.currentlang);
    }

    ngOnInit(): void {}

    useLanguage(language: string): void {
        this.currentlang = language;
        this.translate.use(language.toLowerCase());
    }
}

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClient, HttpClientModule } from '@angular/common/http';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';

import { HomePageComponent } from './pages/home/component';
import { DemoPageComponent } from './pages/demo/component';

@NgModule({
    declarations: [AppComponent, HomePageComponent, DemoPageComponent],
    imports: [
        BrowserModule,
        AppRoutingModule,
        HttpClientModule,
        TranslateModule.forRoot({
            loader: {
                provide: TranslateLoader,
                useFactory: HttpLoaderFactory,
                deps: [HttpClient],
            },
        }),
    ],
    providers: [],
    bootstrap: [AppComponent],
})
export class AppModule {}

// required for AOT compilation
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
    return new TranslateHttpLoader(http);
}

How the app works

The translation is done with the ngx-translate component.

Translation works with different JSON files (for each language a separate file), containing the required translation for each text to be displayed. Each text is addressed with a name within the JSON file.

So, the base structure of each JSON file is the following:

Translation files

assets/i18n/de.json

{
  "i18n-demo-header": "I18N Demo",
  "header": "I18N Funktionalität in Angular"
}

assets/i18n/us.json

{
  "i18n-demo-header": "I18N Example",
  "header": "I18N Functionality in Angular"
}

These translations could be used in a html file by using the translate pipe:

<h1>{{'header' | translate }}</h1>

More information and examples are here.

Changing the language is done with the help of the TranslateService

Inject your app with the TranslateService (in app.component.ts)

constructor(private translate: TranslateService) {
    translate.setDefaultLang('de');
}

Change Language

useLanguage(language: string): void {
    this.translate.use(language.toLowerCase());
}

Integrate in our UI

To easy switching the language, we have to do a few steps

Add a dropdown menu to our navigation bar.

And switching the language is done by calling useLanguage within each menu item:

<div class="btn-group dropstart">
    <button class="btn btn-primary dropdown-toggle" type="button"
        id="dropdownFlags"
        data-bs-toggle="dropdown" aria-expanded="false"><span
        class="flag-icon flag-icon-{{currentlang}}"></span>
    </button>
    <ul class="dropdown-menu">
        <li *ngFor="let lang of languages" [value]="lang"
            (click)="useLanguage(lang)">
            <a class="dropdown-item" 
                [ngClass]="{'active': currentlang == lang}">
                <span class="flag-icon flag-icon-{{lang}}"></span>
                &nbsp;
                {{lang | uppercase}}
            </a>
        </li>
    </ul>
</div>

Setup a list for all menu items:

<ul class="dropdown-menu">
<li *ngFor="let lang of languages" [value]="lang" (click)="useLanguage(lang)">

Scrimba: JavaScript Advent Calendar

Solutions for the Scrimba JavaScriptmas Challenge

Day 24: Test Your Agility!

var pushed = false //Has the stop button been pushed - false is default
var targetInt; //The target number to stop the wheel on
var spinningElem = document.getElementById('spinning'); //The spinning number


const maxLoop = 10;
const waitTime = 250;

//event listener
document.getElementById("buttonPressed").addEventListener("click", buttonPressed);

//When the stop button is pushed
function buttonPressed(){
    pushed = true;
}

//set the target Int
function setTargetInt(){
    var targetElem = document.getElementById('targetNum');
    targetInt=Math.floor(Math.random() * (maxLoop + 1))
    targetElem.innerHTML = targetInt;
}

//sleep const
const sleep = (milliseconds) => {
  return new Promise(resolve => setTimeout(resolve, milliseconds))
}


var curr_spin=-1;

//EDIT THIS FUNCTION
const spin = async () => {
    var spinningElem = document.getElementById('spinning');
    for(let spin=0; spin < maxLoop; spin++) {
        spinningElem.innerHTML = spin;
        console.log('spin:', spin, targetInt, curr_spin)

        if (!pushed) {
            //
        } else if (spin == targetInt) {
            console.log('Button pushed and won')
             var result = document.getElementById('result');
            result.innerText = "WON!";
            spin = maxLoop+1;
        } else {
            console.log('Buitton pushed but missed')
            spin = maxLoop+1;
        }
        
        await sleep(waitTime)
    }
}

//main
setTargetInt();
spin();

Day 23: Social media input challenge

<html>
    <head>
        <link rel="stylesheet" href="index.css">
    </head>
    <body>
        <div class="container">
            <textarea type="text" id="tweet" placeholder="Type in the box"></textarea>
            <div id="counterFooter">140/140</div>
            <button id="btn"><h2>Tweet</h2></button>
        </div>
        <script src="index.js"></script>
    </body>
</html>
body{
  background-color: #023F6A;
  font-family: sans-serif;
}
.container{
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
}
textarea{
  width:5
  height: 30vh;
  background-color: rgb(21, 32, 43);
  color: #fff;
  border-radius:10px;
}
textarea::placeholder{
    color:#fff;
}
#counterFooter {
  margin-top: 0.2rem;
  font-size:0.8rem;
  color: #fff;
  margin-left:3
  font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
button{
  width:5
  background-color: rgb(29, 161, 242);
  border-radius: 10px;
  padding: 0 1
}
.tweetRed {
    color: red;
}
button h2{
    color: #fff;
}
.buttonDisabled {
   opacity: .5;
   cursor: default;
}
const counter = document.getElementById('counterFooter');
const button = document.getElementById('btn');
function updateState() {
    const nrOfChar = 140 - tweet.value.length;
    
    counter.innerText = nrOfChar + '/140';
    
    (nrOfChar < 20) ? tweet.classList.add("tweetRed") : tweet.classList.remove("tweetRed");    
    (nrOfChar <  0) ? btn.classList.add("buttonDisabled") : btn.classList.remove("buttonDisabled");
}
function handleKeyUp(event) {
    updateState()
}
tweet.addEventListener('keyup', handleKeyUp);
updateState()

Day 22: Extract Matrix Column

function extractMatrixColumn(matrix, column) {
    let extracted = []
    
    matrix.forEach( row => {
        extracted.push(row[column])
    })
    
    return extracted;
}
function extractMatrixColumn(matrix, column) {   
    return matrix.map( row => row[column])
}

Day 21: Sum of 2

function sumOfTwo(nums1, nums2, value) {
    let result = false;
    
    nums1.forEach( num1 => {
        nums2.forEach( num2 => {            
            if ( (num1 + num2) === value) {
                result = true;
            }
        })
    })
    return result;
}

Day 20: Domain Type

function domainType(domains) {
    map = { "org": "organization",
            "com": "commercial", 
            "net": "network",
            "info":  "information"
    }
    
    types=[]    
    
    domains.forEach( (domain) => {
        last = domain.split('.').slice(-1)[0]
        
        types.push(map[last])
    })
    
    return types
}

Day 19: Alpahabet Subsequence

function alphabetSubsequence(str) {
    chars = str.split("")
    
    result=true
    for(pos=1; pos<chars.length; pos++) {       
        result = result && chars[pos-1] < chars[pos] 
    }
    return result
}

Day 18: Array previous less

function arrayPreviousLess(nums) {
    let result = [];
    let temp=[];
    
    for(indx=0; indx < nums.length; indx++) {
        curr = nums[indx];
        
        // Build array with previous values
        temp.push(curr)
        
        // Find previous values less than current value
        mins = temp.filter( (val) => val < curr)
        
        // Return value at most right
        max = (mins.length == 0) ? -1 : max = mins[mins.length-1];
        
        result.push(max);
    }
    
    return result;
}

Day 17: Different symbols naive

function differentSymbolsNaive(str) {
    m={};
    
    n=0;
    str.split("").sort().forEach( (c) => {                
        if (! (c in m)) n++;
        m[c]=1;
    });
        
    return n
}

Day 16: Insert dashes

function insertDashes(arr) {
    return arr.split(" ").map( part =>part.split("").join("-") ).join(" ");
}

Day 15: Javascript Carousel

function getIndexOfCurrent(tags) {
	let result = -1;
	tags.forEach((node, indx) => {
		if (node.classList.contains('curr')) {
			result = indx;
		}
	});
	return result;
}
function move(dir) {
	var tags = Array.prototype.slice.call(
		document.getElementsByClassName('card')
	);
	let curr = getIndexOfCurrent(tags);
	if (dir === 'prev') {
		next = curr === 0 ? 0 : curr - 1;
	}
	if (dir === 'next') {
		next = curr === tags.length - 1 ? tags.length - 1 : curr + 1;
	}
	tags[curr].setAttribute('class', 'card last');
	tags[next].setAttribute('class', 'card curr');
}
<html>
    <head>
        <link rel="stylesheet" href="index.css">
    </head>
    <body>
        
        <div class="container">
            <img src="previous.svg" class="previous" alt="previous image">
            <div class="gallery-wrapper">
                <div class="gallery">
                    <img class="card curr" src="presents.jpg" alt="Christmas Cookies">
                    <img class="card" src="cookies.jpg" alt="Christmas Cookies">
                    <img class="card" src="santa.jpg" alt="Christmas Cookies">
                    <img class="card" src="candycane.jpg" alt="Christmas Cookies">
                    <img class="card" src="reindeer.jpg" alt="Christmas Cookies">
                </div>
            </div>
            <img src="next.svg" class="next" alt="next image">
        
        </div>
        
        <script src="index.pack.js"></script>
    
    </body>
</html>
/*
Thanks to these fine individuals from Unsplash:
https://unsplash.com/photos/AmzKuEnr1VY
https://unsplash.com/photos/eDnJQL21amc
https://unsplash.com/photos/LXy2DOOxESQ
https://unsplash.com/photos/KBKHXjhVQVM
https://unsplash.com/photos/PxM8aeJbzvk
*/
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600&display=swap');
html,
body {
	margin: 0;
	padding: 0;
	height: 100vh;
	font-family: 'Playfair Display';
	display: grid;
	justify-content: center;
}
img {
	width: 200px;
}
.previous,
.next {
	width: 35px;
	justify-self: center;
	align-self: center;
	cursor: pointer;
}
.previous {
	opacity: 0.3;
}
.container {
	display: grid;
	grid-template-columns: 2
	place-content: center;
}
.card {
	width: 10
	display: none;
}
.curr {
	display: block;
}
.curr {
	animation-duration: 5s;
	animation-name: slideIn;
}
.last {
	animation-duration: 2s;
	animation-name: slideOut;
}
@keyframes slideOut {
	from { opacity: 1; }
	to   { opacity: 0; }
}
@keyframes slideIn {
	from { opacity: 0; }
	to   { opacity: 1; }
}

Day 14: Maximal Adjacent Difference

    let result=0, max=0, val1=0, val2=0;
    
    for(pos=0; pos < nums.length; pos++) {
        
        [val1, val2 ] = [ nums[pos], nums[pos+1] ];
        max = (val1 > val2) ? (val1 - val2) : (val2 - val1);
        
        if (max > result) {
            result = max;
        }
    }
     
    return result;
}

Day 13: Extract Eacth Kth

function extractEachKth(nums, kth) {
    return nums.filter( (value, index) => (index+1)
}

Day 12: Valid Time

Number.prototype.between = function(min, max) {
  return this > min && this < max;
};
function validTime(str) {
    const parts = str.split(':')
   
    return parseInt(parts[0]).between(0,24) && parseInt(parts[1]).between(0,59);
}

Day 11: Avoid Obstacles

function avoidObstacles(nums) {
    const sorted = nums.sort();
        
    for(num=2; num <= sorted[nums.length-1]; num++) {
        match = nums.filter((val) => (val
        if (match.length === 0) {
            return num;
        }
    }
}

Day 10: Adjacent Elements Product

function adjacentElementsProduct(nums) {
    let result = 0;
    
    for (i1 = 0; i1 < nums.length-1; ++i1) {
        const product=nums[i1]*nums[i1+1];
                 
        if (product > result) {
            result = product;
        }
    }
    
    return result;
}

Day 09: Sum Odd Fibonacci Numbers

function sumOddFibonacciNumbers(num) {
    let fibPrev=1;
    let fib=0;
    let sum=0;
    
    while (fibPrev < num) {       
        [ fibPrev, fib ] = [fib + fibPrev, fibPrev];
        
        if (fib
            sum += fib;
        }
    }
    
    return sum;
}

Day 08: Rolling Dice

<html>
    <head>
        <link rel="stylesheet" href="style.css">
        <script src="index.pack.js"></script>
    </head>
    <body>
        <div class="dice">
            <div class="dot dot_ dot2 dot_ dot4 dot5 dot6"></div>
            <div class="dot dot_ dot_ dot_ dot_ dot_ dot_"></div>
            <div class="dot dot_ dot_ dot3 dot4 dot5 dot6"></div>
            
            <div class="dot dot_ dot_ dot_ dot_ dot_ dot6"></div>
            <div class="dot dot1 dot_ dot3 dot_ dot5 dot_"></div>
            <div class="dot                          dot6"></div>
            
            <div class="dot dot_ dot_ dot3 dot4 dot5 dot6"></div>
            <div class="dot"></div>
            <div class="dot dot_ dot2 dot_ dot4 dot5 dot6"></div>
        </div>
        
    </body>
</html>
function clear_dice() {
    document.querySelectorAll('.dot').forEach( (dot) => dot.style.visibility = "hidden")
}
function roll_dice() {
    const roll = 1 + Math.floor(Math.random() * 6);
    
    console.log(roll);
    clear_dice()
    document.querySelectorAll('.dot'+roll).forEach( (dot) => {
        dot.style.visibility = "visible"
    })
}
clear_dice()
document.getElementsByClassName('dice')[0].addEventListener('click', roll_dice)
body {
    background-color: #AEB8FE;
    display: flex;
}
.dice {
    width: 230px;
    height: 230px;
    border-radius: 10px;
    background-color: #EFE5DC;
    margin: 100px;
    
    display: grid;
    grid-template-columns: repeat(3, 40px);
    gap: 20px;
    grid-auto-rows: minmax(40px, 40px);
}
.dot {
    width: 40px;
    height: 40px;
    border-radius: 15px;
    background-color: black;
    margin: 35px;
    
    visibility: hidden
}
.dot1 { visibility: none; }
.dot2 { visibility: none; }
.dot3 { visibility: none; }
.dot4 { visibility: none; }
.dot5 { visibility: none; }
.dot6 { visibility: none; }

Day 07: Count Vowel Consonant

function countVowelConsonant(str) {
  function reducer(s,c) {
      return s + ("aeiou".includes(c) ? 1 : 2)
  };
    
  return str.split("").reduce(reducer, 0)
}

Day 06: Sort By Length

function sortByLength(strs) {    
    return strs.sort(function(arg1, arg2) { return arg1.length - arg2.length })
}

Day 05: Reverse String

function reverseAString(str) {   
    return str.split("").reverse().join("")
}

Day 04: Century From Year

function centuryFromYear(num) {
    return Math.floor((num + 99) / 100)
}

Day 03: Chunky Monkey

function chunkyMonkey(values, size) {
    result = [];
    pos = 0;
    
    while (pos < values.length) {
        result.push(values.slice(pos, pos+size));
        pos += size;
    }
    
    return result;
}

Day 02: Deposit Profit

function depositProfit(deposit, rate, threshold) {
    let profit = deposit;
    let years = 0
    while (profit < threshold) {
        profit += profit * 0.01 * rate;
        years++;
    }
    
    return years;
}

Day 01: Candies

https://scrimba.com/learn/adventcalendar/note-at-0-00-coef0430d83cd2a72113bbf09

function candies(children, candy) {
    return children * Math.floor(candy / children)//  write code here.
}
rxjs

rxjs | Cookbook

Informations

NGRX – Reactive State for Angular

Original Post is here. Github repo has the code snippets at choopage’s GitHub repo.

Recipes


Chaining of observable

The below snippet would return 0, 1, 2, 3…, n until it is stopped. It would be returned at every 2 sec. See reference here.

import { Observable } <strong>from 'rxjs/Rx';

let obs = Observable.<em>interval</em>(2000);
let req = obs
    .flatMap(v => { return Observable.of(v) })
    .subscribe(
        v   => console.log(v),
        err => console.error(err),
        ()  => console.log(<strong>'done'</strong>)
    );

Response transformer

The below would return undefined at every 2 sec interval.

import { Observable } from 'rxjs/Rx';

let obs = Observable.interval(2000);

obs
    .let(responseTransformer)
    .subscribe(
        v  => console.log(v),
        err=> console.error(err)
    );

responseTransformer(input: Observable<any>) {
    return input.map(v => v.data);
}

Using RxJS composition over base class extension

export class MyAPI {
    constructor(private http: Http) {
    }

    get(url: string, options: any) {
        return this.http.get(url, options).let(responseTransformer);
    }
}

responseTransformer(input: Observable<any>) {
    return input.map(v => v.data);
}

Higher Order Observable

We create higher order observable using .map.

const numObservable = Rx.Observable.interval(1000).take(4);
const higherOrderObservable = numObservable.map(x => Rx.Observable.of(1, 2));

higherOrderObservable.subscribe(<br>x => x.subscribe(y => console.log(y)));

Further use of higher order observable

usingHigherOrderObservable() {
    Observable
        .interval(1000)
        .groupBy(n => n
}

Flatten a higher order observable with RxJS switch

const numObservable = Rx.Observable.interval(1000).take(2);
const higherOrderObservable = numObservable.map(x => Rx.Observable.of(1, 2)).switch();

/* 
------+--------+---------
       \        \
        1,2      1,2
        
        switch
        
------1-2-------1-2-------

Switch map flattens the higher order observable
*/

higherOrderObservable.subscribe(x => console.log(x));

switchMap: map and flatten higher order observables

const clickObservable = Rx.Observable.fromEvent(document, 'click');

function performRequest() {
  return fetch('<a href="http://jsonplaceholder.typicode.com/users/1'" target="_blank" rel="noreferrer noopener">http://jsonplaceholder.typicode.com/users/1'</a>)
  .then(res =>; res.json());
  //this returns a Promise
}
//Observabl<Event> --> Observable<Response><br>const responseObservable = clickObservable<br>  .switchMap(click => performRequest());//switchMap can convert Promise to Observable<br>//switchMap = map .... + ... switchresponseObservable.subscribe(x => console.log(x.email));

Use groupBy in real RxJS applications

See reference here.

const busObservable = Rx.Observable.of(
  {code: 'en-us', value: '-TEST-'},
  {code: 'en-us', value: 'hello'},
  {code: 'es', value: '-TEST-'},
  {code: 'en-us', value: 'amazing'},
  {code: 'pt-br', value: '-TEST-'},
  {code: 'pt-br', value: 'olá'},
  {code: 'es', value: 'hola'},
  {code: 'es', value: 'mundo'},
  {code: 'en-us', value: 'world'},
  {code: 'pt-br', value: 'mundo'},
  {code: 'es', value: 'asombroso'},
  {code: 'pt-br', value: 'maravilhoso'}
).concatMap(x => Rx.Observable.of(x).delay(500));const all = busObservable
  .groupBy(obj => obj.code);
  .mergeMap(innerObs => innerObs.skip(1).map(obj => obj.value));//Alternatively could using filter and map
/*
const enUS = busObservable
  .filter(obj => obj.code === 'en-us')
  .map(obj => obj.value);const es = busObservable
  .filter(obj => obj.code === 'es')
  .map(obj => obj.value);const all = Rx.Observable.merge(enUS, es);
*/all.subscribe(x => console.log(x));

Using .map versus .switchMap

The below code snippet we can view the result of using .map versus .switchMap

//user.service.ts
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from "rxjs";
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

@Injectable()
export class UserService {

  constructor(private http: Http) { }

  getUsers(): Observable<any> {
    return this.http.get('http://jsonplaceholder.typicode.com/users')
        //.map(v => v.json());
        .switchMap(v => v.json());
  }

}
//app.component.ts
import { Component } from '@angular/core';
import { UserService } from "./user.service";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app works!';

  constructor(private userService: UserService) {

  }

  search(term: string) {
    this.userService.getUsers()
        .subscribe(v => console.log(v));

    /*
    //we can do this with .switchMap
    this.userService.getUsers()
        .subscribe(v => {if (v.email != "Sincere@april.biz") {
          console.log(v.email);
        }});
     */
  }
}

Solving the multiple Async Pipe in Angular ≥ 2.0.0 with share operator

Remember to import import "rxjs/add/operator/share"; See reference here.

squareData$: Observable<string> = Observable.range(0, 10)
        .map(x => x * x)
        .do(x => console.log(`CalculationResult: ${x}`)
        .toArray()
        .map(squares => squares.join(", "))
        .share();  // remove this line: console will log every result 3 times instead of 1

Managing Cold and Hot Observables using publish().refCount() which is similar to .share()

ngOnInit() {
    // in angular 2 and above component.ts file add these    this.coldObservable();
    this.hotObservable();
}
/*
* cold observable is like a recast of video
* */
coldObservable() {
    let incrementalObs = Observable.interval(1000).take(10).map(x => x + 1);
    incrementalObs.subscribe(val => console.log('a: ' + val));
    setTimeout(function() {
        incrementalObs.subscribe(val => console.log('      b: ' + val));
    }, 4500);
}


/*
* hot observable is like watching a live video
* */
hotObservable() {
    let incrementalObs = Observable.interval(1000).take(10).map(x => x + 1).publish().refCount(); //can also use .share()
    incrementalObs.subscribe(val => console.log('a: ' + val));
    setTimeout(function() {
        incrementalObs.subscribe(val => console.log('      b: ' + val));
    }, 4500);
}

Observables Array Operations with flatMap

Reference Rangle.io article.

getLoadList(): void {
  this.shareService
      .fetchLoad()
      .take(1)
      .filter(response => {
        if ( response.status === 200 ) {
          return true;
        } else if ( response.status === 304 ) {
          // do something more
          return false;
        } else {
          this.gotoErrorPage();
          return false;
        }
      })
      .flatMap(response => response.data.loads as Load[])
      .filter(obj => obj.content.contentGrade === 'x')
      .subscribe(
          val => console.log(val),
          err => {
            console.error(err);
          });
}

Error Handling


Error handling in RxJS

Some learning points from RxJS lesson videos. This repo is available in my GitHub repo.

import { Component, OnInit } from '@angular/core';

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from "rxjs/BehaviorSubject";

import 'rxjs/add/observable/bindCallback';
import 'rxjs/add/observable/bindNodeCallback';
import 'rxjs/add/observable/combineLatest';
import 'rxjs/add/observable/concat';
import 'rxjs/add/observable/defer';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/forkJoin';
import 'rxjs/add/observable/from';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/observable/fromEventPattern';
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/observable/generate';
import 'rxjs/add/observable/if';
import 'rxjs/add/observable/interval';
import 'rxjs/add/observable/merge';
import 'rxjs/add/observable/race';
import 'rxjs/add/observable/never';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/onErrorResumeNext';
import 'rxjs/add/observable/pairs';
import 'rxjs/add/observable/range';
import 'rxjs/add/observable/using';
import 'rxjs/add/observable/throw';
import 'rxjs/add/observable/timer';
import 'rxjs/add/observable/zip';
import 'rxjs/add/observable/dom/ajax';
import 'rxjs/add/observable/dom/webSocket';
import 'rxjs/add/operator/buffer';
import 'rxjs/add/operator/bufferCount';
import 'rxjs/add/operator/bufferTime';
import 'rxjs/add/operator/bufferToggle';
import 'rxjs/add/operator/bufferWhen';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/combineAll';
import 'rxjs/add/operator/combineLatest';
import 'rxjs/add/operator/concat';
import 'rxjs/add/operator/concatAll';
import 'rxjs/add/operator/concatMap';
import 'rxjs/add/operator/concatMapTo';
import 'rxjs/add/operator/count';
import 'rxjs/add/operator/dematerialize';
import 'rxjs/add/operator/debounce';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/defaultIfEmpty';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/delayWhen';
import 'rxjs/add/operator/distinct';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/distinctUntilKeyChanged';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/exhaust';
import 'rxjs/add/operator/exhaustMap';
import 'rxjs/add/operator/expand';
import 'rxjs/add/operator/elementAt';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/finally';
import 'rxjs/add/operator/find';
import 'rxjs/add/operator/findIndex';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/groupBy';
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/isEmpty';
import 'rxjs/add/operator/audit';
import 'rxjs/add/operator/auditTime';
import 'rxjs/add/operator/last';
import 'rxjs/add/operator/let';
import 'rxjs/add/operator/every';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mapTo';
import 'rxjs/add/operator/materialize';
import 'rxjs/add/operator/max';
import 'rxjs/add/operator/merge';
import 'rxjs/add/operator/mergeAll';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/mergeMapTo';
import 'rxjs/add/operator/mergeScan';
import 'rxjs/add/operator/min';
import 'rxjs/add/operator/multicast';
import 'rxjs/add/operator/observeOn';
import 'rxjs/add/operator/onErrorResumeNext';
import 'rxjs/add/operator/pairwise';
import 'rxjs/add/operator/partition';
import 'rxjs/add/operator/pluck';
import 'rxjs/add/operator/publish';
import 'rxjs/add/operator/publishBehavior';
import 'rxjs/add/operator/publishReplay';
import 'rxjs/add/operator/publishLast';
import 'rxjs/add/operator/race';
import 'rxjs/add/operator/reduce';
import 'rxjs/add/operator/repeat';
import 'rxjs/add/operator/repeatWhen';
import 'rxjs/add/operator/retry';
import 'rxjs/add/operator/retryWhen';
import 'rxjs/add/operator/sample';
import 'rxjs/add/operator/sampleTime';
import 'rxjs/add/operator/scan';
import 'rxjs/add/operator/sequenceEqual';
import 'rxjs/add/operator/share';
import 'rxjs/add/operator/single';
import 'rxjs/add/operator/skip';
import 'rxjs/add/operator/skipUntil';
import 'rxjs/add/operator/skipWhile';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/subscribeOn';
import 'rxjs/add/operator/switch';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/switchMapTo';
import 'rxjs/add/operator/take';
import 'rxjs/add/operator/takeLast';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/operator/takeWhile';
import 'rxjs/add/operator/throttle';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/operator/timeInterval';
import 'rxjs/add/operator/timeout';
import 'rxjs/add/operator/timeoutWith';
import 'rxjs/add/operator/timestamp';
import 'rxjs/add/operator/toArray';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/window';
import 'rxjs/add/operator/windowCount';
import 'rxjs/add/operator/windowTime';
import 'rxjs/add/operator/windowToggle';
import 'rxjs/add/operator/windowWhen';
import 'rxjs/add/operator/withLatestFrom';
import 'rxjs/add/operator/zip';
import 'rxjs/add/operator/zipAll';
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
    title = 'app works!';
    obs = Observable.of(1, 2, 3, 4);

    ngOnInit() {
        this.howToHandleErrorV1();
        this.howToHandleErrorV2();
        this.howToUseRetry();
        this.mergeObservableAndThrowError();
        this.mergeObservableAndErrorResumeNext();
        this.mergeObservableAndErrorCatch();    
    }

    
    
    /*
     * This uses Catch for V1. This introduces Closure. It is effectively the same as V2.
     * */
    howToHandleErrorV1() {
        this.obs
            .map(x => {
                if ( x === 3 ) {
                    throw 'I hate threes'; // When it hitted error it actually unsubscribe itself at x === 3 of throw error
                }
                return x;
            })
            .catch(err => Observable.throw('Caught error here Observable.throw')) // continue go down the error path use Observable.throw
            .catch(err => Observable.of('Caught error here Observable.of')) // catch just use Observable.of
            .subscribe(
                x => console.log(x),
                err => console.error(err), // If not catch any where, the I hate threes errors will be propagated to here
                () => console.log('done completed')
            );
    }

    
    
    /*
     * There is a difference between V1 and V2. For V2 it is using onErrorResumeNext which
     * */
    howToHandleErrorV2() {
        let good = Observable.of('Caught error here Observable.of');

        this.obs
            .map(x => {
                if ( x === 3 ) {
                    throw 'I hate threes'; // When it hit error it actually unsubscribe itself at x === 3 of throw error
                }
                return x;
            })
            .onErrorResumeNext(good) // To catch just use Observable.of
            .subscribe(
                x => console.log(x),
                err => console.error(err), // If not catch any where, the I hate threes errors will be propagated to here
                () => console.log('done completed')
            );
    }

    
    
    /*
     * For this we use see it retries three times then console.error(err);
     * So retryWhen is for trying network connection websocket
     * */
    howToUseRetry() {
        this.obs
            .map(x => {
                if ( x === 3 ) {
                    throw 'I hate threes'; // When it hitted error it actually unsubscribe itself at x === 3 of throw error
                }
                return x;
            })
            .retry(3) // retry three times
            .retryWhen(err => err.delay(2000).take(3)) // similar but with 2 seconds delay and the error is not propagated.
            .retryWhen(err => err.delay(2000).take(3).concat(Observable.throw('bad'))) // this it would throw an error.
            .subscribe(
                x => console.log(x),
                err => console.error(err), // If not catch any where, the I hate threes errors will be propagated to here
                () => console.log('done completed')
            );
    }
/*
 * Using observable merge operator
 * */
mergeObservableAndThrowError() {
    let mergedObs = Observable.merge(
        this.obs, //1, 2, 3, 4
        Observable.throw('Stop Error'),
        Observable.from(this.array), //0, 1, 2, 3, 4, 5
        Observable.of(999) //999,
    );

    mergedObs.subscribe(
        val => console.log(val), //this should show 1, 2, 3, 4, Stop Error
        error => console.log(error),
        () => console.log("completed")
    );
}

/* Using observable onErrorResumeNext just like merge operator
 * */
mergeObservableAndErrorResumeNext() {
    let mergedObs = Observable.onErrorResumeNext(
        this.obs, //1, 2, 3, 4
        Observable.throw('Stop Error'),
        Observable.from(this.array), //0, 1, 2, 3, 4, 5
        Observable.of(999) //999,
    );

    mergedObs.subscribe(
        val => console.log(val), //this should show 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 999
        error => console.log(error),
        () => console.log("completed")
    );
}
/*
 * Using observable merge operator and catch
 * */
mergeObservableAndErrorCatch() {
    let mergedObs = Observable.merge(
        this.obs, //1, 2, 3, 4
        Observable.throw('Stop Error'),
        Observable.from(this.array), //0, 1, 2, 3, 4, 5
        Observable.of(999) //999,
    ).catch(e => {
        console.log(e);
        return Observable.of('catch error here');
    });

    mergedObs.subscribe(
        val => console.log(val), //this should show 1, 2, 3, 4, Stop Error, Catch Error Here
        error => console.log(error),
        () => console.log("completed")
    );
}
}

map vs flatMap in RxJS

Transform the items emitted by an Observable into Observables, then flatten the emissions from those into a single Observable. See my GitHub repo.

obs = Observable.of(1, 2, 3, 4);

ngOnInit() {
    this.usingMap();
    this.usingMapToMakeInnerObservable();
    this.usingMapAndMergeAll();
    this.usingFlatMap();
}usingMap() {
    this.obs
        .map(x => x * 2) // transform the input by multiple of 2
        .subscribe(
            x => console.log(x),
            err => console.error(err),
            () => console.log('done completed')
        );
}
usingMapToMakeInnerObservable() {
    this.obs
        .map(x => Observable.timer(500).map(() => x + 3)) // transform the input wrapping it with another observable and addition of 3
        //.map(x => Observable.timer(500).map((x) => x + 3)) // !!! REMEMBER Not the same as the immediate above
        .subscribe(
            x => console.log(x),
            err => console.error(err),
            () => console.log('done completed')
        );
}
// Map and Merge all is the same as just one FlatMap
usingMapAndMergeAll() {
    this.obs
        .map(x => Observable.timer(500).map(() => x + 3)) // transform the input wrapping it with another observable and addition of 3
        .mergeAll()
        .subscribe(
            x => console.log(x),
            err => console.error(err),
            () => console.log('done completed')
        );
}
// Flat map is the same as map then merge all
// transform the items emitted by an Observable into Observables, then flatten the emissions from those into a single Observable
usingFlatMap() {
    this.obs
        .flatMap(x => Observable.timer(500).map(() => x + 10)) // transform the input wrapping it with another observable and addition of 10
        .subscribe(
            x => console.log(x),
            err => console.error(err),
            () => console.log('done completed')
        );
}

Transforming pure Javascript array vs. Observable from array

See GitHub for source code

array = [0, 1, 2, 3, 4, 5];

ngOnInit() {
    this.setArrayToObservableThenTransform();
}/*
* This keeps creating new array. It is good that it creates new array of arr for immutability.
* But it's bad because there is clean up and resource intensive for mobile
* */
transformArray() {
    let result = this.array
        .filter(( x, i, arr ) => {
            console.log('filtering ' + x);
            console.log('is the source array ' + (arr === this.array));
            return x
        })
        .map(( x, i, arr ) => {
            console.log('mapping ' + x);
            console.log('is the source array ' + (arr === this.array));
            return x + '!';
        })
        .reduce(( r, x, i, arr ) => {
            console.log('reducing ' + x);
            return r + x;
        }, '--');

    console.log(result);
}

/*
* This is more efficient for resource management because it linearly scans and discard when not right
* */
setArrayToObservableThenTransform() {    let obsArray = Observable.from(this.array); // Use Observable.from() instead of Observable.of(). There is diff.    obsArray
        .filter(( x: any ) => {
            console.log('filtering ' + x);
            return x
        })
        .map(( x ) => {
            console.log('mapping ' + x);
            return x + '!';
        })
        .reduce(( r, x ) => {
            console.log('reducing ' + x);
            return r + x;
        }, '--')
        .subscribe(
            x => console.log(x)
        );
}

Using reduce and scan to aggregate RxJs data

See GitHub for source code

array = [0, 1, 2, 3, 4, 5];ngOnInit() {
    this.reduceArray();
    this.reduceObservableArray();
    this.reduceObservableArray_Abstract2();
    this.scanObservableArray();
}
/*
* This is the same as reduceObservableArray()
* */
reduceArray() {
    let result = this.array.reduce(
        (accumulator, currentValue) => accumulator + currentValue, 3
    ); // 3 is the init value.
    console.log('reduceArray ' + result); // output 18 => 3 + (0 ... 5)
}
/*
* This is the same as reduceArray()
* But this waits for all the arrays to finish emitting before reducing them to one single number
* See the next method to understand better
* */
reduceObservableArray() {
    let obsArray = Observable.from(this.array);
    obsArray.reduce(
        (accumulator, currentValue) => accumulator + currentValue, 3
    ).subscribe(
        val => console.log('reduceObservableArray ' + val)
    );
}
/*
* The exact same reduce function/method as of reduceObserableArray() above
* This proves that it waits for all 6 numbers to come in then reduce them
* */
reduceObservableArray_Abstract2() {
    let obsArray = Observable.interval(1000).take(6); //emits 6 times of 0, 1, 2, 3, 4, 5
    obsArray.reduce(
        (accumulator, currentValue) => accumulator + currentValue, 3
    ).subscribe(
        val => console.log('reduceObservableArray_Abstract2 ' + val)
    );
}
/*
* This is the same as the above reduceObserableArray_Abstract2()
* except this is using scan instead of reduce
* */
scanObservableArray() {
    let obsArray = Observable.interval(1000).take(6); //emits 6 times of 0, 1, 2, 3, 4, 5
    obsArray.scan(
        (accumulator, currentValue) => accumulator + currentValue, 3
    ).subscribe(
        val => console.log('scanObservableArray() ' + val)
    );
}

Create, next, and subscribe to Subject and BehaviorSubject

There is a Stack Overflow thread which discussed about the difference between Subject and BehaviorSubject. It’s worth understanding.

Also see my personal GitHub for source code.

app.component.ts

import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from "rxjs/BehaviorSubject";
// create subject
// there is no need for initial value
subject = new Subject<boolean>();

// create behaviorSubject which require initial value
// true is an initial value. if there is a subscription
// after this, it would get true value immediately
behaviorSubject = new BehaviorSubject<boolean>(true);
ngOnInit() {
    this.subject.next(false); /* Subject subscription wont get anything at this point before the subscribeSubject() */    this.subscribeSubject();
    this.subscribeBehaviorSubject();
}
/*
* Push the next val into the behavior subject
* */
nextSubject(val: boolean) {
    this.subject.next(val);
}

/*
* Any values push into the subject would not be can shown
* before this subscribeSubject() is called
* */
subscribeSubject() {
    this.subject
        //.take(1) //when we include .take(1) we will have a complete. Without this it will continue subscribing
        .subscribe(
            val => console.log(val),
            err => console.error(err),
            () => console.log('completed')
        );
}

/*
* This is the proper way to return a subject as observable
* */
getSubject(): Observable<boolean> {
    return this.subject.asObservable();
}

/*
 * Push the next val into the behavior subject
 * */
nextBehaviorSubject(val: boolean) {
    this.behaviorSubject.next(val);
}

/*
* For angular Behavior subject for a data service as a angular service often initializes
* before component and behavior subject ensures that the component consuming the
* service receives the last updated data even if there are no new
* updates since the component's subscription to this data.
* */
subscribeBehaviorSubject() {
    this.behaviorSubject
        // .first()
        .subscribe(
            val => console.log(val),
            err => console.error(err),
            () => console.log('completed')
        );
}

app.component.html

Subject:
<button (click)="nextSubject(true)">true</button>
<button (click)="nextSubject(false)">false</button>

<br>
BehaviorSubject:
<button (click)="nextBehaviorSubject(true)">true</button>
<button (click)="nextBehaviorSubject(false)">false</button>

Finally operator

usingFinallyOperator() {
    Observable
        .interval(500)
        .take(4)
        .finally(() => console.log('End of the observable, Hello World'))
        .subscribe(
            val => console.log('count taker ' + val)
        );
}

Stopping / Intercepting Observable

Imagine using Gmail where it allows you to undo email sent? We can produce similar experience with Observable

// subscription is created when an observable is being subscribed
subscription: Subscription;

// boolean variable for showing stop observable using takeWhile operator
isTrue: boolean = true;
/*
* basic interval can be used as delay too
* Imagine Gmail allows you to send and undo send within 4 seconds of sending
* Use Case: Perform an action 8 seconds later then intercept if user choose to undo the action
* */basicInterval() {
    let undoInSeconds: number = 8;
    this.subscription = Observable
            .interval(1000)
            .take(undoInSeconds)
            .takeWhile(() => this.isTrue)
            .subscribe(
                (val: number) => {
                    console.log(`${val + 1} seconds...         UNDO`);
                    ( val === (undoInSeconds - 1) ) ? console.log('Email sent / Action performed') : null;
                }
            );
}
/*
* This is to stop observable from continuing performance
* Use Case: Stop observable from running like how Gmail could undo email being sent
* */stopObservableUsingUnsubscribe() {
    if (!!this.subscription) {
        this.subscription.unsubscribe();
        console.log('subscription: Subscription is unsubscribed');
    }
}

/*
* This is also to stop observable from continuing performance
* This method is more preferable than subscribing method then unsubscribe
 * Use Case: Stop observable from running like how Gmail could undo email being sent
* */stopObservableUsingTakeWhile() {
    this.isTrue = false;
}

Perform conditional Reactive Form validation

This is my approach to performing conditional validation when using Angular. We will minimally manipulate the Observable of RxJS in this example. Let’s try by creating or using app.component.ts.

1 . Create a form that has two form controls reason and otherReason.

/* * Refer to angular official guide at https://angular.io/guide/reactive-forms on how to create reactive form with form controls * */createForm() {
    this.form = this.formBuilder.group({      reason: ['', Validators.required ],
      otherReason: [''],    });
}

2 . Create two methods addValidator and removeValidator.

/*  * For conditional form validation use  * */
private addValidator( control: AbstractControl, newValidator ){
    let existingValidators = control.validator;
    control.setValidators(Validators.compose([ existingValidators, newValidator ]));
    control.updateValueAndValidity();
}

/*  * For conditional form validation use  * */
private removeValidator( control: AbstractControl ){
    control.clearValidators();
    control.updateValueAndValidity();
}

3 . Create third method called conditionalFormValidation.

conditionalFormValidation( parentField, childField, matchValue = 'Others' )
{
    this.form
        .get(parentField)
        .valueChanges
        .forEach(( value: string ) => {
            const childFieldControl: AbstractControl = this.form.get(childField);
            if ( value === matchValue ) {
                this.addValidator(childFieldControl, Validators.required);
            } else {
                this.removeValidator(childFieldControl);
            }
        });
}

4 . Create or add to ngOnInit method

ngOnInit() {    
    this.createForm();
    this.conditionalFormValidation('reason', 'otherReason');
}

The outcome should illustrate that if we select ‘Others’ option in the dropdown list of reason, it should make otherReason form control field as required.

Drop-down selection for reason e.g. reason for absent from work
On selecting Others, the conditional form validation would make otherReason field turn into a required field

Perform manual operations after reading from Firebase database

When using AngularFire2 with Angular + Firebase, in getting a list of data from Firebase, we will get one observable instance but within that one observable is an array of N size. We can manually filter the array inside that one observable instance using arr.filter. It is different from RxJS .filter operator. Of course we can also flatten what is inside an array using .flatMap() operator. However, we’re going use JavaScript array filtering, instead of non-observable filtering, right after getting an observable object.

this.db.list(`/review`)
       .map(arr => arr.filter(item => item.rating > 3));

We can also reverse an array using JavaScript array reverse function. See reference. On a side note, using negative timestamp to reverse Firebase display is also another option.

this.db.list('/review')
       .map(arr => { return arr.reverse(); });

Besides those above, we can also use the response returned from AngularFire2 to perform “local filter/search”. This result can be valuable for autocomplete filtered list or searches. However, this approach below suffers severely in performance issue where at the magnitude of the size of the returned response from AngularFire2. E.g. if the list has N items. It has to iterate at least 1N. Perhaps an average of 2N.

getFilteredClientList(searchQuery: string): Observable<Client[]> {
    let query = searchQuery.trim().toLowerCase();

    return this.db.list(`/client`)
      .map((arr: Client[]) => {
        return arr.filter((item: Client) => {
          return item.email.toLowerCase().indexOf(query) === 0
            || item.given_name.toLowerCase().indexOf(query) === 0
            || item.family_name.toLowerCase().indexOf(query) === 0
        });
      });
  }

JavaScript: Guide to the JavaScript Ecosystem

Beginning – Libraries and Frameworks

UI

  • Angular
  • Ionic
  • Onson UI
  • Framework 7

Unit Testing

  • Karma
  • Jasmine

End-to-End Testing

Technologies

Unit Testing

Protractor (http://www.protractortest.org/)

Use npm to install Protractor globally with

npm install -g protractor

The webdriver-manager is a helper tool to easily get an instance of a Selenium Server running. Use it to download the necessary binaries with:

webdriver-manager update

Now start up a server with:

webdriver-manager start

Install a demo repository:

git clone https://github.com/juliemr/protractor-demo.git
npm install

Update selenium driver

node_modules/protractor/bin/webdriver-manager update

Run server in background:

node app/expressserver.js &

Now run the tests

npm test 

Error message: No selenium server jar found at the specified location

The Error

If you got this error when testing your javascript app, maybe you hate to update the selenium driver jar.

The Solution

Update the driver depending on the installation of the node module:

Check with

$ which protracto
/usr/local/bin/protractor

If installed global, then use the command

$ /usr/local/lib/node_modules/protractor/bin/webdriver-manager update

If installed in the app directory, use

$ node_modules/protractor/bin/webdriver-manager update
[17:17:13] I/update - chromedriver: file exists node_modules/protractor/node_modules/webdriver-manager/selenium/chromedriver_2.22mac32.zip
[17:17:13] I/update - chromedriver: unzipping chromedriver_2.22mac32.zip
[17:17:14] I/update - chromedriver: setting permissions to 0755 for node_modules/protractor/node_modules/webdriver-manager/selenium/chromedriver_2.22
[17:17:14] I/update - chromedriver: v2.22 up to date
[17:17:14] I/update - selenium standalone: file exists node_modules/protractor/node_modules/webdriver-manager/selenium/selenium-server-standalone-2.53.1.jar
[17:17:14] I/update - selenium standalone: v2.53.1 up to date