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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
getHeroes(): void { | |
this.heroService.getHeroes() | |
.subscribe(heroes => this.heroes = heroes); | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
componentWillLoad() { | |
this.getHeroes(); | |
} |
This is how the updated heroes
component should look like:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} | |
} |
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 theHeroService
- 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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} | |
} |
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 MessageService
and 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!
Leave a Reply