Ember.js 1.1 im Einsatz

Routing und Components

Beim Aufrufen einer Ember-Applikation überprüft das Framework anhand der Routen, was gemacht werden soll. Letztere sind per App.Router.map(function){…} zu definieren:

App.Router.map(function(){
this.resource('users', function(){
this.resource('user', { path:'/:user_id' }, function(){
this.route('edit');
});
this.route('create');
});
});

Die unter Ember-Entwicklern beliebte Chrome-Erweiterung Ember Inspector zeigt die Routen übersichtlich an. Mit dem Befehl Ember.keys(App.Router.router.recognizer.names) kann man sie sich alternativ in der JavaScript-Console anschauen (Tipps zum Debuggen finden sich auf der Webseite des Ember-Projekts unter dem Punkt Debugging). Das Definieren und Verwenden sauberer Routen ist einer der Schlüssel für eine gute Ember-Applikation. Das Framework nutzt sie, um zu prüfen, ob ein entsprechender Controller im Code vorhanden ist. Wurde er nicht vom Programmierer erstellt, erzeugt Ember den Controller anhand von Defaults automatisch.

Mit "Convention over Configuration" und einer klaren Namenskonvention erspart man sich so viel Tipparbeit und behält auch in großen Teams leichter den Überblick. Die Index-Route wird per Convention automatisch definiert. Im Beispiel ist sie allerdings mit App.IndexRoute = Em.Route.extend(…) manuell festgelegt und lässt sich dazu nutzen, mit this.transitionTo('users'); eine Transition zur Route users.index durchzuführen. Dort füllt man das Modell mit allen Usern aus dem definierten Store und setzt per activate-Hook aus jQuery der HTML-Titel der Seite:

App.UsersRoute = Em.Route.extend({
model: function(){
return this.store.find('user');
},
activate: function() {
$(document).attr(’title’, ’Listenansicht’);
}
});

Nach der Route geht es in den Controller App.UsersController in dem man mit sortProperties: ['lastName'], sortAscending: true die Datensätze nach dem Nachnamen sortiert. Am Ende der Kette rendert das Skript das Handlebar-Template mit der ID users.

<script type="text/x-handlebars" id="users">
<div class="row">
<div class="col-md-7">
<h2>Listenansicht</h2>
<table class="table">
<thead>
<tr><th>Name</th><th>E-Mail</th><th></th></tr>
</thead>
{{#each user in controller}}
<tr {{bindAttr class="user.isDirty:warning"}}>
<td>{{user.fullName}}</td>
<td>{{user.email}}</td>
<td>{{#link-to "user" user class="btn btn-default"}}
Detailansicht{{/link-to}}</td>
</tr>
{{else}}
<tr><td colspan="3">Kein User vorhanden.</td></tr>
{{/each}}
</table>
<p>
{{#link-to "users.create" class="btn btn-default"}}User anlegen
{{/link-to}}
</p>
</div>
<div class="col-md-5">
{{outlet}}
</div>
</div>
</script>

Der Handlebar Each-Loop {{#each user in controller}} im obigen Beispiel stellt eine else-Verzweigung zur Verfügung. Mit ihr kann man eine Alternativ-Anzeige erstellen – für den Fall, dass kein Nutzer gespeichert ist. Die Einzelansicht eines Users lässt sich mit der URL index#users/1 aufrufen. Auch hier wird als Erstes die entsprechende Route App.UserRoute eingebunden, in der sich mit this.store.find('user', params.user_id) der Datensatz suchen und an den Controller liefern lässt. Am Schluss rendert das Beispiel das Handlebar-Template mit der ID user in das im users Handlebar-Template zur Verfügung gestellte {{outlet}}.

Detailansicht eines einzelnen Datensatzes

Das in der User-Detailansicht angezeigte Avatar-Bild stellt ein sogenannter Component zur Verfügung. Er greift die vom W3C diskutierte Idee selbst definierter HTML-Tags auf und setzt sie mit Handlebar um. Das folgende Codebeispiel zeigt den Quellcode, der den Component {{gravatar-image email=email size=“100“}} erst ermöglicht.

<script type="text/x-handlebars" id="components/gravatar-image">
<img {{bind-attr src=gravatarUrl}} class="img-rounded">
</script>

[...]

App.GravatarImageComponent = Ember.Component.extend({
size: 100,
email: '',

gravatarUrl: function() {
var email = this.get('email'),
size = this.get('size');
return 'http://www.gravatar.com/avatar/'
+CryptoJS.MD5(email) + '?s=' + size;
}.property('email', 'size')
});

Natürlich lassen sich nicht alle Ereignisse einer Webapplikation per URL und damit per Route abbilden. Beim Abspeichern eines gerade editierten Users kommt deshalb eine per Button ausgelöste Aktion im UserEditController zum Einsatz:

App.UserEditController = Em.ObjectController.extend({
actions: {
save: function(){
var user = this.get('model');
user.save();
this.transitionToRoute('user', user);
}
}
});

Wenn man sich an die Namenskonventionen gewöhnt hat, kann man sich schnell in fremden Code einarbeiten und auch selbstgeschriebener Code profitiert letztlich. Der Name eines Controllers beziehungsweise einer Route zieht sich wie ein roter Faden durch die Applikation.