Observables With Angular and Feathers

Feathers is a modern API framework for Node.js. It exposes its backend services as a REST API or as a WebSocket API. To consume the exposed WebSockets from an Angular app, it makes sense to create Angular services to abstract the respective Feathers services in a way that makes it easy for our Angular components to consume them. This tutorial is assuming that you are using the Angular CLI for your app.

In my example, I’ll use a simple to-do service and a component that lists those to-dos.

Import libraries

As a first step, we’ll need to add the two libraries socket.io-client and feathers-client to our project. This can easily be done using npm:

$ npm install --save socket.io-client feathers-client

The TypeScript typings of feathers-client are already included in the library. However, we need to include the ones for socket.io-client manually:

$ npm install --save @types/socket.io-client

Create base API service

Then we create an abstract class to extend upon, which contains the basic properties of a backend service:

// src/app/api.service.ts

export abstract class ApiService {
  private _url: string = "https://my-todos-api.com";

  get url(): string {
    return this._url;
  }
}

Create data model

Next, we create a class to represent our to-do’s data model:

// src/app/todos/todo.ts

export class Todo {
  title: string;
}

Create actual service

Then we create the actual service which connects to the Feathers backend and exposes it as a service in Angular. It inherits from the base service we’ve created above. The Feathers service is exposed as an RxJS Obersvable which our components can then subscribe to:

// src/app/todos/todo.service.ts

import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { Observer } from "rxjs/Observer";
import * as io from "socket.io-client";
import feathers from "feathers-client";

import { ApiService } from "../api.service";
import { Todo } from "./todo";

@Injectable()
export class TodoService extends APIService {
  public todos$: Observable<Todo[]>;
  private todosObserver: Observer<Todo[]>;
  private feathersService: any;
  private dataStore: {
    todos: Todo[];
  };

  constructor() {
    super();

    const socket = io(this.url);
    const client = feathers().configure(feathers.socketio(socket));
    this.feathersService = client.service("todo");

    this.feathersService.on("created", (todo) => this.onCreated(todo));
    this.feathersService.on("updated", (todo) => this.onUpdated(todo));
    this.feathersService.on("removed", (todo) => this.onRemoved(todo));

    this.todos$ = new Observable((observer) => (this.todosObserver = observer));

    this.dataStore = { todos: [] };
  }

  public find() {
    this.feathersService.find((err, todos: Todo[]) => {
      if (err) return console.error(err);

      this.dataStore.todos = todos;
      this.todosObserver.next(this.dataStore.todos);
    });
  }

  private getIndex(id: string): number {
    let foundIndex = -1;

    for (let i = 0; i < this.dataStore.todos.length; i++) {
      if (this.dataStore.todos[i].id === id) {
        foundIndex = i;
      }
    }

    return foundIndex;
  }

  private onCreated(todo: Todo) {
    this.dataStore.todos.push(todo);

    this.todosObserver.next(this.dataStore.todos);
  }

  private onUpdated(todo: Todo) {
    const index = this.getIndex(todo.id);

    this.dataStore.todos[index] = todo;

    this.todosObserver.next(this.dataStore.todos);
  }

  private onRemoved(todo) {
    const index = this.getIndex(todo.id);

    this.dataStore.todos.splice(index, 1);

    this.todosObserver.next(this.dataStore.todos);
  }
}

Consume from component

Now, our Angular service is ready. To use the Observable it exposes in an Angular component, follow the structure below:

// todos/todos.component.ts

import {
  ChangeDetectorRef,
  ChangeDetectionStrategy,
  Component,
  OnDestroy,
  OnInit,
} from "@angular/core";
import { Subscription } from "rxjs/Subscription";

import { TodoService } from "./todo.service";
import { Todo } from "./todo";

@Component({
  selector: "app-todos",
  providers: [TodoService],
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: "./todos.component.html",
})
export class TodosComponent implements OnDestroy, OnInit {
  private todos: Todo[] = [];
  private subscription: Subscription;

  constructor(
    private todoService: TodoService,
    private ref: ChangeDetectorRef,
  ) {}

  public ngOnInit(): void {
    this.subscription = this.todoService.todos$.subscribe(
      (todos: Todo[]) => {
        this.todos = todos;
        this.ref.markForCheck();
      },
      (err) => {
        console.error(err);
      },
    );
    this.todoService.find();
  }

  public ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

Our component TodosComponent now has a property todos which contains the to-dos it gets from the respective Feathers service and which can be used in the app. It live-updates the UI every time a to-do in the Feathers API is removed or added.