Picture is for comedic value, I have nothing against Angular.
If you just landed on this tutorial make sure to read part 1 over here first: https://nerdic-coder.com/2018/04/21/stencil-tutorial-tour-of-heroes/
If you have not done part 1 yet, you can pick up the code from here to follow along in this part 2 of the tutorial: https://github.com/nerdic-coder/stencil-tour-of-heroes/tree/v1.1
Display a Heroes List
Now we will expand the Tour of Heroes app to display a list of heroes, and allow users to select a hero and display the hero’s details.
Create mock heroes
Create a file called mock-heroes.ts
in the src/components/heroes/
folder. Define a HEROES
constant as an array of ten heroes and export it. The file 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 { Hero } from '../../models/hero'; | |
export const HEROES: Hero[] = [ | |
{ id: 11, name: 'Mr. Nice' }, | |
{ id: 12, name: 'Narco' }, | |
{ id: 13, name: 'Bombasto' }, | |
{ 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' } | |
]; |
Displaying heroes
Open the Heroes
component file and import the mock HEROES
.
import { HEROES } from './mock-heroes';
Add a heroes
property to the class that exposes these heroes for binding.
private heroes: Hero[] = HEROES;
List heroes with map JSX operator
Open the Heroes
component file and make the following changes in the render return:
- Add an
<h2>
at the top, - Below it add an HTML unordered list (
<ul>
) - Insert an
<li>
within the<ul>
that displays properties of ahero
. - Sprinkle some CSS classes for styling (you’ll add the CSS styles shortly).
- Add the
map
operator around the<li>
element.
Make it 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
<ul class="heroes"> | |
{this.heroes.map((hero) => | |
<li> | |
<span class="badge">{hero.id}</span> {hero.name} | |
</li> | |
)} | |
</ul> |
Might not be as clean looking as Angulars *ngFor, but here it’s very clear what’s a part of the loop.
Style the heroes
Add the following css code to the heroes.css
file:
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
/* HeroesComponent's private CSS styles */ | |
.selected { | |
background-color: #CFD8DC !important; | |
color: white; | |
} | |
.heroes { | |
margin: 0 0 2em 0; | |
list-style-type: none; | |
padding: 0; | |
width: 15em; | |
} | |
.heroes li { | |
cursor: pointer; | |
position: relative; | |
left: 0; | |
background-color: #EEE; | |
margin: .5em; | |
padding: .3em 0; | |
height: 1.6em; | |
border-radius: 4px; | |
} | |
.heroes li.selected:hover { | |
background-color: #BBD8DC !important; | |
color: white; | |
} | |
.heroes li:hover { | |
color: #607D8B; | |
background-color: #DDD; | |
left: .1em; | |
} | |
.heroes .text { | |
position: relative; | |
top: -3px; | |
} | |
.heroes .badge { | |
display: inline-block; | |
font-size: small; | |
color: white; | |
padding: 0.8em 0.7em 0 0.7em; | |
background-color: #607D8B; | |
line-height: 1em; | |
position: relative; | |
left: -1px; | |
top: -4px; | |
height: 1.8em; | |
margin-right: .8em; | |
border-radius: 4px 0 0 4px; | |
} |
Just like in Angular stylesheets identified in the @Component
metadata are scoped to that specific component.
Master/Detail
When the user clicks a hero in the master list, the component should display the selected hero’s details at the bottom of the page.
In this section, you’ll listen for the hero item click event and update the hero detail.
Add a click event binding
Add a click event binding to the <li>
like this:
<li onClick={ () => this.onSelect(hero)}>
Here we use the default html onClick event instead of Angular’s (click), other then that it’s very much the same.
Add the click event handler
Rename the component’s hero
property to selectedHero
but don’t assign it. There is no selected hero when the application starts.
Add the following onSelect()
method, which assigns the clicked hero from the template to the component’s selectedHero
.
@State() private selectedHero: Hero; onSelect(hero: Hero): void { this.selectedHero = hero; }
Update the details template
The template still refers to the component’s old hero
property which no longer exists. Rename hero
to selectedHero
.
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
<h2>{ this.selectedHero.name.toUpperCase() } Details</h2> | |
<div><span>id: </span>{this.selectedHero.id}</div> | |
<div> | |
<label>name: | |
<input type="text" value={this.selectedHero.name} onInput={(event) => this.handleChangeName(event)} placeholder="name" /> | |
</label> | |
</div> |
Hide empty details with the JavaScript ternary operator
After the browser refreshes, the application is broken.
Open the browser developer tools and look in the console for an error message like this:
TypeError: Cannot read property ‘name’ of undefined
What happened?
When the app starts, the selectedHero
is undefined
by design.
Binding expressions in the template that refer to properties of selectedHero
— expressions like {this.selectedHero.name}
— must fail because there is no selected hero.
The fix
The component should only display the selected hero details if the selectedHero
exists.
In Stencil we fix this by wrapping the details in a ternary operator 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
{this.selectedHero ? ( | |
<div> | |
<h2>{ this.selectedHero.name.toUpperCase() } Details</h2> | |
<div><span>id: </span>{this.selectedHero.id}</div> | |
<div> | |
<label>name: | |
<input type="text" value={this.selectedHero.name} onInput={(event) => this.handleChangeName(event)} placeholder="name" /> | |
</label> | |
</div> | |
</div> | |
) : ( | |
<p>Select a hero!</p> | |
) | |
} |
So the expression starts with checking if this.selectedHero
is defined and if it is defined it renders the first parentheses after the question mark, if it’s not defined it shows the content of the second parentheses. If you wouldn’t want to show anything if selectedHero is undefined you could just set null
inside the second parentheses.
Style the selected hero
That selected hero coloring is the work of the .selected
CSS class in the styles you added earlier. You just have to apply the .selected
class to the <li>
when the user clicks it.
Add this conditional class attribute to the hero list <li>
element:
<li class={(this.selectedHero === hero ? ‘selected’ : ‘’)} onClick={ () => this.onSelect(hero)}>
Master/Detail Components
Now we’re going to break out the hero details part into it’s own Stencil component, so it can be reusable for example.
Create a new folder hero-details
inside the components
folder and create the following files in the new folder,
- hero-details.css
- hero-details.spec.ts
- hero-details.tsx
Add the following code to the hero-details.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 { HeroDetails } from './hero-details'; | |
describe('app-hero-details', () => { | |
it('should build', () => { | |
expect(new HeroDetails()).toBeTruthy(); | |
}); | |
describe('rendering', () => { | |
beforeEach(async () => { | |
await render({ | |
components: [HeroDetails], | |
html: '<app-hero-details></app-hero-details>' | |
}); | |
}); | |
}); | |
}); |
Remove the details part of the heroes
component and add it to the hero-details
component so it looks something 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 { Component, Prop } from '@stencil/core'; | |
import { Hero } from '../../models/hero'; | |
@Component({ | |
tag: 'app-hero-details', | |
styleUrl: 'hero-details.css' | |
}) | |
export class HeroDetails { | |
@Prop({ mutable: true }) private hero: Hero; | |
handleChangeName(event) { | |
this.hero = { | |
id: this.hero.id, | |
name: event.target.value | |
}; | |
} | |
render() { | |
return ( | |
<div class='app-heroes'> | |
{this.hero ? ( | |
<div> | |
<h2>{ this.hero.name.toUpperCase() } Details</h2> | |
<div><span>ids: </span>{this.hero.id}</div> | |
<div> | |
<label>name: | |
<input type="text" value={this.hero.name} onInput={(event) => this.handleChangeName(event)} placeholder="name" /> | |
</label> | |
</div> | |
</div> | |
) : ( | |
null | |
) | |
} | |
</div> | |
); | |
} | |
} |
Add the @Prop()
hero property
Props are custom attribute/properties exposed publicly on the element that developers can provide values for. We add the mutable option to the Prop() to let the component know that it’s allowed to update the hero
variable, when we edit the name in the input field for example.
Show the HeroDetails Component
Now simply to be able to show the hero details again on the heroes page, simply add the following html-tag to the heroes.tsx
file below the heroes list,
<app-hero-details hero={this.selectedHero}></app-hero-details>
The two components will have a parent/child relationship. The parent Heroes
component will control the child HeroDetails
by sending it a new hero to display whenever the user selects a hero from the list.
Conclusion
That concludes part 2 of this tutorial. In the next part we will look at how we can add service providers to deliver the heroes data to the components.
You can check the result of this tutorial in the Github repository tag v2.0: https://github.com/nerdic-coder/stencil-tour-of-heroes/tree/v2.0
You can continue to part 3 here: https://nerdic-coder.com/2018/05/01/stencil-tutorial-tour-of-heroes-part-3/
Hope you have enjoyed this part and let me know in the comments what you think of this guide.
Leave a Reply