I have been playing with Ember in and out since the beginning of 2012, you may have seen my two (obsolete?) libraries ember-facebook and ember-formbuilder. I wrote them out of real needs I had on projects back then.
I was away from ember since version 0.9.8 and ember-data I don’t even remember if it had a version number yet, and I started playing around with it again this last few weeks.
Right now there are not a lot decent resources of getting started with ember and unfortunately some of them are outdated. I am sure that this will get better and better now that the framework is stabilizing.
Anyway, I am working in a little project with a friend and we decided to use ember on it. First thing we implemented was authentication. This is a very simple problem with several solutions. We want to use devise. A very simple one is to rely on the devise’s engine and just make it a separate part of the app, after that you render the user’s session JSON on a metatag and parse it in ember. There is a very nice tutorial on how to do that by Alexander Zaytsev here.
Alternatively you can implement all the interface yourself and have a truly single page application, and it is not that hard. First thing you have to do is turn devise into an API by creating a few controllers. I am just going to show sign-up and login here.
Turning Devise into and API
For sign up let’s create our own users controller to handle registrations:
class UsersController < ApplicationController
def create
user = User.new(params[:user])
if user.save
render json: user, status: :created
else
respond_with user
end
end
end
It’s very simple, an API endpoint to create users using the devise’s user model and respond with the JSON for that user (you may or may not be using active_model_serializers here, but you should be).
Similarly we create a sessions controller to handle, well, sessions:
class SessionsController < ApplicationController
def create
user = User.find_for_database_authentication(email: params[:session][:email])
if user && user.valid_password?(params[:session][:password])
sign_in user
render json: {
session: { id: user.id, email: user.email }
}, status: :created
else
render json: {
errors: {
email: "invalid email or password"
}
}, status: :unprocessable_entity
end
end
def destroy
sign_out :user
render json: {}, status: :accepted
end
end
This one is longer, but it is also very simple: we find the user, we validate whether the password is correct and we then respond with either the basic user data and a successful status or an error message to be shown by ember with an errors status.
We are also adding the destroy action for loggout.
Don’t forget that you need to add theses to your routes.rb:
resources :users, only: [:create]
resources :sessions, only: [:create, :destroy]
Models
We are going to need two Ember models for this, one for our registration and another for the sessions, models in ember-data are extremely simple and there is not a whole lot to say about them.
App.User = DS.Model.extend({
email: DS.attr('string'),
password: DS.attr('string'),
passwordConfirmation: DS.attr('string')
});
App.Session = DS.Model.extend({
email: DS.attr('string'),
password: DS.attr('string')
});
That is already enough for us to play around in the console and create users/sessions:
var user = App.User.createRecord({email: '[email protected]', password: 'password', password_confirmation: 'password'});
user.save();
var session = App.Session.createRecord({email: '[email protected]', password: 'password'});
session.save();
Routes
Let’s define our routes:
App.Router.map(function() {
this.resource('users', function() {
this.route('new');
});
this.resource('sessions', function() {
this.route('new');
this.route('destroy');
});
});
The first two routes we have to add are simple and are just setting the content properties of their controllers. The destroy route is slightly more complicated:
App.UsersNewRoute = Ember.Route.extend({
model: function() {
return App.User.createRecord();
},
setupController: function(controller, model) {
controller.set('content', model);
}
});
App.SessionsNewRoute = Ember.Route.extend({
model: function() {
return App.Session.createRecord();
},
setupController: function(controller, model) {
controller.set('content', model);
}
});
App.SessionsDestroyRoute = Ember.Route.extend({
enter: function() {
var controller = this.controllerFor('currentUser');
controller.set('content', undefined);
App.Session.find('current').then(function(session) {
session.deleteRecord();
controller.store.commit();
});
this.transitionTo('index');
}
});
This destroy route does not have a controller or view (at least not an explicit one), it basically unsets the content of the currentUser
controller (check next section) then issue a delete request on the sessions controller of our API and redirect the user back to the root. Note that we are “finding” the session using the id of “current”, ember-data does not support singletons explicitly, so we have to work around that fact by doing that. You can read more about this here.
Current User
This part is based on the post I mentioned earlier by Alexander Zaytsev. It’s an object controller that will hold the existence (or not) of the current user session.
App.CurrentUserController = Ember.ObjectController.extend({
isSignedIn: function() {
return this.get('content') && this.get('content').get('isLoaded');
}.property('content.isLoaded')
});
We’ll also need this initializer to populate it once the app is loaded.
Ember.Application.initializer({
name: 'currentUser',
initialize: function(container) {
var store = container.lookup('store:main');
var user = App.User.find('current');
container.lookup('controller:currentUser').set('content', user);
container.typeInjection('controller', 'currentUser', 'controller:currentUser');
}
});
Here we’re asking or API for the current user with the same singleton strategy App.User.find('current')
, the app is supposed to return either a successful message with the user JSON or an error. We now need a show
endpoint in our users controller, as simple as respond_with current_user
, don’t forget to add the show route in your resource on routes.rb.
Templates
We need two things here, we need a sign up form and a login form. Another thing we may want is navigation links. Let’s start with the forms. There is no point in logging-in if you don’t have an account right? So let us sign up!
<h1>Create an Account</h1>
<form class="form-horizontal user-form">
<fieldset>
<div class="control-group" {{bindAttr class="errors.email:error"}}>
{{view Ember.TextField valueBinding='email' name='email' placeholder='Email'}}
<span class="help-inline">
{{errors.email}}
</span>
</div>
<div class="control-group" {{bindAttr class="errors.password:error"}}>
{{view Ember.TextField type="password" valueBinding='password' placeholder='Password'}}
<span class="help-inline">
{{errors.password}}
</span>
</div>
<div class="control-group" {{bindAttr class="errors.passwordConfirmation:error"}}>
{{view Ember.TextField type="password" valueBinding='passwordConfirmation' placeholder='Password Confirmation'}}
<span class="help-inline">
{{errors.passwordConfirmation}}
</span>
</div>
<a href='#' {{action cancel}} class='btn'>Cancel</a>
<button type="submit" {{action save}} class='btn btn-large btn-primary'>Sign Up</button>
</fieldset>
</form>
Assuming you’re on rails and using the standard ember gems you should put this file in app/assets/javascripts/templates/users/new.hbs
, ember will automatically pick it up if you follow it’s defaults.
I am using a bootstrap form structure here to show the inputs and error messages. Ember-data will automatically populate the errors property for you if you’re API is returning the right thing, which it is.
And our Login form:
<h1>Login</h1>
<form class="form-horizontal user-form">
<fieldset>
<div class="control-group" {{bindAttr class="errors.email:error"}}>
{{view Ember.TextField valueBinding='email' name='email' placeholder='Email'}}
<span class="help-inline">
{{errors.email}}
</span>
</div>
<div class="control-group" {{bindAttr class="errors.password:error"}}>
{{view Ember.TextField type="password" valueBinding='password' placeholder='Password'}}
</div>
<a href='#' {{action cancel}} class='btn'>Cancel</a>
<button type="submit" {{action save}} class='btn btn-large btn-primary'>Login</button>
</fieldset>
</form>
This files goes into app/assets/javascripts/templates/sessions/new.hbs
.
Very similar to the sign up form, in this case we’re only showing errors on the email field because we’re not specifying whether the email or password was wrong, sometimes you may want to do that, this is not our case here.
There you have it, a very simple sign up and login for that will just work. Right? No, we still have to define our actions, but we’re getting there. Note that we defined in both forms {{action save}}
in our submit buttons, now we need our controllers to handle that.
Oh, and you may also want to add these navigation links somewhere:
{{#if currentUser.isSignedIn}}
Logged in as {{currentUser.email}}
{{#linkTo 'sessions.destroy'}}Logout{{/linkTo}}
{{else}}
{{#linkTo 'users.new'}}Sign Up{{/linkTo}} |
{{#linkTo 'sessions.new'}}Login{{/linkTo}}
{{/if}}
Controllers
Currently we have defined one controller that is the currentUser controller to control the active session, we now need to define controllers to react on actions on our forms, once again we’ll start with the signup form:
App.UsersNewController = Ember.ObjectController.extend({
save: function() {
var self = this;
this.content.save().then(function() {
self.transitionToRoute('index');
});
},
cancel: function() {
this.content.deleteRecord();
this.transitionToRoute('index');
}
});
When the user hits ‘save’ we tell the model to save itself this.content.save()
and then we redirect the user to the root when we’re done. Everything else is being handled by ember and ember-data here behind the scenes, validation errors will automatically update the bindings in the template and will NOT call the ‘then’ callback.
When the user hits ‘cancel’ we just clean up the model this.content.deleteRecord()
and redirect back to the root.
The sessions controller is a bit more complicated, let’s take a look:
App.SessionsNewController = Ember.ObjectController.extend({
needs: ['currentUser'],
save: function() {
var self = this;
this.content.save().then(function() {
var userJSON = self.content.toJSON();
userJSON.id = 'current';
var object = self.store.load(App.User, userJSON);
var user = App.User.find('current');
self.get('controllers.currentUser').set('content', user);
self.transitionToRoute('index');
});
},
cancel: function() {
this.content.deleteRecord();
this.transitionToRoute('index');
}
});
The ‘cancel’ action here is exactly the same as before, so let’s talk about saving. The first step is the same, we ask ember-data to ‘save’ our model this.content.save()
and we hook into the ‘then’ callback. Since our create session endpoint is return a user’s JSON we’re gonna use it to populate the currentUser controller. At this point self.content.toJSON()
will have the user’s email, but it may have more information as you application needs grow. We have to manually set the ‘id’ property on that JSON object since ember-data will not serialize it. We set it to ‘current’ to refer to our singleton currentUser.
And then we finally load that into a User
object using the ember-data store. We then set the content of our currentUser controller to be that loaded user: self.get('controllers.currentUser').set('content', user)
.
And there we have it, if everything is wired correctly we should have a working signup/login/logout app. I hope it was as helpful for you as it was for me.
References
- Alexander Zaytsev – Using Rails & Devise with Ember.js
- Brian Cardarella – Building an Ember app with RailsAPI
- Jesse Wolgamott – API JSON authentication with Devise
Caveats
1. I had to add this line to my devise.rb file inorder to be able to get rid of the devise_for :users
on the routes.rb file. That’s because devise won’t provide you with the dynamic helpers like current_user or user_signed_in? if you don’t call add_mapping, and devise_for calls that method underneath the hood.
config.add_mapping :users, {}
2. This strategy relies on cookies and sessions, if you’re going with this instead of using a auth token you should make sure your app is secure by having protect_from_forgery
on. Then you will need to hack into ember-data’s ajax adapter to send the csrf-token on every request. You can achieve that with something like this:
$(function() {
var token = $('meta[name="csrf-token"]').attr('content');
$.ajaxPrefilter(function(options, originalOptions, xhr) {
xhr.setRequestHeader('X-CSRF-Token', token);
});
});