Stencil — Tutorial: Tour of Heroes Part 3

Robin the hidden hero, just like service classes below the components surface!

If you just landed on this tutorial make sure to read part 1 and part 2 over here first: https://nerdic-coder.com/2018/04/21/stencil-tutorial-tour-of-heroes/

If you have not done part 1 and 2 yet, you can pick up the code from here to follow along in this part 3 of the tutorial: https://github.com/nerdic-coder/stencil-tour-of-heroes/tree/v2.0

Services

In this tutorial, we’ll create a HeroService that all application classes can use to get heroes. Instead of creating that service with new or relying on Angular dependency injection, we will create a static instance of the HeroService inside itself that will be used to inject it into the Heroes component constructor.

Services are a great way to share information among classes that don’t know each other.

Create the HeroService

First create a new folder under under src named services. Then create the file hero.service.ts. The HeroService class should look like this:


import { Observable } from 'rxjs';
import { of } from 'rxjs';
import { Hero } from '../models/hero';
import { HEROES } from './mock-heroes';
export class HeroService {
private static _instance: HeroService;
getHeroes(): Observable<Hero[]> {
return of(HEROES);
}
public static get Instance(): HeroService {
// Do you need arguments? Make it a regular method instead.
return this._instance || (this._instance = new this());
}
}

view raw

hero.service.ts

hosted with ❤ by GitHub

Instance services

One of the hardest part for me in creating this service was how I should find a similar solution to Angular’s injectable solution. First I found a similar injectable solution called InversifyJS, but I could not get it to work at all and it seems to be more complex to use then Angular’s solution.

Then I tried another solution that I found called “Creating Shared State in Stencil”, that solution I actually got working but I felt like it added to much overhead when you had to create several extra injector classes for each service to make it work.

So finally I found the solution that I’m using above, with plain typescript to create a static instance of the HeroService class and that instance can be fetched from anywhere with the method call HeroService.Instance .

Observable data

Just like in Angular we are using the Observable features from the RxJS library.

In this tutorial, we simulate getting data from the server with the RxJS of() function.

You need to add the rxjs npm package with npm install rxjs --save .

Get hero data

The HeroService could get hero data from anywhere—a web service, local storage, or a mock data source.

Removing data access from components means you can change your mind about the implementation anytime, without touching any components. They don’t know how the service works.

The implementation in this tutorial will continue to deliver mock heroes.

Let’s begin with moving the mock-heroes.ts to the services folder.

Then we import the Hero and HEROES.

We add a getHeroes method to return the mock heroes.

Update Heroes Component

Open the Heroes class file.

Delete the HEROES import as you won’t need that anymore. Import the HeroService instead.

import { HeroService } from '../../services/hero.service';

Remove the HEROES assignment from the heroes definition and add a State decorator to it so that the render function will be called again.

@State() private heroes: Hero[];

Inject the HeroService

Create a private class variable and assign it to the HeroService instance in the constructor.


private heroService: HeroService;
constructor() {
this.heroService = HeroService.Instance;
}

This sets the heroService variable to the singleton instance of HeroService.

Add getHeroes()

Create a function to retrieve the heroes from the service.


getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}

view raw

getHeroes.ts

hosted with ❤ by GitHub

Call it in componentWillLoad

While you could call getHeroes() in the constructor, that’s not the best practice.

Reserve the constructor for simple initialization such as wiring constructor parameters to properties. The constructor shouldn’t do anything.

Instead, call getHeroes() inside the Stencil’s componentWillLoad lifecycle hook.


componentWillLoad() {
this.getHeroes();
}

This is how the updated heroes component should look like:


import { Component, State } from '@stencil/core';
import { Hero } from '../../models/hero';
import { HeroService } from '../../services/hero.service';
@Component({
tag: 'app-heroes',
styleUrl: 'heroes.css'
})
export class Heroes {
private heroService: HeroService;
@State() private selectedHero: Hero;
@State() private heroes: Hero[];
constructor() {
this.heroService = HeroService.Instance;
}
/**
* The component will load but has not rendered yet.
*
* This is a good place to make any last minute updates before rendering.
*
* Will only be called once
*/
componentWillLoad() {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
render() {
return (
<div class='app-heroes'>
<h2>My Heroes</h2>
<ul class="heroes">
{this.heroes ? (this.heroes.map((hero) =>
<li class={(this.selectedHero === hero ? 'selected' : '')} onClick={ () => this.onSelect(hero)}>
<span class="badge">{hero.id}</span> {hero.name}
</li>
)) : (null)}
</ul>
<app-hero-details hero={this.selectedHero}></app-hero-details>
</div>
);
}
}

view raw

heroes.tsx

hosted with ❤ by GitHub

Show messages

In this section you will

  • add a Messages component that displays app messages at the bottom of the screen.
  • create an injectable, app-wide MessageService for sending messages to be displayed
  • inject MessageService into the HeroService
  • display a message when HeroService fetches heroes successfully.

Create a MessageService

Create a message.service.ts file in the services folder and fill it with the following content:


import { Observable, Subject } from 'rxjs';
export class MessageService {
private static _instance: MessageService;
private messages: string[] = [];
private subject: Subject<string[]> = new Subject();
add(message: string) {
this.messages.push(message);
this.subject.next(this.messages);
}
clear() {
this.messages = [];
this.subject.next(this.messages);
}
getMessages(): Observable<string[]> {
return this.subject.asObservable();
}
public static get Instance(): MessageService {
// Do you need arguments? Make it a regular method instead.
return this._instance || (this._instance = new this());
}
}

This looks very similar to the HeroService class, but here we also have a Subject that is a type of observable that makes it possible for us to subscribe to changes on arrays and other variables. Here we want to tell every subscriber as soon as a new message is added to the messages array or if we clear the messages array.

Inject it into the HeroService

Re-open the HeroService and import the MessageService.

import { MessageService } from './message.service';

Create a private class variable for the MessageService and assign the MessageService instance to it in the constructor:

private messageService: MessageService;
constructor() {
  this.messageService = MessageService.Instance;
}

Add a message when the heroes is loaded in getHeroes() method:

getHeroes(): Observable<Hero[]> {
  // TODO: send the message _after_ fetching the heroes
  this.messageService.add(‘HeroService: fetched heroes’);
  return of(HEROES);
}

Create a Messages Component

Now we’re going to make a stencil component to show the messages that have been added to the MessageService .

Create a new folder messages inside the components folder and create the following files in the new folder,

  • messages.css
  • messages.spec.ts
  • messages.tsx

Add the following code to the messages.spec.ts :


import { render } from '@stencil/core/testing';
import { Messages } from './messages';
describe('app-messages', () => {
it('should build', () => {
expect(new Messages()).toBeTruthy();
});
describe('rendering', () => {
beforeEach(async () => {
await render({
components: [Messages],
html: '<app-messages></app-messages>'
});
});
});
});

Add the following to the messages.tsx :


import { Component, State } from '@stencil/core';
import { MessageService } from '../../services/message.service';
@Component({
tag: 'app-messages',
styleUrl: 'messages.css'
})
export class Messages {
private messageService: MessageService;
@State() private messages: string[];
constructor() {
this.messageService = MessageService.Instance;
}
componentWillLoad() {
this.getMessages();
}
getMessages(): void {
this.messageService.getMessages()
.subscribe(messages => {
this.messages = [];
this.messages = messages;
});
}
render() {
return (
<div class='app-messages'>
Messages:
{this.messages ?
(
<button class="clear"
onClick={() => this.messageService.clear()}>clear</button>
) : ( null )
}
{this.messages ?
(
this.messages.map((message) =>
<p>{message}</p>
)
) : ( null )
}
</div>
);
}
}

view raw

messages.tsx

hosted with ❤ by GitHub

Here we first import the MessageService and assign it’s instance to a private variable.

When the component loads we subscribe to the observable message list from the MessageServiceand in the render method we loop the messages. For the renderer to reload I had to first assign the messages to an empty array and then to the new messages list.

We also have a clear button above the list that calls the clear function in MessageService.I tried having it in the same if-statement as the loop, but it would not compile. So if anyone knows the correct syntax for that, please let me know!

Conclusion

This concludes part 3 of this series and next up is dealing with routing between different pages with the Stencil router.

You can continue on to part 4 here: https://nerdic-coder.com/2018/05/10/stencil-tutorial-tour-of-heroes-part-4/

You can review the full example of this tutorial on this Github tag: https://github.com/nerdic-coder/stencil-tour-of-heroes/tree/v3.0

If you gotten this far thanks for reading!

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Create a website or blog at WordPress.com

Up ↑

%d bloggers like this: