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
https://github.com/creationix/nvm
curl https://raw.githubusercontent.com/creationix/nvm/v0.24.0/install.sh | bash
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 Yeoman and Gulp
npm install -g yo gulp jsmp
Follow http://rethinkdb.com/docs/install/
npm install -g koa
See https://github.com/zewa666/generator-aurelia
npm install -g generator-aurelia
Follow this guide http://aurelia.io/get-started.html
$ mkdir navigation-app && cd navigation-app
$ yo aurelia
$ npm install
$ jspm install -y
Setup watcher
$ gulp watch
See Live app in the browser: http://localhost:9000/
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.
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!
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>
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
$ 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)
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)
Aurelia uses YUIdoc (see http://yui.github.io/yuidoc/)
Clone repo from https://github.com/jdanyow/aurelia-breeze-sample (or fork at @kristianmandrup)
$ git clone [email protected]:jdanyow/aurelia-breeze-sample.git
$ npm install && jspm install
$ gulp watch
See app in browser
$ open localhost:9001
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 :)
This was written when
io.js
was all the type, before the merge back withnode.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!!!