A stateful TaskService for most scenarios

So far, we have covered how to inject an HTTP service into a service constructor and have been able to subscribe to such a service from a component. In some cases, a component might want to deal with the data directly and not with Observables. In fact, most of our cases are like that. So, we don't have to use Observables much; HTTP services are utilizing Observables, right? We are talking about the component layer. Currently, we have this happening inside of our component:

// app/tasks/task.service.ts

@Component({
template: `
<div *ngFor="let task of tasks$ | async">
{{ task.name }}
</div>
`
})
export class TaskListComponent {
tasks$:Observable<Task[]>;

constructor(private taskService: TaskService ) {}

ngOnInit() {
this.tasks$ = this.taskService.getTasks();
}
}

Here, we see that we assign taskService.getTasks() to a stream called tasks$. What is the $ at the end of the tasks$ variable? This is a naming convention that we use for streams; let's try to follow that for any future streams/Observable fields that we declare. We use the words Observable and stream interchangeably, as they come to mean the same thing in the context of Angular. We also let the | async async pipe handle it with *ngFor and display our tasks.

We can do this in an even simpler way, like so:

// app/tasks/tas.alt.component.ts

@Component({
template: `
<div *ngFor="let task of tasks">
{{ task.name }}
</div>
`
})
export class TaskComponent {
constructor(private taskService: TaskService ) {}

get tasks() {
return this.taskService.tasks;
}
}

So, the following changes took place:

  • ngOnInit() and the assigning to the tasks$ stream was removed
  • The async pipe was removed
  • We replaced the tasks$ stream with a tasks array

How can this still work? The answer lies in how we define our service. Our service needs to expose an array of items and we need to ensure the array is changed when we get some data back from HTTP, or when we receive data from elsewhere, such as from a web socket or a product like Firebase.

We just mentioned two interesting methods, sockets and Firebase. Let's explain what those are and how they relate to our service. A web socket is a technique that establishes a two-way communication, a so-called full duplex connection, with the help of the TCP protocol. So, why is it interesting to mention it in the context of HTTP? Most of the time, you have simple scenarios where you fetch data over HTTP, and you can leverage Angular's HTTP service. Sometimes, the data might come from a full duplex connection in addition to it coming from HTTP.

What about Firebase? Firebase is a product by Google that allows us to create a database in the cloud. As can be expected, we can perform CRUD operations on the database, but the strength lies in the fact that we can set up subscriptions to it and thereby listen to changes when they occur. This means we can easily create collaboration apps, where a number of clients are operating on the same data source. This is a very interesting topic. This means you can quickly supply your Angular app with a backend, so for that reason, it deserves its own chapter. It also happens to be the next chapter of this book.

Back to the point we were trying to make. On paper, it sounds like the addition of sockets or Firebase would complicate our service a whole lot. In practice, they don't. The only thing you need to keep in mind is that when such data arrives, it needs to be added to our tasks array. We make the assumption here that it is interesting to deal with tasks from a HTTP service as well as from full duplex connections like Firebase or web sockets. 

Let's have a look at what it would look like to involve an HTTP service and sockets in our code. You can easily leverage sockets by using a library that wraps its API.

WebSockets are supported natively by most browsers, but it is considered experimental still. With that said, it still makes sense to rely on a library that helps us work with sockets, but it's worth keeping track of when WebSockets are becoming less experimental as we would no longer consider using a library. For the interested reader, please check the official documentation at https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API 

One such library is the socket.io library; it can be installed in the following way:

npm install socket.io

To start using this in Angular, you need to:

  1. Import the socket.io-client.
  2. Establish a connection by calling io(url); this will return a socket that you can add subscriptions to.
  3. Wait for incoming events that will contain a payload that we want to display in our app.
  4. Generate events and send the possible payload when you want to talk to a backend

The following code will only show you how to do these steps. There is more to socket implementation, though, such as creating a backend. To see what a full example with Angular and socket.io looks like, the interested reader is encouraged to have a look at the following article by Torgeir Helgwold:

http://www.syntaxsuccess.com/viewarticle/socket.io-with-rxjs-in-angular-2.0

This isn't really an HTTP topic, which is why we settle for only showing the points of interest in the code, which is where we would receive the data and add it to our tasks array. We also highlight the setting up and tearing down of the socket. Highlighting is done in bold, as follows: 

import * as io from 'socket.io-client';

export class TaskService {
subscription;
tasks:Task[] = [];
constructor(private http:HttpClient) {
this.fetchData();

this.socket = io(this.url); // establishing a socket connection

this.socket.on('task', (data) => {
// receive data from socket based on the 'task' event happening
this.tasks = [ ..this.tasks, data ];
});

}

private fetchData() {
this.subscription =
this.http.get<Task[]>('/tasks')
.subscribe( data => this.tasks = data );
}

// call this from the component when component is being destroyed
destroy() {
this.socket.removeAllListeners('task'); // clean up the socket
connection

}
}

This is a very simple example that works well for showing data in a template, as well updating the template when the tasks array changes. As you can see, if we involve a socket, it won't matter; our template will still be updated.

This way of doing it also comprises another scenario—how do two sibling components or more communicate? The answer is quite simple: they use the TaskService. If you want the other components template to be updated, then simply change the contents of the tasks array and it will be reflected in the UI. The code for this follows:

@Component({
template: `
<div *ngFor="let task of tasks">
{{ task.name }}
</div>
<input [(ngModel)]="newTask" />
<button (click)="addTask()" ></button>
`
})
export class FirstSiblingComponent {
newTask: string;

constructor(private service: TaskService) {}

get tasks() {
return this.taskService.tasks;
}

addTask() {
this.service.addTask({ name : this.newTask });
this.newTask = '';
}
}

This means that we also need to add a addTask() method to our service, like so:

import * as io from 'socket.io-client';

export class TaskService {
subscription;
tasks: Task[] = [];
constructor(private http:Http) {
this.fetchData();

this.socket = io(this.url); // establishing a socket connection

this.socket.on('task', (data) => {
// receive data from socket based on the 'task' event happening
this.tasks = [ ..this.tasks, data ];
});
}

addTask(task: Task) {
this.tasks = [ ...this.tasks, task];
}

private fetchData() {
this.subscription =
this.http.get('/tasks')
.subscribe(data => this.tasks = data);
}

// call this from the component when component is being destroyed
destroy() {
this.socket.removeAllListeners('task'); // clean up the socket
connection
}
}

The other component would look pretty much identical in terms of the parts that relate to setting up the taskService, exposing a tasks property and manipulating the tasks list. Regardless of which one of the components takes the initiative to change the task list through user interaction, the other component would be notified. I want to highlight what makes this general approach work, though. For this approach to work, you need to expose the tasks array through a getter in the component, like so:

get tasks() {
return this.taskService.tasks;
}

Otherwise, the changes to it won't be picked up. 

There is one drawback, though. What if we wanted to know exactly when an item was added and, say, display some CSS based on that? In that case, you have two options:

  • Set up the socket connection in the component and listen for the data changes there.
  • Use a behavior subject inside of the task service instead of a task array. Any changes from HTTP or socket will write to the subject through subject.next(). If you do this, then you can simply subscribe to the subject when a change happens.

The last alternative is a bit complicated to explain in a few words, so the whole next section is dedicated to explaining how you can use a BehaviourSubject over an array.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset