Stencil — Tutorial: Tour of Heroes Part 2

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:


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' }
];

view raw

mock-heroes.ts

hosted with ❤ by GitHub

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 a hero.
  • 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:


<ul class="heroes">
{this.heroes.map((hero) =>
<li>
<span class="badge">{hero.id}</span> {hero.name}
</li>
)}
</ul>

view raw

hero-loop.html

hosted with ❤ by GitHub

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:


/* 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;
}

view raw

heroes.css

hosted with ❤ by GitHub

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.


<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>

view raw

hero-details.ts

hosted with ❤ by GitHub

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.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>
)
}

view raw

heroes.tsx

hosted with ❤ by GitHub

So the expression starts with checking if this.selectedHerois 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 :


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:


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.

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 )

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: