Ember.js Lessons Learned So Far
Ember.js
Lessons Learned and Stuff
Justin Ball
- CTO Atomic Jolt
- Consultant, Rubyist, Javascriptist, Cyclist, Maker, Entrepreneur
- Purveyor of Buzzwords
What is Ember.js?
- A framework for creating ambitious web applications.
- Client-side MVC Framework
- Open Source – Github Pulse
- Single page apps are sexy
- Ember.js makes it easy*
* Easy is exclusive of the tears shed while learning Ember.js
Why Single Page Apps?
iFrames!
Safari and IE no longer let you write a cookie to an iFrame unless the user has visited the site
Real World – LTI apps
Heads Up
- Ember.js changes a lot
- Ignore anything over 3 or 4 months old
- When you choose Ember.js you are all in
Using Rails?
gem 'ember-rails'
gem 'ember-source', '1.4.0' # or the version you need
//= require handlebars
//= require ember
//= require ember-data
Why choose the gem over anything else?
AMD?
I love require.js
fogetaboutit
Or Try Ryan Florence’s Ember Tools*
*See “Ember Select doesn’t work” on this page to learn about the dangers
Feel Good About Polluting The Global Namespace
App = Ember.Application.create({
});
I don’t do Rails
Try
Ember.js and AngularJS
Everything is Awesome
AngularJS from an Ember perspective
Ember.js is Magic!
Except for when it’s not
Debugging
The Bug
Assertion failed: Cannot call get with ‘id’ on an undefined object. application.js?body=1:16925
Uncaught TypeError: Cannot read property ‘__ember1375726885234_meta’ of undefined
The Code
var map = MapModel.createRecord({
title: 'New Map'
});
map.save().then(function(){
this.transitionTo('map', map);
}.bind(this));
The Cause?
Failure to call ‘transition.abort();’
Now We Know Better
Assertion failed: Cannot call get with ‘id’ on an undefined object. application.js?body=1:17079
Uncaught TypeError: Cannot read property ‘__ember1375989556474_meta’ of undefined application.js?body=1:18391
Trying to get configured shortcut getDocumentSelection.js:51
Assertion failed: Emptying a view in the inBuffer state is not allowed and should not happen under normal circumstances. Most likely there is a bug in your application. This may be due to excessive property change notifications.
{% raw %}
<li>{{#linkTo 'map.destroy' title="Delete the current map"}}<i class="icon-trash"></i> Delete{{/linkTo}}</li>
{% endraw %}
No we don’t. ‘map’ was null.
Now That We’re Experts
Assertion failed: Cannot call get with ‘id’ on an undefined object. ember.js?body=1:364
Uncaught TypeError: Cannot read property ‘__ember1377186615643_meta’ of undefined ember.js?body=1:1676
Assertion failed: Emptying a view in the inBuffer state is not allowed and should not happen under normal circumstances. Most likely there is a bug in your application. This may be due to excessive property change notifications. ember.js?body=1:364
Uncaught Error: You cannot modify child views while in the inBuffer state ember.js?body=1:18835
“Most likely there is a bug in your application”
Would not have guessed that
Bad Code
{% raw %}
{{#each post in Posts}}
{{#linkTo 'posts.show' title="View post"}}{{post.name}}{{/linkTo}}
{{/each}}
{% endraw %}
</pre>
<p>Good Code</p>
{% raw %}
{{#each post in Posts}}
{{#linkTo 'posts.show' post title="View post"}}{{post.name}}{{/linkTo}}
{{/each}}
{% endraw %}
</pre>
<p class="fragment roll-in">Forgot to include 'post'</p>
</section>
</section>
<section>
<section>
<h2><a href="dont-forget-your-period-computed-property-that-depends-on-ember_arraycontroller-contents">(Really) Little Things Matter</a></h2>
<pre><code data-trim="" contenteditable="">
App.ContactController = Ember.ObjectController.extend({
needs: ['selected_contacts'],
selectedContacts: null,
selectedContactsBinding: 'controllers.selected_contacts.content',
isSelected: function(){
console.log('Checking Selected for' + this.get('name'));
return this.selectedContacts.contains(this.get('content'));
}.property('selectedContacts@each')
});
</code></pre>
<p class="fragment roll-in">It looks like the #$%#! sample code.</p>
<p class="fragment roll-in">But isSelected never fires!!!!</p>
</section>
<section>
<h2>I see you still use email</h2>
<p>Bad</p>
<pre><code data-trim="" contenteditable="">
property('selectedContacts@each')
</code></pre>
<p>Good</p>
<pre><code data-trim="" contenteditable="">
property('selectedContacts.@each')
</code></pre>
</section>
</section>
<section>
<section>
<h2>Multiple Ember Apps in a Rails App</h2>
<p>Stay DRY. Share code</p>
</section>
<section>
<p>Setup to reuse code from 'Common'</p>
<pre><code class="more_code">
Common
components
helpers
models
mixins
templates
SomethingAwesome
controllers
helpers
mixins
routes
templates
views
Admin
controllers
helpers
mixins
routes
templates
views
</code></pre>
</section>
<section>
<h2>The Problem</h2>
<p>Ember won't be able to find your templates</p>
</section>
<section>
<h2>The Solution</h2>
<p>Override the Resolver</p>
<pre><code data-trim="" contenteditable="">
AppNamespace = 'SomethingAwesome';
SomethingAwesome = Ember.Application.create({
Resolver: Ember.DefaultResolver.extend({
resolveTemplate: function(parsedName){
var fullNameWithoutType = parsedName.fullNameWithoutType;
parsedName.fullNameWithoutType = AppNamespace + "/" + fullNameWithoutType;
var result = this._super(parsedName);
if(!result){
parsedName.fullNameWithoutType = "common/" + fullNameWithoutType;
result = this._super(parsedName);
}
return result;
}
})
});
</code></pre>
<p><a href="https://github.com/tatemae/oea">Check out Open Assessments source for an example</a></p>
</section>
</section>
<section>
<section>
<h2>Transitions</h2>
<p>Do This Infinitely</p>
<pre><code data-trim="" contenteditable="">
App.ApplicationRoute = Ember.Route.extend({
model: function(){
return this.store.find('user', params.user_id);
},
afterModel: function(transition){
this.transitionTo('anotherPlace');
}
});
</code></pre>
</section>
<section>
<p>Check Your Target</p>
<pre><code data-trim="" contenteditable="">
App.ApplicationRoute = Ember.Route.extend({
model: function(){
return this.store.find('user', params.user_id);
},
afterModel: function(transition){
if(transition.targetName == "application.index"){
this.transitionTo('anotherPlace');
}
}
});
</code></pre>
</section>
</section>
<section>
<h2><a href="building-with-ember">Choose your event names wisely</a></h2>
<p>'destroy' is reserved</p>
<pre><code data-trim="" contenteditable="">
{% raw %}
<a href="#" {{action 'destroy'}} class="btn">Delete</a>
{% endraw %}
</code></pre>
<p>'destroy_node' is not</p>
<pre><code data-trim="" contenteditable="">
{% raw %}
<a href="#" {{action 'destroy_node'}} class="btn">Delete</a>
{% endraw %}
</code></pre>
</section>
<section>
<h2><a href="building-with-ember">Set controller for route</a></h2>
<pre><code data-trim="" contenteditable="">
var ThingEditRoute = Ember.Route.extend({
controllerName: 'thing',
renderTemplate: function(controller, model){
// You have to pass the controller to render or it will generate a new controller
this.render({ controller: controller, into: 'application', outlet: 'modal' });
}
});
</code></pre>
<p>Looks easy but couldn't find the docs</p>
</section>
<section>
<h2><a href="pass-attributes-to-an-emberjs-view">Pass a Value to a View</a></h2>
<p>Add a property to your view:</p>
<pre><code data-trim="" contenteditable="">
App.ModalView = Ember.View.extend({
aClassName: 'modal'
});
</code></pre>
<p>Use the value in your view template:</p>
<pre><code data-trim="" contenteditable="">
{% raw %}
<div {{bindAttr class="view.aClassName"}}>
More stuff here
</div>
{% endraw %}
</code></pre>
<p>When you use the view just set the property:</p>
<pre><code data-trim="" contenteditable="">
{% raw %}
{{#view App.ModalView aClassName="wide_modal"}}
Some great content goes here
{{/view}}
{% endraw %}
</code></pre>
</section>
<section>
<section>
<h2><a href="immutable-arrays-in-ember">Immutable Arrays</a></h2>
<p>Adding parameters to 'find' results in an immutable model</p>
<pre><code data-trim="" contenteditable="">
App.ApplicationRoute = Ember.Route.extend({
model: function(){
return this.store.find('course', {user_id: user_id});
}
});
</code></pre>
</section>
<section>
<p>Use an ArrayProxy to builid a collection that can be modified</p>
<pre><code data-trim="" contenteditable="">
App.ApplicationRoute = Ember.Route.extend({
model: function(){
var courses = Ember.ArrayProxy.create({content: []});
this.store.find('courses', user_id: user_id}).then(function(data){
data.forEach(function(course){
if(!courses.contains(course)){
courses.pushObject(course);
}
}.bind(this));
}.bind(this));
return courses;
}
});
</code></pre>
<p class="fragment block">But then you have to manually keep the collection updated.</p>
</section>
<section>
<h2>Even Better - Filters!</h2>
<pre><code data-trim="" contenteditable="">
var filter = this.store.filter('course', function(course){
return !course.get('user_id') == userId;
});
this.controllerFor('courses').set('model', filter);
// Load all courses into the store.
Ember.run.once(this, function(){
this.store.find('course');
});
</code></pre>
</section>
</section>
<section>
<section>
<h2>What about Ember Data?</h2>
<p class="fragment block">Beta</p>
<p class="fragment block">Rapidly evolving</p>
<div class="fragment block">
<p>Not Production Ready*</p>
<div class="block">
<p class="small">* We use it in production anyway</p>
<p></p></div>
<p></p></div>
</section>
<section>
<h2>More Ember Data</h2>
<p>All pre-beta examples on the internet are now wrong</p>
<p><a href="https://github.com/emberjs/data/blob/master/TRANSITION.md">Let the Ember Data Transition Guide Take You to a Happy Place</a></p>
<p>1.0 Beta changed to reduce dependance on global application object</p>
</section>
<section>
<div class="left">
<p>Ember Data 0.13</p>
<pre><code data-trim="" contenteditable="">
App.Post.find();
App.Post.find(params.post_id);
</code></pre>
<p></p></div>
<div class="right">
<p>Ember Data 1.0.beta.1:</p>
<pre><code data-trim="" contenteditable="">
this.store.find('post');
this.store.find('post', params.post_id);
</code></pre>
<p></p></div>
</section>
<section>
<div class="left">
<p>Ember Data 0.13</p>
<pre><code data-trim="" contenteditable="">
App.Post.createRecord();
</code></pre>
<p></p></div>
<div class="right">
<p>Ember Data 1.0.beta.1:</p>
<pre><code data-trim="" contenteditable="">
this.store.createRecord('post');
</code></pre>
<p></p></div>
</section>
</section>
<section>
<h2>I already have Rails models</h2>
<p><a href="generate-ember.js-models-using-your-rails-schema">Generate Ember.js Models Using Your Rails Schema*</a></p>
<p></p><pre><code class="more_code" data-trim="" contenteditable="">
namespace :ember do
desc "Build ember models from schema"
task :models => :environment do
# Change these values to fit your project
namespace = 'App' # The Ember application's namespace.
# The directory where ember models will be written. We drop them
# in the tmp directory since we might not want an ember model for every table in the
# database.
output_dir = File.join(Rails.root, "tmp/ember_models")
schema_file = File.join(Rails.root, 'db/schema.rb')
current = ''
file = ''
max = 0
attrs = []
File.readlines(schema_file).each do |line|
# Stuff to ignore
next if line.strip.blank?
next if /#.*/.match(line)
next if /add_index.+/.match(line)
next if /ActiveRecord::Schema.define/.match(line)
# Find tables in the schema
if m = /create_table \"(.+)\".*/.match(line)
current = "#{namespace}.#{m.captures[0].classify.singularize} = DS.Model.extend({\n"
file = "#{m.captures[0].singularize}.js"
elsif m = /t\.(.+)\s+"([0-9a-zA-z_]+)".*/.match(line)
max = m.captures[1].length if m.captures[1].length > max
attrs << m.captures
elsif m = /end/.match(line) && current.present?
attrs.each_with_index do |attr, i|
spaces = ''
type = 'string'
if %w(integer float).include?(attr[0])
type = 'number'
elsif %w(datetime time date).include?(attr[0])
type = 'date'
elsif %w(boolean).include?(attr[0])
type = 'boolean'
end
comma = ','
if attrs.size-1 == i
comma=''
end
((max + 1) - attr[1].length).times{spaces << ' '}
if attr[1].ends_with?('_id')
relation = attr[1][0...(attr[1].length-3)]
current << " #{relation}: #{spaces}DS.belongsTo('#{relation.camelize(:lower).singularize}'),\n"
end
current << " #{attr[1]}: #{spaces}DS.attr('#{type}')#{comma}\n"
end
current << "});\n"
f = File.join(output_dir, file)
if File.exists?(f)
puts "Ember model already exists: #{f}"
else
current.gsub!('_spaces_', '')
puts "Writing Ember model: #{f}"
File.open(f, 'w'){|f| f.write(current)}
end
current = ''
file = ''
max = 0
attrs = []
else
if /end/.match(line).blank?
puts "Don't know how to handle: #{line}"
end
end
end
end
end
</code></pre>
<p class="small">*It's not my fault if this code nukes your site or ruins your relationships.</p>
<p> </section></p>
<section>
<section>
<h2>Ember is Full of Promises</h2>
<p class="fragment block">Think Asyncronous</p>
</section>
<section data-transition="linear" data-background-transition="slide">
<h2>Make a Promise</h2>
<h4>Docs Show This</h4>
<pre><code class="more_code" data-trim="" contenteditable="">
return new Promise(function(resolve, reject){
}.bind(this));
</code></pre>
<h4>But Do this</h4>
<pre><code class="more_code" data-trim="" contenteditable="">
return new Ember.RSVP.Promise(function(resolve, reject){
}.bind(this));
</code></pre>
</section>
<section data-transition="linear" data-background="#c0392b" data-background-transition="slide">
<h4>Original</h4>
<p>Requirement: wait until the entire tree is loaded before transition.</p>
<pre><code class="more_code" data-trim="" contenteditable="">
App.GoogleFile = Ember.Object.extend({
});
App.ApplicationRoute = Ember.Route.extend({
model: function(){
var model = App.GoogleFile.create({
id: $('meta[name="google-folder-id"]').attr('content');
children: Ember.ArrayProxy.create({content: []})
});
this.loadChildren(model.get('children'));
return model;
},
afterModel: function(transition){
if(transition.targetName == "application.index"){
this.wait(model.get('children'), function(){
this.transitionTo('anotherPlace');
});
}
},
// Waiting for content to load using a timer.
wait: function(children, callback){
if(Ember.isEmpty(children)){
Ember.run.later(this, function () {
this.wait(children);
}, 10);
} else {
callback(children);
}
},
loadChildren: function(node){
var token = $('meta[name="google-access-token"]').attr('content');
var query = encodeURIComponent('"' + node.get('id') + '" in parents');
$.get('https://www.googleapis.com/drive/v2/files?q=' + query + '&access_token=' + token, function(data){
data.items.forEach(function(item){
var f = App.GoogleFile.create({
name: item.title,
id: item.id,
icon: item.iconLink,
mime: item.mimeType,
embed: item.embedLink,
edit: item.alternateLink,
children: Ember.ArrayProxy.create({content: []})
});
if(item.mimeType === "application/vnd.google-apps.folder"){
this.loadChildren(f);
}
node.get('children').pushObject(f);
}.bind(this));
}.bind(this));
}
});
</code></pre>
</section>
<section data-transition="linear" data-background-transition="slide">
<h4>The Refactor</h4>
<pre><code class="more_code" data-trim="" contenteditable="">
App = Ember.Application.create({
});
App.GoogleFile = Ember.Object.extend({
});
App.ApplicationRoute = Ember.Route.extend({
model: function(){
var model = App.GoogleFile.create({
id: $('meta[name="google-folder-id"]').attr('content');,
children: Ember.ArrayProxy.create({content: []})
});
return new Ember.RSVP.Promise(function(resolve, reject){
this.loadChildren(model, resolve, reject);
}.bind(this));
},
// The afterModel won't fire until the promise is fullfilled.
afterModel: function(transition){
if(transition.targetName == "application.index"){
this.transitionTo('anotherPlace');
}
},
loadChildren: function(node, resolve, reject){
var token = $('meta[name="google-access-token"]').attr('content');
var query = encodeURIComponent('"' + node.get('id') + '" in parents');
// Don't resolve the promise when the ajax call returns. We have to process the data and decide if we need to make more calls.
$.get('https://www.googleapis.com/drive/v2/files?q=' + query + '&access_token=' + token, function(data){
var promises = [];
data.items.forEach(function(item){
var f = App.GoogleFile.create({
name: item.title,
id: item.id,
icon: item.iconLink,
mime: item.mimeType,
embed: item.embedLink,
edit: item.alternateLink,
children: Ember.ArrayProxy.create({content: []})
});
if(item.mimeType === "application/vnd.google-apps.folder"){
// We need to make more ajax calls. Create a new promise which can be resposible for
// resolving existing promises once it is fullfilled.
var promise = new Ember.RSVP.Promise(function(resolve, reject){
this.loadChildren(f, resolve, reject);
}.bind(this));
promises.push(promise);
}
node.get('children').pushObject(f);
}.bind(this));
Promise.all(promises).then(function(){
resolve(node);
});
}.bind(this));
}
});
</code></pre>
<p><a href="http://jsbin.com/koveg/6/edit?html,js,console" target="_blank" rel="noopener">Working code snippet on jsbin</a></p>
</section>
</section>
<section>
<section>
<h2>You're sitting around doing nothing And...</h2>
<pre><code class="more_code" data-trim="" contenteditable="">
var Adapter = DS.RESTAdapter.extend({
ajaxError: function(jqXHR){
if(jqXHR.status == 401){
window.location.href = '/users/sign_in?timeout=true';
}
if(jqXHR){
jqXHR.then = null;
}
return jqXHR;
}
});
</code></pre>
</section>
<section>
<h2>You have to warn me about these things</h2>
<pre><code class="more_code" data-trim="" contenteditable="">
<div class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title" id="tmModalLabel">Inactivity Warning</h4>
</div>
<div class="modal-body">
You will be logged out in 30 seconds.
</div>
</div>
</div>
</div>
</code></pre>
</section>
<section>
<h2>Component!</h2>
<pre><code class="more_code" data-trim="" contenteditable="">
App.InactivityWarningComponent = Ember.Component.extend({
active: false,
inactiveTimeout: 12000000, // Amount of time before we redirect to the sign in screen - the session should have expired by this point. (20 minutes)
warningTimeout: 30000, // Amount of time the user has to perform an action before the last keep alive fires - 30 seconds
timeout: 1170000, // 19.5 minutes. We want to be less than the 20 minute timeout to be sure the session is renewed.
didInsertElement: function(){
//if($('meta[name="in-development"]').attr('content')){ return; } // Uncomment and add a meta tag to your head if you want to avoid session timeout in development
var context = this;
var keepActive = function(){
if(context.active){
// Keep the session alive
$.ajax({
url: "/stayin_alive"
}).done(function(result){
// Go inactive until the user moves the mouse or presses a key
context.active = false;
// The user now has another 20 minutes before the session times out
// Restart the timer to keep the user logged in
Ember.run.later(context, keepActive, context.timeout);
// Set a timer to show a modal indicating the user is about to be logged out.
Ember.run.debounce(context, context.show, context.timeout - context.warningTimeout);
// Set a timer that will send the user to the login screen
Ember.run.debounce(context, context.forceLogin, context.inactiveTimeout);
});
}
};
$(window).mousemove(function(e){
context.active = true;
// Make sure the modal is hidden. This will cause the modal to hide if the user moves the mouse or presses a key.
// Use debounce so we don't call it over and over again since this method is called from mousemove
Ember.run.debounce(context, context.hide, 1000);
});
$(window).keypress(function(e){
context.active = true;
// Make sure the modal is hidden. This will cause the modal to hide if the user moves the mouse or presses a key.
context.hide();
});
// The user has 5 minutes before they are logged out. We need to send a keep Active before then.
Ember.run.later(context, keepActive, context.timeout);
},
forceLogin: function(){
window.location.href = '/users/sign_out?timeout=true';
},
show: function(){
// Warn the user that they will be logged out if we are inactive
if(this.active === false){
// Start countdown timer
this.$('.modal').modal('show');
}
},
hide: function(){
this.$('.modal').modal('hide');
}
});
</code></pre>
<p><a href="session-timeout-for-your-ember-application">Ember.js Session Timeout In Depth</a></p>
</section>
</section>
<section>
<section>
<h2><a href="ember-textfield-ignores-arrow-keys.-how-to-monitor-key-events-in-your-textfield">Monitor key events in your textfield</a></h2>
<p>Monitor changes as the user types</p>
<pre><code data-trim="" contenteditable="">
{% raw %}
{{ view Ember.Textfield
class="form-control"
placeholderBinding="controller.prompt"
valueBinding="controller.value"
onEvent="keyPress"
action="typing"
}}
{% endraw %}
</code></pre>
<p class="fragment block">but keyPress doesn't fire when you press the arrow keys</p>
</section>
<section>
<p>No problem! Make your own Textfield</p>
<pre><code class="more_code" data-trim="" contenteditable="">
App.Textfield = Ember.TextField.extend({
init: function() {
this._super();
this.on("keyUp", this, this.interpretKeyEvents);
},
interpretKeyEvents: function(event){
var map = TM.Textfield .KEY_EVENTS;
var method = map[event.keyCode];
if (method){
return this[method](event);
} else {
this._super(event);
}
},
arrowUp: function(event){
this.sendAction('arrow-up', this, event);
},
arrowDown: function(event){
this.sendAction('arrow-down', this, event);
}
});
App.Textfield.KEY_EVENTS = {
38: 'arrowUp',
40: 'arrowDown'
};
</code></pre>
</section>
<section>
<p>Use that new code</p>
<pre><code data-trim="" contenteditable="">
{% raw %}
{{ view App.Textfield
class="form-control"
placeholderBinding="view.prompt"
valueBinding="view.value"
viewName="inputField"
arrow-up="arrowUp"
arrow-down="arrowDown"
}}
{% endraw %}
</code></pre>
<p>Add 'arrowUp' and 'arrowDown' to your controller and be filled with joy.</p>
</section>
</section>
<section>
<h1>THE END</h1>
<h3>Justin Ball</h3>
<p>justinball.com</p>
<p>justinball@gmail.com</p>
<p>@jbasdf</p>
<p class="small" style="display:none;"><a href="#pdf-export">Download PDF</a></p>
</section>
<p></div>
</p>
<p></p>
</section>