Ember.js ist ein Javascript-Framework zum Erstellen von Single-Page-Apps, die sich immer größerer Beliebtheit erfreuen. Andere prominente Vertreter dieser Technologie sind Backbone.js und AngularJS. Der aus der Rails-Welt bekannte Entwickler Yehuda Katz ist der Schöpfer dieses Projekts und einer der Entwickler.
Es ist ursprünglich aus dem SproutCore-Framework entstanden und hat seit Beginn dieses Jahres den 1.0 Release-Candidate-Status. Dadurch wird es in nächster Zeit keine größeren Änderungen mehr an der API geben.
Aktuell zum Zeitpunkt der Veröffentlichung dieses Blog-Eintrags ist die Version
1.2.0
. Ein Starter-Kit zum schnellen Einstieg ist auf der
Ember Webseite
verfügbar. Mit ember-rails ist ebenso
die einfache Verwendung in der Asset-Pipeline von Rails 3.1+-Anwendungen möglich.
Empfehlenswert zum Einstieg und schnellem Testen von Code-Schnippseln ist ebenfalls JSFiddle.
Bestandteile von Ember.JS
Wenn man aus der Rails-Welt kommt, ist ein gewisses Umdenken notwendig, da Ember nicht das gewohnte MVC-Pattern von Ruby on Rails verwendet. Zu bekannten Komponenten wie den Controllern, den Models und den Views kommt zusätzlich das zentrale Konzept des Routers.
Router
Der Router ist verantwortlich für den state der Anwendung: Er sorgt dafür,
dass zu einer gewissen URL ein Model geladen wird und die richtigen Templates
angezeigt werden. Außerdem setzt er das content
-Property des Controllers.
Für einen tieferen Einstieg in das Thema state und Routing empfehle ich den Beitrag Building Web Applications with Ember.js von Yehuda Katz von der diesjährigen HTML5DevConf.
Ein Beispiel für eine Route, die dann über /foo
erreichbar wäre, sieht so aus:
App = Ember.Application.create({
LOG_TRANSITIONS: true
});
App.Router.map(function() {
this.route('test');
});
URLs können entweder mit den bekannten Hash-Tags #
(hashchange-Event des
Browsers) oder mit der History-API erzeugt werden. Das Verhalten kann über das
location
-Property des Routers gesteuert werden:
App.Router.reopen({
location: 'history'
});
Routen können auch mit einer resource
gruppiert
und verschachtelt werden:
App.Router.map(function() {
this.resource('posts', function(){
this.route('about');
})
});
Dadurch werden zwei Routen angelegt:
posts
mit der URL/posts
posts.about
mit der URL/posts/about
Die Pfade sind konfigurabel aber standardmässig entsprechen sie dem Namen der Route.
Ein zentrales Konzept von Ember.JS ist, dass ein verschachteltes User-Interface durch verschachtelte Routen abgebildet werden sollte. Darauf wird auch noch später in den Templates eingegangen.
Model
Das Model beinhaltet die “Nutzdaten” der Anwendung und ist für Persistenz
verantwortlich. Änderungen, die beim Neubauen des DOM noch vorhanden sein
müssen, sind hier vorzunehmen. Das Laden und Speichern von Models übernimmt
Ember-Data. Hierzu muss ein sog. store
konfiguriert werden, der je nach
verwendetem Addapter festlegt, ob und wie Daten zu einer Datenquelle repliziert
werden sollen.
Das Laden von Models mittels find()
funktioniert fast analog zu Rails:
App.PostsRoute = Ember.Route.extend({
model: function() {
var store = this.get('store');
// Posts in den Store laden
store.find('post');
// Nur bereits gespeicherte Posts in der Liste anzeigen
return store.filter('post', function(post){
return !post.get('isNew');
});
}
});
Model-Attribute müssen mit ihrem Typ deklariert werden. Der REST-Adapter kennt
vier verschiedene Attribut-Typen: string
, number
, boolean
und
date
. Diese werden dann über den REST-Adapter und die zugrunde liegende
JSON-API automatisch befüllt. Ein einfaches Model mit zwei Attributen title
und body
sähe beispielsweise so aus:
App.Post = DS.Model.extend({
title: DS.attr('string'),
body: DS.attr('string')
});
Die JSON-Antwort des Servers sollte dementsprechend folgendermaßen aussehen:
{
"post": {
"title": "Ember.js",
"body": "A framework for creating abitious web applications"
}
}
Auf Attribute kann man mit get
zugreifen und mit set
schreiben:
post = store.find('post', 1);
post.get("title"); # -> "Ember.js"
new_title = "Hello, Ember!";
post.set("title", new_title);
Desweiteren kann man sog. computed properties
definieren. Es handelt sich
hierbei um Methoden, die Anhand von Attributen weitere Werte zur Verfügung
stellen und diese auch mittels Bindings aktualisieren, wenn sich die
referenzierten Attribute ändern. Um an das vorherige Beispiel anzuknüpfen,
möchte ich beispielsweise eine Methode teaser
haben, die mir den ersten Satz
des Attributs body
bereitstellt:
App.Post = DS.Model.extend({
title: DS.attr('string'),
body: DS.attr('string'),
teaser: function() {
var body = this.get("body");
if(body) return body.split(".")[0]
}.property("body")
1-zu-N-Beziehungen analog zu ActiveRecord
s has_many
und belongs_to
können
ebenfalls abgebildet werden. Darauf möchte ich hier nicht näher eingehen und es
soll an dieser Stelle auf die
Dokumentation verwiesen
werden.
Store und REST-Adapter
Der Store ist für das Laden und Speichern von Models veranwortlich. Das
Verhalten kann über einen von verschiedenen Adaptern gesteuert werden. Im
Momement stehen dafür durch Ember-Data der FixtureAdapter
und RestAdapter
zur Verfügung.
Der FixtureAdapter
lädt definierte Testdaten in den Speicher,
Änderungen sind demnach nicht persistent. Mit Fixtures kann eine Anwendung
zunächst unabhängig von einem Backend, das später die Daten bereitstellt,
entwickelt werden, daher ist er vor allem in der frühen Entwicklung
hilfreich. Der RestAdapter
beschreibt die JSON-Schnittstelle zu einer
Backend-Anwendung (API). Wir werden uns im folgenden vor allem darauf
konzentrieren.
Um den Fixture-Adapter zu verwenden, wird zunächst der Store konfiguriert:
App.Store = DS.Store.extend({
revision: 15,
adapter: 'App.ApplicationAdapter'
});
Fixtures in einem Model können beispielsweise so hinterlegt werden:
App.Post.FIXTURES = [
{
id: 1,
title: 'Ember.js',
body: 'A framework for creating abitious web applications'
}
]
Controller
Controller bilden in Embers Objektmodell das Zwischenstück zwischen Templates
und Models. Es ist möglich nicht-persistente (im Sinne des
Adapters) Properties auf einem Model zu definieren. Wenn im Template auf ein
Property (zum Beispiel title
eines Artikels) zugegriffen wird, dann wird
zunächst im Controller nach diesem gesucht, und falls dort nicht vorhanden,
direkt im Model.
Ein Controller kann ein einzelnes Model repräsentieren oder ein Array von
Models. Die beiden Superklassen heissen jeweils ObjectController
und
ArrayController
. Das Model des Controllers muss über die Property
setupController
innerhalb einer Route gesetzt werden:
App.PostRoute = Ember.Route.extend({
model: function(params){
return this.store.find('post', params.post_id);
},
setupController: function(controller, post){
controller.set('content', post);
this.controllerFor('posts').set('activePostID', post.get('id'));
}
});
App.PostsNewRoute = Ember.Route.extend({
model: function() {
return this.store.createRecord('post');
},
setupController: function(controller, post) {
controller.set('content', post);
}
});
Für diesen gängigen Fall ist es nicht notwendig das Model und den Controller in der Route festzulegen, da Ember es durch active code generation selbständig erledigt. Das Controller-Objekt wird ebenfalls standardmäßig von Ember erzeugt.
Templates
In Ember wird die Handlebars Template-Engine
verwendet. Damit kann das HTML-Gerüst durch dynamische Ausdrücke erweitert
werden. Handlebars-Befehle werden in geschweifte Klammern gesetzt,
beispielsweise, um über alle Models eines Controllers zu iterieren (siehe
model
-Property im Abschnitt “Router”):
{{#each model}}
{{model.title}}
{{/each}}
Um ein genestetes Template zu rendern, wird analog zum bekannten yield
aus
Rails der Ausdruck outlet
verwendet:
<!-- Sidebar -->
<div class="nav">
<ul>
{{#each model} }
<li>{{model.title}}</li>
{{/each} }
</ul>
</div>
<!-- Content -->
<div class="main">
{ {outlet} }
</div>
Das würde den folgenden Routen entsprechen:
App.Router.map(function() {
this.resource('posts', function(){
this.resource('post', { path: ':post_id' });
})
});
Und damit dem folgenden Layout des Interfaces:
+---------------------------------------------------------------+
| + |
| | |
| Post-Liste | Einzelner Post |
| | |
| (posts-Controller) | (post-Controller) |
| | |
| /posts | /posts/1 |
| | |
| | |
| + |
+---------------------------------------------------------------+
Views und data bindings
Views werden verwendet, um Events (Klicken, etc.) an Controller-Events zu delegieren und wiederverwendbare UI-Bestandteile zu erzeugen.
Zwischen Models und Views (das ist die übliche Konstellation, aber es geht im
Prinzip zwischen allen Objekten) sorgen automatische Bindings von Ember dafür,
dass verknüpfte Properties sich automatisch aktualiseren, wie in folgendem
Beispiel. Wird im Controller ein Model mit dem Attribut name
instaniziert oder
geladen, dann sorgt folgender Template-Snippet dafür, dass das Attribut bereits
beim Schreiben bereits aktualisiert wird.
Weiterführende Links
- Offizieller Ember-Guide
- Ember-API
- Youtube-Video von Dan Gebhardt zum Thema Ember Data und API-Design
- Gepflegtes Ember Data Beispiel, dass eine Rails-API verwendet
- Ember Weekly
- Ember-Inspector für Google Chrome
- Ember Demo-Anwendung mit Node.JS-Backend