From Angular to Composi

Angular has a tutorial called Tour of Heroes which they use to show how to use many common Angular patterns. You can see a live version on Plnkr. It uses Angular directives, two-way data binding, formating with Angular pipes, services and client side routing.

Recently I read an article where John Papa took an Angular tutorial and converted it into VueJS.

I thought it would be interesting to see how to convert an Angular tutorial to Composi. So, I decided to use the famous Angular tutorial: Tour of Heroes. In contrast with Angular, we’ll be using patterns popularized by React: no directives, instead we’ll use props to pass data and other things down to child components, one-way data flow, and client side routing to conditionally render subcomponents. The source code for this tutorial is available on Github: https://github.com/composor/tour-of-heroes

The Parts

We need to break this down into the pieces we need to build. From the images we can see that the menu is consistence across screens. So that will be the first part we’ll create. Next we’ll need the create the dashboard, followed by the list of heroes. Then we’ll need to make it so the user can click on a dashboard item or list item to get to the hero detail view.

Here are some screenshots of what we will create:

Dashboard

Hero Search

Hero List

Hero Detail

New Project

We’ll start by creating a new project:

composi -n tour-of-heroes

In our dev folder we'll create a file mock-heroes.js with the following data:

// Mock Data:

const mockHeroes = [

{ "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" }

]

The Title

The first thing we’ll do is update the project’s title component. You’ll find that in the components folder of the dev folder. Update to this:

export default new Component ({

container: 'header',

state: 'Tour of Heroes',

render: (message) => <h1><a href="/">{message}</a></h1>

})

Because we’re instantiating a class with state, no need to do anything more than import it into our app.js file. And that's already set up by default when we created a new project.

Codepen example:

Menu

Now let’s make that menu. Create a new file in the components folder in the dev folder. Call it: menu.js . Inside it we'll define the menu. It's a very simple component:

export default new Component({

container: 'section',

state: true,

render: (data) => {

return (

<nav>

<ul>

<li><a href="#/dashboard">Dashboard</a></li>

<li><a href="#/heroes">Heroes</a></li>

</ul>

</nav>

)

}

})

All we’re doing is creating two links: one to the dashboard and one to the list of heroes. Later we’ll setup client-side routing to handle loading the dashboard and list.

The menu component is expecting a section tag. We'll need to open the index.html file and add a section tag right after the header tag.

Codepen example:

Open up the project’s app.js file inside the dev folder. Delete all the default code except for the imports.

Create a new file in the components folder and call it app.js . This is different from the default app.js at the root of the project. This will be a class. At the top of the file add the following imports:

import {h, render, Component} from 'composi'

Now let’s create the App class shell:

export default class App extends Component {

constructor(props) {

super(props)

this.container = 'section'

this.state = {

activeComponent: 'dashboard',

heroes: [],

selectedHero: '',

searchResults: []

}

}

}

In the class constructor we are designating a section tag as the component container. We also give it some initial state. All the values are blank except for one: activeComponent: 'dashboard' . This means that when the component is initialized, the active component it will show will be the dashboard. For that to happen, we will of course need a dashboard component. We'll skip ahead and create that, then return here to the App component.

Dashboard

The dashboard component will be a child of our App component that we just started. It will get its props from its parent. The dashboard component will be a functional component — no class necessary. Use functional components as children of class components. These are consumers of the parent class state, methods, event handlers, etc.

For now we’re going to start with a simplified version that just shows the four most important heroes. Later we’ll add the ability to search for a hero by name.

export default function HeroDashboard({heroes}) {

const selectHeroes = heroes.slice(1, 5)

return (

<div class='dashboard'>

<h3>Top Heroes</h3>

<div class="grid grid-pad">

{

selectHeroes.map(hero => (

<a class="col-1-4" href={`#/detail/${hero.id}`}>

<div class="module hero">

<h4>{hero.name}</h4>

</div>

</a>

))

}

</div>

</div>

)

}

Render the Dashboard

Now let’s make it so the App component can render the dashboard.

import {h, render, Component} from 'composi'

// Import the dashboard component:

import HeroDashboard from './hero-dashboard' export default class App extends Component {

constructor(props) {

super(props)

this.container = 'section'

this.state = {

activeComponent: 'dashboard',

heroes: [],

selectedHero: '',

searchResults: []

}

}

render(state) {

return (

{

state.activeComponent === 'dashboard' &&

<HeroDashboard heroes={this.state.heroes} />

}

)

}

}

We Need Heroes!

We have one problem so far, we have data for heroes but we haven’t supplied them to our App component yet. We’ll create a folder called utils in our dev folder. Inside it we'll create a file called fetch-heroes . We'll keep this simple and just use fetch to fetch the data.

// Fetch mock heroes:

export default function fetchHeroes() {

return fetch('/dev/data/mock-heroes.js')

.then(function(response) {

return response.json()

})

}

Then, back in our App component, we’ll update the imports to include the fetchHeroes function. Then we'll add a lifecycle hook to our App component. We'll use componentDidMount (same as componentWasCreated . We fetch the heroes and set them to the App component's state:

import {h, render, Component} from 'composi'

import HeroDashboard from './hero-dashboard'

// Import fetch function:

import fetchHeroes from '../utils/fetch-heroes' export default class App extends Component {

constructor(props) {

super(props)

this.container = 'section'

this.state = {

activeComponent: 'dashboard',

heroes: [],

selectedHero: '',

searchResults: []

}

}

render(state) {

return (

{

state.activeComponent === 'dashboard' &&

<HeroDashboard heroes={this.state.heroes} />

}

)

} componentDidMount() {

// Fetch data for heroes:

fetchHeroes()

.then(heroes => {

// Set the heroes to the component's state:

this.setState({heroes})

})

}

}

Codepen example:

Routing

We need to make the menu work so that when the user clicks on Heroes they get the list of heroes and when they click on Dashboard they get that. For routing we’re going to use composi-router, which is based on Routie. It’s being maintained and has a few extra feautures, which we aren’t using in this demo, but whatever.

We need to add composi-router to our project:

npm i -D composi-router

Then, in that utils folder we created earlier for fetch-heroes we'll add a new file: routes.js . Inside it we'll import composi-router and setup our routes.

import {h, render, Component} from 'composi'

import {Router} from 'composi-router'

We're going to make three routes for now. Later we'll add a fourth for the details view. Our first route will handle the app title. If you remember, we set the title up to be a link. It points to the root domain:

<h1><a href="/">{message}</a></h1>

We want this link to be the default, which is to show the dashboard. Then we want to handle the two links in the menu component: the dashboard and the heroes list. We will handle which component gets shown by the App component state property activeComponent . By default we set it to 'dashboard', but when the user clicks on the heroes link in the menu, we want to set it to heroes . And when the user clicks on dashboard or the title, we'll set it back to dashboard :

import {h, render, Component} from 'composi'

import {Router} from 'composi-router' export default function(app) {

router({

'/': function() {

app.setState({activeComponent: 'dashboard'})

}, '/dashboard': function() {

app.setState({activeComponent: 'dashboard'})

}, '/heroes': function() {

app.setState({activeComponent: 'heroes'})

}

})

}

Heroes List

With routing set up, we need to create that hero list. In the components folder we'll create a file called hero-list.js . We are going to create another functional component. For now we're just displaying the heroes. Later we'll upgrade the list to enable adding a new hero or deleting an existing one.

import {h, Component} from 'composi' export default function HeroList({heroes}) {

return (

<div>

<ul class="heroes">

{

heroes.map(hero => (

<li>

<a href={`#/detail/${hero.id}`}>

<span class="badge">{hero.id}</span>

<span class='hero-link'>{hero.name}</span>

</a>

</li>

))

}

</ul>

</div>

)

}

With our component defined, we can update our App class to handle showing it or the dashboard, depending on the value of activeComponent . We need to update our imports to bring in the routing and the hero list component:

import {h, render, Component} from 'composi'

import HeroDashboard from './hero-dashboard'

// Import hero list component:

import HeroList from './hero-list'

import fetchHeroes from '../utils/fetch-heroes'

// Import routing:

import setupRoutes from '../utils/routes' export default class App extends Component {

constructor(props) {

super(props)

this.container = 'section'

this.state = {

activeComponent: 'dashboard',

heroes: [],

selectedHero: '',

searchResults: []

}

}

// Update to render either the dashboard or hero list:

render(state) {

return (

{

state.activeComponent === 'dashboard' &&

<HeroDashboard heroes={this.state.heroes} />

}

{

state.activeComponent === 'heroes' &&

<HeroList heroes={this.state.heroes} />

}

)

} componentDidMount() {

// Fetch data for heroes:

fetchHeroes()

.then(heroes => {

// Set the heroes to the component's state:

this.setState({heroes})

})

}

}

We now have some basic routing where the user can load the dashboard or hero list.

Codepen example:

Detail View

Now we’ll create the detail view. When the user clicks on a button in the dashboard or an item in the hero list, we’ll show the detial component. We’ll make a new file in the components folder called hero-details.js . We're keeping this basic right now just to ouput a detail view. Later we'll update it to allow modifying the hero name, or switch back to the original name. For now this is all we need:

import {h, Component} from 'composi' export default function HeroList({hero}) {

return (

<div id='hero-detail'>

<h2>{hero.name} details!</h2>

<div><label>id:</label> {hero.id}</div>

</div>

)

}

The dashboard items and hero list items are already set up with links that will set the url for the detail view. For example, if we click on the Narco button in the dashboard, the url would be updated to /detail/12 . So we need to update our defineRoutes function to handle that particular route:

// Define routes:

function setupRoutes(self) {

routie({

'': function() {

self.setState({activeComponent: 'dashboard'})

},

'/dashboard': function() {

self.setState({activeComponent: 'dashboard'})

},

'/heroes': function() {

self.setState({activeComponent: 'heroes'})

},

// Capture the value after `detail/` to determine which hero was chosen:

'/detail/:id': function(id) {

const state = app.state

const position = state.heroes.findIndex(person => person.id === id)

const hero = state.heroes[position]

hero.originalName = hero.name

app.setState({activeComponent: 'detail', selectedHero: hero})

}

})

}

Next we need to update the render function in our App component to render out the detail component when that route loads:

render(state) {

return (

<div class="app-root">

{

state.activeComponent === 'dashboard' && <HeroDashboard

heroes={this.state.heroes} />

}

{

state.activeComponent === 'heroes' && <HeroList

heroes={this.state.heroes} />

}

{

state.activeComponent === 'detail' &&

<HeroDetail hero={this.state.selectedHero} />

}

</div>

)

}

Now we have the detail component loading. Here it is on Codepen:

The Basics

We now have all the parts for the Tour of Heroes. We just need to add in some more details to the components to complete it.

Adding Search to the Dashboard

The first item we need to address is adding the ability to search for a hero from the dashboard. This will give us a dropdown menu of all matches based on what the user entered. Of course, if there are no matches there will be no dropdown. When there are matches, clicking on a match should show the detail for that hero. That means the dropdown will need to have links that set the url to trigger routing. That way our detail route will handle showing the correct hero automatically.

To implement search we’ll first need to create a search component. This has two purposes. It provides a serach box and then shows the results in a dropdown menu. In the components folder create a file and name it hero-search.js .

import {h} from 'composi' export default function HeroSearch({search, searchResults, blurSearchInput}) {

return (

<div id="search-component">

<h4>Hero Search</h4> <input id="search-box" onkeyup={(e)=> search(e)} onblur={() => blurSearchInput()} /> {

searchResults.length > 0 && (

<ul class="search-result">

{

searchResults.map(hero => (

<li>

<a href={`#/detail/${hero.id}`}>{hero.name}</a>

</li>

))

}

</ul>

)

}

</div>

)

}

Notice how we’re passing in three props: search , searchResults , blurSearchInput . Two are event handlers, search and blurSearchInput . searchResults will be the value from the App component's state. By default we have it set to an empty array. When that gets passed down, no dropdown will render. However, when the user does a search, that value will get updated. If the results are positive, the dropdown will appear.

We’ll need to update our dashboard component to import the serach component and render it. We also need to update the props that the dashboard gets:

import {h, Component} from 'composi'

import HeroSearch from './hero-search' export default function HeroDashboard({heroes, search, searchResults, blurSearchInput}) {

const selectHeroes = heroes.slice(1, 5)

return (

<div class='dashboard'>

<h3>Top Heroes</h3>

<div class="grid grid-pad">

{

selectHeroes.map(hero => (

<a class="col-1-4" href={`#/detail/${hero.id}`}>

<div class="module hero">

<h4>{hero.name}</h4>

</div>

</a>

))

}

</div>

<HeroSearch

search={search}

searchResults={searchResults}

blurSearchInput={blurSearchInput} />

</div>

)

}

Searching for a Hero

Now we need to make search happen, for that we’ll add two new methods to the App component. We also need to update the props we pass to the dashboard component:

import {h, render, Component} from 'composi'

import HeroDashboard from './hero-dashboard'

import HeroList from './hero-list'

import fetchHeroes from '../utils/fetch-heroes'

import setupRoutes from '../utils/routes' export default class App extends Component {

constructor(props) {

super(props)

this.container = 'section'

this.state = {

activeComponent: 'dashboard',

heroes: [],

selectedHero: '',

searchResults: []

}

}

// Update to render either the dashboard or hero list:

render(state) {

return (

{

state.activeComponent === 'dashboard' &&

<HeroDashboard

heroes={this.state.heroes}

search={this.search.bind(this)}

searchResults={this.state.searchResults}

blurSearchInput={this.blurSearchInput.bind(this)} />

}

{

state.activeComponent === 'heroes' &&

<HeroList heroes={this.state.heroes} />

}

{

state.activeComponent === 'detail' &&

<HeroDetail hero={this.state.selectedHero} />

}

)

} componentDidMount() {

// Fetch data for heroes:

fetchHeroes()

.then(heroes => {

// Set the heroes to the component's state:

this.setState({heroes})

})

} search(e) {

const input = e.target

const value = input.value

const heroes = this.state.heroes

const searchResults = heroes.filter(hero => {

const name = hero.name.toLowerCase()

return name.match(value.toLowerCase())

})

this.setState({searchResults: searchResults})

} blurSearchInput(e) {

const searchResults = this.state.searchResults

setTimeout(() => {

this.setState({searchResults: []})

}, 250)

}

}

With these changes in place we now have search enabled. Here is the Codepen version:

Adding a Hero

Next we want to let the user add a new hero to the list, as well as click on a list item’s x to delete a hero. Clicking on a new item should show the new hero’s detail view. That means it must have a unique id to use in the url fragment so that routing works. We’ll also need to new methods on our App component: addHero and deleteItem . Here's our updated hero list component. Notice that we are passing these new props to the component:

import {h, Component} from 'composi' export default function HeroList({heroes, deleteItem, addHero}) { return (

<div>

<p class='form--add-hero'>

<label htmlFor="add-hero">Hero name: </label>

<input id='add-hero' type="text"/>

<button onclick={addHero}>Add</button>

</p>

<ul class="heroes">

{

heroes.map(hero => (

<li>

<a href={`#/detail/${hero.id}`}>

<span class="badge">{hero.id}</span>

<span class='hero-link'>{hero.name}</span>

</a>

<button data-id={hero.id} class="delete" title="delete hero"

onclick={(e) => deleteItem(e)}>x</button>

</li>

))

}

</ul>

</div>

)

}

And here’s our updated App component with the two new methods for adding and delete a hero. Notice how we updated the render function to pass the two new methods to the HeroList tag so that they get exposed to the list component:

import {h, render, Component} from 'composi'

import HeroDashboard from './hero-dashboard'

import HeroList from './hero-list'

import fetchHeroes from '../utils/fetch-heroes'

import setupRoutes from '../utils/routes' export default class App extends Component {

constructor(props) {

super(props)

this.container = 'section'

this.state = {

activeComponent: 'dashboard',

heroes: [],

selectedHero: '',

searchResults: []

}

}

// Update to render either the dashboard or hero list:

render(state) {

return (

{

state.activeComponent === 'dashboard' &&

<HeroDashboard

heroes={this.state.heroes}

search={this.search.bind(this)}

searchResults={this.state.searchResults}

blurSearchInput={this.blurSearchInput.bind(this)} />

}

{

state.activeComponent === 'heroes' &&

<HeroList

heroes={this.state.heroes}

deleteItem={this.deleteItem.bind(this)}

addHero={this.addHero.bind(this)} />

}

{

state.activeComponent === 'detail' &&

<HeroDetail

hero={this.state.selectedHero} deleteItem={this.deleteItem} onHeroNameChange={this.onHeroNameChange.bind(this)} resetName={this.resetName.bind(this)} saveName={this.saveName.bind(this)} />

}

)

} componentDidMount() {

// Fetch data for heroes:

fetchHeroes()

.then(heroes => {

// Set the heroes to the component's state:

this.setState({heroes})

})

} search(e) {

const input = e.target

const value = input.value

const heroes = this.state.heroes

const searchResults = heroes.filter(hero => {

const name = hero.name.toLowerCase()

return name.match(value.toLowerCase())

})

this.setState({searchResults: searchResults})

} blurSearchInput(e) {

const searchResults = this.state.searchResults

setTimeout(() => {

this.setState({searchResults: []})

}, 250)

} addHero(e) {

const input = e.target.parentNode.querySelector('#add-hero')

const value = input.value

if (value) {

const lastId = this.state.heroes[this.state.heroes.length -1].id

const newHero = {id: String(parseInt(lastId) + 1), name: value}

const heroes = this.state.heroes

heroes.push(newHero)

this.setState({heroes})

input.value = ''

}

} deleteItem(e) {

const id = e.target.dataset.id

const heroes = this.state.heroes

const position = heroes.findIndex(hero => id == hero.id)

heroes.splice(position, 1)

this.setState({heroes})

}

}

To create a new id for a new hero, notice how in the addHero method above we get the id of the last hero in the list and then add one to it. Since the new hero will be appended after that, increase the id by one will work here. If you are add items to a list and need new keys, it's best to use a more powerful uuid function for that. There are a few available on NPM.

To delete a hero, we get the id that was output as a dataset. Then we find out what its index is the the heroes array in the state. Then we splice that and update the component’s state.

Changing or Resetting a Hero’s Name

Lastly, we need to update the detail component so that the user can modify a hero’s name and save it, or reset it to the original name. To do this we need to modify the hero-detail component by adding an input and some buttons.

import {h, Component} from 'composi' export default function HeroDetail({hero, onHeroNameChange, resetName, saveName}) {

return (

<div id='hero-detail'>

<h2>{hero.name} details!</h2>

<div><label>id:</label> {hero.id}</div>

<div>

<label for='update-name'>name: </label>

<input id='update-name' placeholder={hero.name} oninput={(e) => onHeroNameChange(e)} />

</div>

<p class='hero-detail--buttons'>

<button onclick={resetName}>Reset</button>

<button onclick={saveName}>Save</button>

</p>

</div>

)

}

As you can see, we’ve also add several new props that will need to be passed to the list: onHeroNameChange , resetName , saveName . All three are event handlers. These will need to be added to the App component and passed to the list component.

import {h, render, Component} from 'composi'

import HeroDashboard from './hero-dashboard'

import HeroList from './hero-list'

import fetchHeroes from '../utils/fetch-heroes'

import setupRoutes from '../utils/routes' export default class App extends Component {

constructor(props) {

super(props)

this.container = 'section'

this.state = {

activeComponent: 'dashboard',

heroes: [],

selectedHero: '',

searchResults: []

}

}

// Update to render either the dashboard or hero list:

render(state) {

return (

{

state.activeComponent === 'dashboard' &&

<HeroDashboard

heroes={this.state.heroes}

search={this.search.bind(this)}

searchResults={this.state.searchResults}

blurSearchInput={this.blurSearchInput.bind(this)} />

}

{

state.activeComponent === 'heroes' &&

<HeroList

heroes={this.state.heroes}

deleteItem={this.deleteItem.bind(this)}

addHero={this.addHero.bind(this)} />

}

{

state.activeComponent === 'detail' &&

<HeroDetail

hero={this.state.selectedHero}

deleteItem={this.deleteItem}

onHeroNameChange={this.onHeroNameChange.bind(this)}

resetName={this.resetName.bind(this)}

saveName={this.saveName.bind(this)} />

}

)

} componentDidMount() {

// Fetch data for heroes:

fetchHeroes()

.then(heroes => {

// Set the heroes to the component's state:

this.setState({heroes})

})

} search(e) {

const input = e.target

const value = input.value

const heroes = this.state.heroes

const searchResults = heroes.filter(hero => {

const name = hero.name.toLowerCase()

return name.match(value.toLowerCase())

})

this.setState({searchResults: searchResults})

} blurSearchInput(e) {

const searchResults = this.state.searchResults

setTimeout(() => {

this.setState({searchResults: []})

}, 250)

} addHero(e) {

const input = e.target.parentNode.querySelector('#add-hero')

const value = input.value

if (value) {

const lastId = this.state.heroes[this.state.heroes.length -1].id

const newHero = {id: String(parseInt(lastId) + 1), name: value}

const heroes = this.state.heroes

heroes.push(newHero)

this.setState({heroes})

input.value = ''

}

} deleteItem(e) {

const id = e.target.dataset.id

const heroes = this.state.heroes

const position = heroes.findIndex(hero => id == hero.id)

heroes.splice(position, 1)

this.setState({heroes})

} onHeroNameChange(e) {

const value = e.target.value

if (value) {

const selectedHero = this.state.selectedHero

selectedHero.name = value

this.setState({selectedHero})

}

} resetName(e) {

const selectedHero = this.state.selectedHero

selectedHero.name = selectedHero.originalName

this.setState({selectedHero})

} saveName(e) {

window.location.hash = '#/heroes'

}

}

onHeroNameChange updates the value of the hero's name in the component's state object. The resetName method gets the original name passed to it by the detail route. If you remember, when we set up the route for the detail view, we also set a property on the hero and set the component state with it:

// Capture the value after `detail/` to determine which hero was chosen:

'/detail/:id': function(id) {

const state = app.state

const position = state.heroes.findIndex(person => person.id === id)

const hero = state.heroes[position]

// Capture the oringal name.

// We can use this to reset the name if the user chooses.

hero.originalName = hero.name

app.setState({activeComponent: 'detail', selectedHero: hero})

}

saveName just needs to leave the current edit as is and return the useer back to the heroes list.

And, here’s the final Tour of Heroes example on Codepen:

Summary

With this final edition, we have completed the Tour of Heroes. Using the patterns of JSX function components, props a class-based component and client-side routing to conditionally render subcomponents, we were easily able to reproduce the Angular Tour of Heroes, and in much less code. Because Composi shares many patterns with React, a developer familiar with it should be able to convert this to React with minor changes here and there.

Source Code

You can get the source code for this from Github: https://github.com/composor/tour-of-heroes