Writing your first Ember app, part 1
Get started
Install ember using Ember CLI -- Ember is similar to Django by making a lot of decisions for you
Create a new project by doing ember new mysite
-- Like Django, Ember has helper tools to kickstart a project. This will take some time, because npm
will go and download half the internet for you.
The development server
ember serve
will start a nice auto-reloading server. Note when you install a new add-on you have to manually restart it. --The similarities with Django continue
Creating the Polls app
Ember doesn't really have the notion of separate apps, so we'll skip this.
Write your first view
Here's where the similarities start to break down. Django, being a server-side framework, has a relatively simple architecture:
Request
comes in- Passes through some middleware
- Use the URL to try to find a handler (could be a function or a class)
- The handler processes the request, returns a
Response
- Middleware
- Response gets served to the client.
Ember on the other hand, being client-side has a more desktop app feel to it, in the sense that there's a lot of (unavoidable, to some extent) state going on (and a run loop, somewhere deep down). Ember tries to organize this state so it's easier to reason about:
- The app loads (once)
- Use the URL to extract a
Route
- Load the route, load its model, setup the (singleton) controller, render the template
- Process events that update state, refresh the UI to reflect new state
- Repeat previous step (and transition/redirect to new Routes as needed)
The closest thing therefore to Django's view is a Route.
Note: Ember tries (too hard, IMO) to go "convention-over-configuration" so at some point it does seem like magic. When you start an empty project, and visit the page as served by ember serve
in the browser, you'll see a page saying "Welcome to Ember". It follows that a Route must be there somewhere, but the routes
directory is empty. Ember will often "auto-generate" missing classes for you, and here it auto generated the application
Route which is the "root" of your application. Indeed if you see the templates
folder, you'll find application.hbs
which is what gets rendered. You can ember generate route application
to make this explicit.
To follow the Django tutorial, we need to create a Route under /polls
. This is generated for us easily by doing ember g route polls
. Note: Ember also is fussy about naming things. Stick to plural nouns with the routes.
Edit the templates/polls.hbs
template and open /polls
in the browser. You should see the contents of your template. Observe how as you edit the template the page auto reloads.
part 2
Database setup
Obviously Ember.js doesn't have a database component. Instead it has Ember Data which is an "ORM-like" component. As in Django, it is completely optional and you could use whatever you want in there.
The idea is that you have a client-side Store
that you query for records. The store delegates the actual work to an Adapter
that connects to your actual back-end. It also maintains its own cache (which you can bypass by doing explicit reloads or background reloads).
Note: Here's another big deviation from Django (or any server-side framework). Because of the split concerns, there's going to be duplication of concerns. You do client-side validation, then you have to do server-side validation. You save a record on the client, and you save it on the server. Any error on the server should be propagated to the client, and any background state change in the server should be displayed in the client as well. It's a mess that I believe still hasn't been resolved cleanly. The most promising area there is a client-side framework that integrates tightly with a server-side backend (so probably something in Node.js) but I'm not familiar with the landscape to know if this is going to happen.
I've documented how to setup a Django-specific backend for the built-in JSONAPIAdapter of Ember.js. You should probably follow that guide and come back here when the API is working.
Let's create the models by doing ember g model question
and ember g model choice
-- routes are plural, models are singular
Edit the two new files:
// models/question.js
export default DS.Model.extend({
questionText: DS.attr('string'),
pubDate: DS.attr('date'),
choices: DS.hasMany('choice')
});
// models/choice.js
export default DS.Model.extend({
choiceText: DS.attr('string'),
votes: DS.attr('number'),
question: DS.belongsTo('question')
});
Note: Javascript/Ember convention dictates camelCase for fields.
Obviously there is no database involved so far, but you can install the Ember Inspector Google Chrome extension to get a console of sorts.
Go to routes, find any route, click on the $E and you will have it in the console under $E
.
var q = $E.store.createRecord('question');
q.set("questionName", "What is your name?");
q.set("pubDate", new Date());
q.toString(); // UGLY!
q.toJSON(); // observe how Date is serialized to a string.
Note: You must use these clunky get/set calls instead of doing normal JS property access. The worst thing is that proper property access will still work but you will get weird bugs as soon as you use other helpful Ember features like computed properties.
The toString
produces some ugly stuff. You can either override in the model entirely, or add this:
toStringExtension: function() {
return this.get('questionText');
}
Now the toString will have a last component which can help you identify your records a bit better.
Let's add the wasPublishedRecently
method too:
wasPublishedRecently: function() {
var p = this.get('pubDate');
var n = new Date();
var diffInMillis = n - p;
var dayInMillis = 24 * 60 * 60 * 1000;
return diffInMillis > dayInMillis;
}
Let's use the console to add a couple of choices:
var c1 = $E.store.createRecord('choice');
c1.set("choiceText", "Brave Sir Robin");
c1.set("votes", 1);
c1.set("question", q); //from our previous session
var c2 = $E.store.createRecord('choice');
c2.set("choiceText", "Lancelot the Pure");
c2.set("votes", 2);
c2.set("question", q); //from our previous session
q.get("choices").then(function (c){console.log(c.length)}); // prints 2
c2.deleteRecord(); //mark as deleted, don't push to backend yet
q.get("choices").then(function (c){console.log(c.length)}); // prints 1
Note: While Django relations are synchronous, Ember Data relationships are async, meaning when you do q.get("choices")
you get a promise back that you have to attach a callback on. This is annoying as well, because the attributes themselves are not promises so you will have to use both ways of accessing a model in your code. Templates, however, natively support this async nature of relations.
Introducing the Django Admin
Here's where ember completely lacks anything like the Django Admin. There might be add-ons out there but nothing really complete. Good thing you can keep using the native Django admin if you use a Django back-end!
Part 3, creating the public interface
The Django tutorial creates 3 similar views - show a question, see results, vote on a question. They are all mapped under a dynamic "question_id". For ember, we will create those as routes:
ember g route polls/view --path /:question_id
ember g route polls/vote --path /:question_id/vote
ember g route polls/results --path /:question_id/results
Because of Ember's magic and a side effect of how "nested routes" work, we are going to need to also explicitly generate a previously auto-generated Route: the index
:
ember g route polls/index --path /
So our router.js
file should look like this:
// router.js
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
this.route('polls', function() {
this.route('index', {
path: '/'
});
this.route('view', {
path: '/:question_id'
});
this.route('vote', {
path: '/:question_id/vote'
});
this.route('results', {
path: '/:question_id/results'
});
});
});
export default Router;
It is conceptually similar to the Django urls.py
: you define a parent namespace, polls
and then inside you created nested paths, with some dynamic element (:question_id) in particular.
Let's go and edit the auto-generated templates and try to navigate some urls:
<!-- templates/polls.hbs -->
Polls namespace
{{outlet}}
<!-- templates/polls/index.hbs -->
INDEX
<!-- templates/polls/view.hbs -->
VIEW
<!-- templates/polls/vote.hbs -->
VOTE
<!-- templates/polls/results.hbs -->
RESULTS
Then try to navigate to /polls
, /polls/1
, /polls/1/vote
, and /polls/1/results
. You should see "Polls namespace INDEX" and so on. This means that we got our URL handling correct, and for our polls
namespace we now have 4 different Routes we can customize.
Writing views that actually do something
Ember Routes have a special model
method that you use to get a model and pass it to the template for rendering. Ember does some introspection to make this appear magic, but I don't like magic so I prefer to define this explicitly. Let's first do the index:
// routes/polls/index.js
export default Ember.Route.extend({
model() {
return this.store.findAll('question');
}
});
<!-- templates/polls/index.hbs -->
{{#if model}}
<ul>
{{# each model as |question| }}
<li>
{{#link-to "polls.view" question.id }} {{ question.questionText }} {{/link-to}}
</li>
{{/each}}
</ul>
{{else}}
<p>No polls are available.</p>
{{/if}}
Django here constructs link manually (although it replaces that with the url
template tag later on). You could do the same in Ember but this would incur a full app reload, so you instead use the link-to
helper, passing it the name of the Route (polls.view
) and the parameters to load a model question.id
.
Let's also update the polls.view
route:
// routes/polls/view.js
export default Ember.Route.extend({
model(params) {
return this.store.findRecord('question', params.question_id);
}
});
<!-- templates/polls/view.hbs -->
<h1>{{ model.questionText }}</h1>
<ul>
{{#each model.choices as |choice| }}
<li>{{ choice.choiceText }}</li>
{{/each}}
</ul>
As you can see, the fact that model.choices
is an async call doesn't matter as the templates are smart enough to update when the promise actually resolves.
Part 4, allowing visitors to vote
The Django tutorial here defines a form to allow to send data back to the server. In our case we don't really need that, but instead we'll use "actions". Edit the view route:
<!-- templates/polls/view.hbs -->
<li>{{ choice.choiceText }} -- {{choices.votes}} vote(s) <button {{action "vote" choice}}>Vote!</button></li>
// routes/polls/view.js
export default Ember.Route.extend({
model(params) {
return this.store.findRecord('question', params.question_id);
},
actions: {
vote(choice) {
console.log("VOTING");
console.log(choice.toJSON());
choice.incrementProperty('votes');
choice.save();
}
}
});
Now how everything updates almost immediately - however the results are also saved in the backend as well. Try editing the various records in Django admin and updating the page to see how everything is connected.
Welp, it seems we don't need the other routes at all! This is something that will happen a lot of times when moving from "server-side" to "client-side" thinking. In Django, you had to define URLs and views for pretty much every action you had to do. In Ember, you define URLs only if conceptually the view changes enough to warrant a new URL (or if you can imagine a visitor would like to bookmark this specific page/view for later).
Go ahead and delete the previously defined routes:
ember destroy route polls/vote
ember destroy route polls/results
Part 5, testing
Just visit /tests
to run your automatically generated tests. I am not well versed on the approaches to testing in Ember, but again batteries are included so you could just dive in writing the tests in the pre-generated files Ember CLI gives you.