Skip to content

Instantly share code, notes, and snippets.

@kristianmandrup
Last active February 22, 2017 16:15
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save kristianmandrup/e1099f54bbb7f6968af7 to your computer and use it in GitHub Desktop.
Save kristianmandrup/e1099f54bbb7f6968af7 to your computer and use it in GitHub Desktop.
Aurelia Getting started - walk through

Aurelia

Recipe

  • Install NVM
  • Install IO.js
  • Install global Node.js utility modules (gulp, jspm, yo)
  • Install RethinkDB
  • Install Koa.js
  • Install Aurelia generator
  • Create Aurelia app via generator

Install NVM

https://github.com/creationix/nvm

curl https://raw.githubusercontent.com/creationix/nvm/v0.24.0/install.sh | bash

Install IO.js

What is IO.js? https://developer.atlassian.com/blog/2015/01/getting-to-know-iojs/

Go to https://iojs.org/en/index.html, click the IO icon and choose your package. Then follow installation guidelines...

Open a new terminal. Make sure Node is set to use io, by checking Node version Alternatively install using brew:

brew install iojs

To use with Node Version Manager (aka nvm) https://keymetrics.io/2015/02/03/installing-node-js-and-io-js-with-nvm/

nvm install iojs nvm use iojs

$ node -v

Should be higher than 1.0

Install global node packages

Install Yeoman and Gulp

npm install -g yo gulp jsmp

Install RethinkDB

Follow http://rethinkdb.com/docs/install/

Install Koa.js

npm install -g koa

Install Aurelia generator

See https://github.com/zewa666/generator-aurelia

npm install -g generator-aurelia

Create Aurelia navigator app

Follow this guide http://aurelia.io/get-started.html

$ mkdir navigation-app && cd navigation-app

Run the aurelia generator on the project

$ yo aurelia

Install Gulp NPM dependencies

$ npm install

Install Aurelia JSPM dependencies

$ jspm install -y

Run the App

Setup watcher

$ gulp watch

See Live app in the browser: http://localhost:9000/

Refactor app

Extract method configure

In app.js let's extract a configure method from the App constructor.

  constructor(router) {
    this.router = router;
    this.configure();
  }

  configure() {
    this.router.configure(config => {
      config.title = 'Aurelia';
      config.map([
        { route: ['','welcome'],  moduleId: 'welcome',      nav: true, title:'Welcome' },
        { route: 'flickr',        moduleId: 'flickr',       nav: true },
        { route: 'child-router',  moduleId: 'child-router', nav: true, title:'Child Router' }
      ]);
    });    
  }

You should notice that the gulp watcher picks up the change and reloads the app which should work just like before.

Extract NavHeader component

In nav-bar.html let extract the block for <div class="navbar-header"> into its own component. Since it belongs to Navbar, we create a subfolder src/navbar and add a nav-header.html file there.

<template>
  <div class="navbar-header">
    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
      <span class="sr-only">Toggle Navigation</span>
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
    </button>
    <a class="navbar-brand" href="#">
      <i class="fa fa-home"></i>
      <span>${router.title}</span>
    </a>
  </div>
</template>

To use this component from the navbar.html we must import the component and then use it.

<template>
  <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
    <import from='./navbar/nav-header'></import>
    <nav-header router.bind="router"></nav-header>
    ...

We make router available inside the new component by binding it via router.bind="router". We now get an error about a missing file nav-header.js. Aurelia expects every component to consist of a html file with the template (layout) and a js file of the same name which contains a class with the functionality.

We need to create a file src/navbar/nav-header.js

import {Behavior} from 'aurelia-framework';

export class NavHeader {
  static metadata(){ return Behavior.withProperty('router'); }
}

Now everything should play nicely again!

Nav-collapse component

We can componentize navbar-collapse this way as well...

<template>
  <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
  ...

The navbar-collapse.js component

import {Behavior} from 'aurelia-framework';

export class NavCollapse {
  static metadata(){ return Behavior.withProperty('router'); }
}

The navbar-collapse.html component layout

<template>
  <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
    <ul class="nav navbar-nav">
      <li repeat.for="row of router.navigation" class="${row.isActive ? 'active' : ''}">
        <a href.bind="row.href">${row.title}</a>
      </li>
    ...

Note that repeat.for="row of router.navigation" iterates through each row of router.navigation to display navigation (menu) options!

The navigation property on the router is an array populated with all the routes you marked as nav:true in your route config.

Also on the li you can see a demonstration of how to use string interpolation to dynamically add/remove classes. Further down in the view, there's a second ul. See the binding on its single child li? if.bind="router.isNavigating" This conditionally adds/removes the li based on the value of the bound expression. Conveniently, the router will update its isNavigating property whenever it is navigating.

Completing the wiring in navbar.html

  <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
    <import from='./navbar/nav-header'></import>
    <import from='./navbar/nav-collapse'></import>

    <nav-header router.bind="router"></nav-header>
    <nav-collapse router.bind="router"></nav-collapse>

Testing

See http://blog.durandal.io/2015/02/16/end-to-end-testing-with-aurelia-and-protractor/

Install phantomjs headless browser

$ brew install phantomjs

Open up another console and run the E2E gulp task:

$ gulp e2e

It might first update the selenium and chromedrivers

Updating selenium standalone
downloading https://selenium-release.storage.googleapis.com/2.45/selenium-server-standalone-2.45.0.jar...
Updating chromedriver

All tests should pass!

Finished in 8.495 seconds
4 tests, 4 assertions, 0 failures

Unit tests

$ karma start

Karma should now open a new browser and connect a socket to display test output. In our case all tests should pass!

INFO [karma]: Karma v0.12.31 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 41.0.2272 (Mac OS X 10.10.2)]: Connected on socket dja5tBmoLjUlDLIeVIxD with id 8112027
Chrome 41.0.2272 (Mac OS X 10.10.2): Executed 12 of 12 SUCCESS (0.01 secs / 0.006 secs)

Breaking a test

In test/unit/app.spec.js change the toEqual value.

  it('configures the router title', () => {
    expect(sut.router.title).toEqual('Aurelia2');
  });

This should automatically trigger a re-run of the tests and an error:

INFO [watcher]: Changed file "/Users/kristianmandrup/repos/aurelia-projects/navigation-app/test/unit/app.spec.js".
Chrome 41.0.2272 (Mac OS X 10.10.2) the App module configures the router title FAILED
	Expected 'Aurelia' to equal 'Aurelia2'.
	    at Object.<anonymous> (/Users/kristianmandrup/repos/aurelia-projects/navigation-app/test/unit/app.spec.js:51:36)

Documentation

Aurelia uses YUIdoc (see http://yui.github.io/yuidoc/)

Aurelia with Breeze

Clone repo from https://github.com/jdanyow/aurelia-breeze-sample (or fork at @kristianmandrup)

$ git clone git@github.com:jdanyow/aurelia-breeze-sample.git

$ npm install && jspm install

$ gulp watch

See app in browser

$ open localhost:9001

Disecting the Breeze app

In main.js we see how the aurelia-breeze plugin is "plugged in" to our app

export function configure(aurelia) {
  aurelia.use
    .plugin('aurelia-breeze')

Notice the Views placeholder <router-view> in app.html. This is where a (efault) router view is rendered

  <div class="page-host">
    <router-view></router-view>
  </div>

In app.js we see the following route config:

      config.map([
        { route: ['','repos'], moduleId: 'repos/repos', nav: true, title: 'Repositories' },
        { route: 'members', moduleId: 'members/members', nav: true, title: 'Members' },
      ]);

In the repos/repos.html component we see reference to aureliaRepos.

          <a repeat.for="repo of aureliaRepos.repos | sort:'stargazers_count':'number':'descending'"
             href.bind="'#repos/' + repo.name"
             class="list-group-item">

Which is configured in repos.js

import {Router} from 'aurelia-router';
import {AureliaRepos} from './aurelia-repos';

export class Repositories {
  static inject() { return [Router, AureliaRepos]; }

  constructor() {
    ...
    this.aureliaRepos = aureliaRepos;
  }

  activate() {
    return this.aureliaRepos.ready;
  }
}

Now we can look at the imported file aurelia-repos.js. We see that it creates an Entity Manager and a breeze.EntityQuery which is executed (a Promise set to instance variable ready).

import breeze from 'breeze';
import {createEntityManager} from '../github';

export class AureliaRepos {
  constructor() {
    var entityManager = createEntityManager(),
        query = breeze.EntityQuery.from('orgs/aurelia/repos').toType('Repository');

    this.repos = [];

    this.ready = entityManager.executeQuery(query)
      .then(queryResult => {
        this.repos = queryResult.results;
      });
  }
}

We follow the rabbit further down the rabbit hole to github.js. Here we see the breeze.EntityManager used being set up to use a dataservice for the github API.

var dataService = new breeze.DataService({
      serviceName: 'https://api.github.com/',
      hasServerMetadata: false 
  }),
      
  entityManager = new breeze.EntityManager({ dataService: dataService }),
  ... 

export function createEntityManager() {
  return entityManager.createEmptyCopy();
}  

Note that hasServerMetadata: false means that we are not getting the metadata from the server and thus must configure it on our end ourselves... The Metadata (Schema) configuration is done as follows:

  memberTypeConfig = {
    shortName: 'Member',

    dataProperties: {
      id: { dataType: breeze.DataType.Int64, isPartOfKey: true },
      login: { /* string type by default */ },
      avatar_url: { },
      ...
      type: { },
      site_admin:  { dataType: breeze.DataType.Boolean }
    }  
  },

  memberType = new breeze.EntityType(memberTypeConfig),

In the data-form.js we see the following:

import {Behavior} from 'aurelia-framework';

export class DataForm {
  static metadata(){ return Behavior.withProperty('entity', 'entityChanged'); } //, Behavior.withProperty('submit')]; }

  constructor() {
    this.dataProperties = [];
  }

  entityChanged() {
    if (this.entity) {
      this.dataProperties = this.entity.entityType.getProperties().filter(p => p.isDataProperty);
      //this.dataProperties.foreach(p => p.test = 'form-control');
    } else {
      this.dataProperties = [];
    }
  }
}  

Notice the line Behavior.withProperty('entity', 'entityChanged'). Here we attach a behavior to the DataForm element. Attached behaviors "attach" new behavior or functionality to existing HTML elements by adding a custom attribute to your markup. Attached Behaviors tend to represent cross-cutting concerns. See http://aurelia.io/docs.html#attached-behaviors

withProperty(x,y,[z]) Creates a BehaviorProperty that tells the HTML compiler that there's a specific property on your class that maps to an attribute in HTML. The first parameter of this method is your class's property name. The last parameter is the attribute name, which is only required if it is different from the property name (ie. optional). The second parameter optionally indicates a callback on the class which will be invoked whenever the property changes.

We see the callback method entityChanged() which is called anytime the entity instance variable of the DataForm instance is changed.

Looking closer at data-form.html we can see how it is wired together:

<input type="checkbox"
       if.bind="property.dataType.name === 'Boolean'"
       checked.bind="$parent.entity[property.name]" />
...
<input class="form-control" type="${property.dataType.isNumeric ? 'number' : 'text'}"
       if.bind="property.dataType.name !== 'Boolean'"
       id.bind="property.name"
       placeholder.bind="'Enter ' + property.name"
       value.bind="$parent.entity[property.name]" />
</div>

Notice the checked.bind="$parent.entity[property.name]" and value.bind="$parent.entity[property.name]". The $parent references the parent, ie. the data-form element with the entity instance variable which triggers on change. Sophisticated data binding at work!

We can see this infrastructure at play in repo.html and member.html which both bind a value to entity of the data-form component which they both leverage!!

  <data-form entity.bind="repo" submit.bind="submit" /> 

Sweet :)

@tvld
Copy link

tvld commented Feb 26, 2016

Like what I am reading. Just wondering about your choice for io.js (over node? ) and RethinkDB (over Mongo) ... ?

@kristianmandrup
Copy link
Author

This was written when io.js was all the type, before the merge back with node.js. Also RethinkDB has built in change feeds over web sockets. Not sure if Mongo DB provides that yet or requires custom add-ons/solutions. Feel free to improve this!!!

@kristianmandrup
Copy link
Author

Also the breeze-sample is deprecated for https://github.com/jdanyow/aurelia-breeze-northwind

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment