Time to make your final decision!
If you just landed on this tutorial make sure to read part 1 to part 4 over here first: Stencil — Tutorial: Tour of Heroes Part 1
If you have not done part 4 yet, you can pick up the code from here to follow along in this part 5 of the tutorial: https://github.com/nerdic-coder/stencil-tour-of-heroes/tree/v4.0
HTTP Requests
Now it’s time to take the step from mock data to fetching the heroes from a backend server. It could anything from firebase, mongodb, php server or any system you can think that can deliver some JSON-data.
Json-server
For this tutorial it will be enough with a simple json-server that I found on npmjs.com, so start by installing the json server as a developer dependency with command:
npm install json-server --save-dev
Then create a json file db.json
in the root folder, that will be used as the database for the json-server, we will populate it with our mocked heroes list as a starting point:
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
{ | |
"heroes": [ | |
{ | |
"id": 11, | |
"name": "Mr. Nice" | |
}, | |
{ | |
"id": 12, | |
"name": "Narcos" | |
}, | |
{ | |
"id": 13, | |
"name": "Bombasto coolio" | |
}, | |
{ | |
"id": 14, | |
"name": "Celeritas" | |
}, | |
{ | |
"id": 15, | |
"name": "Magneta" | |
}, | |
{ | |
"id": 16, | |
"name": "RubberMan" | |
}, | |
{ | |
"id": 17, | |
"name": "Dynama" | |
}, | |
{ | |
"id": 18, | |
"name": "Dr IQ" | |
}, | |
{ | |
"id": 19, | |
"name": "Magma" | |
}, | |
{ | |
"id": 20, | |
"name": "Tornado" | |
}, | |
{ | |
"id": 25, | |
"name": "Kalle" | |
} | |
] | |
} |
Then we will create a new npm script in the package.json
to make a shorter command to start the json-server, add the following to the scripts section of package.json
:
"mock": "json-server --watch db.json"
Then we can start it with command npm run mock
. So now we just need to rewrite the app to use this http service instead of the mock file.
Server configuration
Since it’s a bad idea to have the server url hard coded in all the methods calling the server, we will create a simple config file containing the server url. This file can also be used for other global configurations in the future.
Create a new folder under src
called global
and the create a file config.ts
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
export const CONFIG: Config = | |
{ SERVER_URL: 'http://localhost:3000/' }; | |
class Config { | |
SERVER_URL: string; | |
} |
Now if the server url changes for some reason, we will only need to update it in one place.
Get heroes with a XMLHttpRequest
Since we don’t have Angular’s fancy HttpClient here we will use the old school Javascript standard XMLHttpRequest object.
So the first thing we need to do in the HeroService
class is importning the CONFIG
like this:
import { CONFIG } from '../global/config';
Then we rewrite the getHeroes
method 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
getHeroes(): Observable<Hero[]> { | |
return Observable.create((observer) => { | |
const xhr = new XMLHttpRequest(); | |
xhr.open('GET', CONFIG.SERVER_URL + 'heroes'); | |
xhr.onload = () => { | |
if (xhr.status === 200) { | |
this.messageService.add(`HeroService: fetched heroes`); | |
observer.next(JSON.parse(xhr.responseText)); | |
} | |
else { | |
observer.error(xhr.response); | |
} | |
}; | |
xhr.send(); | |
}); | |
} |
So first we create an Observable that will return the Heroes when the response from the server is processed.
The lines after that we setup the configuration for the XMLHttpRequest instance xhr
with the server url and the path to the heroes on the server, we are using http method GET so the server knows we want a list of heroes returned.
Then we send the request with xhr.send();
and wait for the response. Then we process the response inside xhr.onload
.
If the request is successful we set the returned hero list to the observable so that all subscribers know that a new list of heroes have been fetched from the backend. If an error occurs the observer returns an error.
That’s all we need to change, the rest of the application continuous to operate as before.
Get hero by id
Now we also want to change the getHero
method in HeroService
to get the hero from the API.
Change the getHero
method 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
getHero(id: number): Observable<Hero> { | |
return Observable.create((observer) => { | |
const xhr = new XMLHttpRequest(); | |
xhr.open('GET', CONFIG.SERVER_URL + `heroes/${id}`); | |
xhr.onload = () => { | |
if (xhr.status === 200) { | |
this.messageService.add(`HeroService: fetched hero with id:${id}`); | |
observer.next(JSON.parse(xhr.responseText)); | |
} | |
else { | |
observer.error(xhr.response); | |
} | |
}; | |
xhr.send(); | |
}); | |
} |
It looks very similar to getHeroes
method but here we add an id to the API path to only get one hero from the API.
Update heroes
Now we’re going into the more interesting part with having a backend service, updating heroes information.
So now we will create a updateHero
method in the HeroService
class 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
/** PUT: update the hero on the server */ | |
updateHero(hero: Hero): Observable<any> { | |
return Observable.create((observer) => { | |
const xhr = new XMLHttpRequest(); | |
xhr.open('PUT', CONFIG.SERVER_URL + `heroes/${hero.id}`, true); | |
xhr.setRequestHeader('Content-type','application/json; charset=utf-8'); | |
xhr.onload = () => { | |
if (xhr.status === 200) { | |
this.messageService.add(`HeroService: updated hero with id:${hero.id}`); | |
observer.next(JSON.parse(xhr.responseText)); | |
} | |
else { | |
observer.error(xhr.response); | |
} | |
}; | |
xhr.send(JSON.stringify(hero)); | |
}); | |
} |
Looks very similar to the other once but here we send in a hero
object that is to be updated. We use HTTP method PUT instead of GET to inform the server that we want to update an hero, we set a request header with a Content-type ‘application/json; charset=utf-8’, so the server knows what format we are sending to it.
Now let’s fix so that the hero-details
component can actually save the changes on a hero.
Let’s start with adding a save method 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
save(): void { | |
this.heroService.updateHero(this.hero) | |
.subscribe(() => this.goBack()); | |
} |
This method will take the current state of the hero object in hero-details
and send it to our newly created updateHero
method in HeroService
and then it will leave the hero-details view.
Now all we have to do is add a button that will trigger the save method:
<button onClick={() => this.save()}>save</button>
Now we can update heroes on the hero-details view!
Add a new hero
Let’s create a form to add new heroes, we will begin with adding a new addHero
method in the HeroService
class 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
/** POST: add a new hero to the server */ | |
addHero (hero: Hero): Observable<Hero> { | |
return Observable.create((observer) => { | |
const xhr = new XMLHttpRequest(); | |
xhr.open('POST', CONFIG.SERVER_URL + `heroes`, true); | |
xhr.setRequestHeader('Content-type','application/json; charset=utf-8'); | |
xhr.onload = () => { | |
if (xhr.status === 201) { | |
this.messageService.add(`HeroService: added new hero`); | |
observer.next(JSON.parse(xhr.responseText)); | |
} | |
else { | |
observer.error(xhr.response); | |
} | |
}; | |
xhr.send(JSON.stringify(hero)); | |
}); | |
} |
Again looks very similar to the updateHero
method but now we make a POST requst instead telling the server to create a new hero instead of updating an existing hero.
Next step is to add a form to the heroes
component that will be used for creating new heroes.
We will take the handleChangeName
method that helps us keeping the hero object up to date and then we will add an add
method that will save the new hero for us, 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
handleChangeName(event) { | |
this.hero = { | |
id: this.hero.id, | |
name: event.target.value | |
}; | |
} | |
add(): void { | |
this.hero.name = this.hero.name.trim(); | |
if (!this.hero.name) { return; } | |
this.heroService.addHero(this.hero).subscribe(hero => { | |
this.heroes.push(hero); | |
this.hero = { | |
id: this.hero.id, | |
name: "" | |
}; | |
}); | |
} |
Nothing mind blowing going on here, the hero is sent to the service and then pushed into the heroes list once it’s saved.
Now let’s add the html form under the hero list 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
<div> | |
<label>Hero name: | |
<input type="text" value={this.hero.name} onInput={(event) => this.handleChangeName(event)} placeholder="name" /> | |
</label> | |
<button onClick={() => this.add()}> | |
add | |
</button> | |
</div> |
Delete a hero
It might get crowded in your hero list while trying all this out, so it’s a good idea at this point to create a delete button in the heroes list.
Let’s add a deleteHero
method to the HeroService
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
/** DELETE: delete the hero from the server */ | |
deleteHero (hero: Hero | number): Observable<Hero> { | |
return Observable.create((observer) => { | |
const id = typeof hero === 'number' ? hero : hero.id; | |
const xhr = new XMLHttpRequest(); | |
xhr.open('DELETE', CONFIG.SERVER_URL + `heroes/${id}`, true); | |
xhr.onload = () => { | |
if (xhr.status === 200) { | |
this.messageService.add(`HeroService: deleted hero with id:${id}`); | |
observer.next(JSON.parse(xhr.responseText)); | |
} | |
else { | |
observer.error(xhr.response); | |
} | |
}; | |
xhr.send(); | |
}); | |
} |
Again we setup the request and sends in the hero id but this time we send http DELETE request to remove the hero from the server.
Then let’s add a delete button on the hero list next to each hero:
<button class="delete" title="delete hero" onClick={() => this.delete(hero)}>x</button>
Let’s create the delete method that will glue this all together in the heroes.tsx file:
We begin with filtering out the hero from the heroes list, so we don’t have to reload the whole heroes list. Then we delete the hero from the server.
Search by name
The last thing we will add is a search method for our heroes.
Start with adding the search method in the HeroService.tsx file:
Similar request as the others, but now we also add a search term to the end of the request ‘heroes/?q=${term}‘.
Let’s create a new ‘HeroSearch’ component so we then will be able to insert a search form wherever we like later on.
Create the following file inside ‘/src/components/hero-search’:
- hero-search.css
- hero-search.spec.ts
- hero-search.tsx
With the following contents:
So the core part here is the ‘private searchTerms = new Subject<string>();’, it’s an observable type called Subject that is wired to the search-box input.
Then in the search(event) method we update the searchTerm that then trigger the subscription we configured in the componentWillLoad() method. That updates the heroes list if all criteria in the configuration are meet.
Finally add the HeroSearch component to the end of dasboard.tsx ‘<app-hero-search></app-hero-search>’.
Conclusion
You have now reach the end of this series of converting the Angular Tour of Heroes to Stencil.
You can review the final solution on GitHub here: https://github.com/nerdic-coder/stencil-tour-of-heroes/tree/v5.0
Hope you have enjoyed this journey and please let me know in the comments what you think of this articles.
Please share this with a nerdic friend who want to learn more about Stencil.