Writing Big (ambitious) Apps Using
Just Got Easier
Oren Rubin
Wix Web Architect
IBM Distributed Computing
Spring Diagnostics Machine Learning
Cadence Compiler Engineer
Ember history
Ember currently
Why ember?
why ember?
Why ember?
- Fast development
- Performance, optimizations for DOM updates
- Debugging tools
- Useful helpers (Array's filterProperty, Em.K, etc.)
What is a typical Ember Application?
Router
Models
CONTROLLERS
The Business Logic
Take some Model object
And show it using Views
CONTROLLERS
Views
Business Logic
to
UI Interaction
Generates HTML using Templates
Business Logic
Templates
EMBER CLASSES
Bird View
classes
Hello Web
<html>
<head></head>
<body>
Hello Web
</body>
</html>
Hello Web
hello ember
<body>
<script src="jquery.js"/>
<script src="handlebars.js"/>
<script src="ember.js"/>
<script>
var App = Ember.Application.create();
</script>
<script type="text/x-handlebars">
Hello Ember!
</script>
<body>
=
Hello Ember!
Tip: use zepto for mobile
Binding
var App = Ember.Application.create({ name: "Ember App!" })
+
<script type="text/x-handlebars"> Hello {{App.name}} </script>
=
Hello Ember!
Tip: naming convention #1: uppercase means global (App)
Classes & instances
var MyClass = Ember.Object.extend({
x: 5,
bestMethodEva: function() {
return this._super() + ":" + this.get('x')
}
})
var myInstance = MyClass.create()
console.log(myInstance.get('x'))
console.log( myInstance.bestMethodEva() );
Tip: naming convention #2: An application extends Namespace, which is why it's uppercase
CLASSES & INSTANCES
var obj = Ember.Object.create({
a: {
b: {
c: 5
}
}
})
var c = obj.get('a.b.c'); // getPath in ember < 1.0
if (c) {...} // c is undefined if 'a' or 'b' are falsy
// never again!
if (obj.a && obj.a.b && obj.a.b.c) {
...
}
array
App.TodosController = Ember.Object.extend({
todos: []
})
App.todosController = App.TodosController.create()
+
{{App.todosController.todos.length}} TODOs
=
0 TODOs
Array
setTimeout(function(){
var newTodo = Ember.Object.create({
description: "best todo ever"
})
var todos = App.todosController.get('todos')
todos.pushObject(newTodo)
// console.log(todos.get('lastObject'))
}, 5000)
+
{{App.todosController.todos.length}} TODOs
=
1 TODOs
Binding
App.TodosController = Ember.Object.extend({
todos: [],
appNameBinding: 'App.name'
// appName field will be added "in-memory", and updated
})
use with either
{{App.todosController.appName}}
or
{{App.name}}
Tip: Ember generates objects in-memory, rather than files
computed properties
App.TodosController = Ember.Object.extend({
appNameBinding: 'App.name',
todosName: function() {
return 'todos controller in ' + this.get('appName')
}.property('appName')
})
+
{{App.todosController.todosName}}
=
todos controller in Ember App!
Tip: Add console.log/alert to check it only runs once.
computed properties
App.TodosController = Ember.Object.extend({
todos: [],
remaining: function() {
var todos = this.get('todos');
var completed = todos.filterProperty('isDone');
return todos.length - completed.length;
}.property('todos.@each.isDone')
});
+
{{App.todosController.remaining}} remaining
=
2 remaining
Tip: 'remaining' field will be recalculated only if a todo's isDone is changed, todo is added/removed, or the entire array replaced.
but there's more!
Observers
App.TodosController = Ember.Object.extend({
selectedTodo: undefined,
todosChanged: function() {
var selected = this.get('selectedTodo')
if (!todos.contains(selected)) {
this.set('selected', undefined)
}
}.observes('todos.@each') //.beforeObserver()
})
Tip: can be notified for only removal
Riddle
var MyClass = Ember.Object.Extend({
people: [],
add: function(name) {
var people = this.get('people')
people.push(name)
console.log(people)
}
});
var obj1 = MyClass.create()
var obj2 = MyClass.create()
obj1.add('Ran Tavory')
obj2.add('Ori Lahav')
Tip: javascript uses prototype inheritance
Templates
Handlebars
HTML
<div>
{{name}} is fastest programmer eva!
</div>
Handlebars
<div>
<script id="metamorph-3-start" type="text/placeholder"/>
Itay Maman
<script id="metamorph-3-end" type="text/placeholder"/>
is fastest programmer eva!
</div>
Humans
Itay Maman is fastest programmer eva!
Handlebars
<ul id="todo-list">
<li>
Take over the universe
</li>
<li>
Have the penne arrabiata (without a tray)
</li>
</ul>
handlebars
<ul id="todo-list">
{{#each todo in App.todosController.todos}}
<li>
{{todo.description}}
</li>
{{/each}}
</ul>
Logic-less
Logic-less
In CSS, all states in one place.
section {
background: rgba(0,0,0, 0.5);
}
section:hover {
background: #BADA55;
}
Same in ember
{{#if todos.isLoaded}}
<div> spinner </div>
{{#else}}
<ul>
<!-- some list -->
</ul>
{{end}}
Tip: using ember-data, you'll get the 'isLoaded' for free!
Templates
Template is a template
<script type="text/x-handlebars" template-name="todo">
todo description: {{description}}
</script>
{{helperName}} or {{helperName X}} or {{helperName X=Y A=B}}
Tip: You can write custom helpers
Views
Views
View
MyView.create().append('#container')
{{view path.to.class}}
View
App.TodoView = Ember.View.extend({
templateName: 'todo',
description: "best TODO eva!"
});
<script type="text/x-handlebars" template-name="todo">
todo description: {{description}}
</script>
{{view App.TodoView}}
=
todo description: best TODO eva!
handle appearance
App.TodoView = Ember.View.create({
todo: {..},
a: false,
f: function() {
if(sky.contains('sun') {
this.set('a', true)
}
}
isSelected: {
var selectedTodods = App.todoController.selected
return selected Todos.contains( this.get('todo') )
}.property('App.todosController.selected'),
});
handle appearance
<div {{bindAttr class="isSelected"}}>
<div {{bindAttr class="a:a-is-true:a-is-false"}}/>
</div>
=
<div class="is-selected">
<div class="a-is-false">
</div>
</div>
or<div>
<div class="a-is-true">
</div>
</div>
2 more options, depending on the weather
(and if the todo is selected)
handle appearance
<div class="is-selected" bind-attr-id="18">
<div class="a-is-false" bind-attr-id="53">
</div>
</div>
or<div bind-attr-id="18">
<div class="a-is-true" bind-attr-id="53">
</div>
</div>
Top Secret: Ember uses "bind-attr-id" to find what should be updated
HANDLE DOM EVENT
<div>
<div class="inner">
{{todo.description}}
</div>
</div>
Tip: choose tag using "tagName" field
Handle DOM Events
App.TodoView = Ember.View.create({
templateName: 'todo',
description: "best TODO eva!",
click: function() {
alert('someone clicked inside somewhere')
}
});
HANDLE DOM EVENTs
HANDLE DOM EVENTs
var App = Em.Application.create({
customEvents: {
// jquery-event-name: method-name-in-view
mousewheel: 'mouseWheel'
}
});
Top Secret: Ember captures the events on root
HANDLE DOM EVENT
<div>
<div class="inner" {{action methodName on="click"}}>
{{todo.description}}
</div>
</div>
Tip: click is default, remove redundant code (on="click")
Handle dom event
App.TodoView = Ember.View.create({
templateName: 'todo',
didInsertElement: function() {
var wrappingElement = this.$()
var elementInside = this.$('.inner')
elementInside.click(function(){
alert('yay')
})
})
)}
Tip: perfect for jQuery plugins!
life cycle
life cycle
destroy: function () {
var c = this;
this.unbind();
try {
var elem = this._element
elem.pause(),
elem.removeEvent("error", this._triggerError),
elem.removeEvent("ended", this._triggerEnd),
elem.removeEvent("canplay", this._triggerReady),
elem.removeEvent("loadedmetadata",this._onLoadedMetadata)
_.each(b, function (a) {
c._element.removeEvent(a, c._bubbleProfilingEvent)
}),
_.each(a, function (a) {
c._element.removeEventListener(a, c._logEvent)
}),
elem = null,
this.trigger("destroy")
} catch (d) {}
}
life cycle
Life cycle
Views are added and removed all the time!
{{#if todo.empty}}
<div> no todos.. call luke </div>
{{#else}}
<ul> <--loop and print them--> </ul>
{{end}}
SAy what?!
Life cycle
c'tor is for initialization, not DOM
App.TodoView = Ember.View.create({
templateName: 'todo',
init: function() {
// constructor
console.log( this.get('state') ); // "preRender"
});
});
Views
App.TodoView = Ember.View.extend({
tagName: 'li',
classNames: [a, b]
});
{{view TodoView appNameBinding="App.name" classNames="a b"}}
var view App.TodoView.create({
classNames: "a b"
})
debugging Views
Inline template
{{view DeathStar}}
{{#view DeathStar}}
Don't use DeathStar's template, use the one of
rebel alliance
{{/view}}
Tip: inner components are specified in the template. Great for unit tests isolation.
Views
Views - summary
Controllers
Controllers
* One controller responsible for list of emails.
Controllers
Controllers
<script type="x-handlebars" data-template-name="sound">
<p>
<strong>Song</strong>: {{name}} by {{artist}}
</p>
<p>
<strong>Duration</strong>: {{duration}}
</p>
</script>
Top Secret: named and artist are proxied from the model
models
models
App.Store = DS.Store.extend({
revision: 11, // let you know if breaking changes happen.
adapter: 'DS.RESTAdapter', // built-in REST adapter
// adapter: 'DS.FixtureAdapter' // run solo
// adapter: 'App.MyCustomAdapter' // custom
});
models
var attr = DS.attr;
App.Person = DS.Model.extend({
firstName: attr('string'),
lastName: attr('string'),
birthday: attr('date'),
fullName: function() {
return this.get('firstName') +' '+ this.get('lastName');
}.property('firstName', 'lastName')
});
Tip: Computed properties won't be serialized
fixtures
App.Person.FIXTURES = [
{
id: 1, // this is a MUST
firstName: 'Ferris',
lastName, 'Bueller'
},
{
id: 2,
firsName: 'Moshe',
lastName: 'Oofnik',
}
]
Relationship
One to one
var attr = DS.attr;
App.User = DS.Model.extend({
profile: DS.belongsTo('App.Profile')
});
One to Many
App.Post = DS.Model.extend({
comments: DS.hasMany('App.Comment')
});
Many to Many
App.BlogPost = DS.Model.extend({
tags: DS.hasMany('App.Tag')
});
models
var posts = App.Post.find();
var post = App.Post.find(1);
var people = App.Person.find({ name: "Peter"});
person.get('isDirty');
//=> false
person.set('isAdmin', true);
person.get('isDirty');
//=> true
Model
{
"post": {
"id": 1,
"title": "Rails is omakase",
"comments": [1, 2, 3]
},
"comments": [{
"id": 1,
"body": "But is it _lightweight_ omakase?"
},
{
"id": 2,
"body": "I for one welcome our new omakase overlords"
}]
}
router
Router
Router
App.Router.map(function() {
this.resource("about"); // { path: "/about" }); redundant
this.resource("favorites", { path: "/favs" });
})
The Routes are always connected to a controller
App.AboutRoute = Ember.Route.extend({
setupController: function(controller) {
// Set the AboutController's 'title'
controller.set('title', "My App")
}
})
Router
/
index
IndexController
IndexRoute
index
/about
about
AboutController
AboutRoute
about
/favs
favorites
FavoritesController
FavoritesRoute
favorites
Load data
App.UsersRoute = Ember.Route.extend({
model: function(){
return App.User.find()
}
})
Nested routes
App.Router.map(function() {
this.resource("users", function () {
this.resource("user", {path: ':user_id'})
})
})
Example path: #/users/5
"If your UI is nested, your routes should be nested"
- Yehuda Katz
Router - extras
finishing
outlet
<script type="..." template-name="application">
kiss my shiny metal ass!
</script>
<script type="..." template-name="application">
kiss my shiny metal {{outlet}}
</script>
<script type="..." template-name="index">
index template
</script>
outlet
<script type="..." template-name="users">
all users will be shown here
</script>
App.UsersRoute = Ember.Route.extend({
model: function(){ return App.User.find() }
})
Tip: This is the default implementation
show all users
App.Person.FIXTURES = [
{
id: 1,
firstName: ferris,
lastName, bueller
}
]
<script type="..." template-name="users">
{{#each user in controller}}
{{user.name}}
{{/each}}
</script>
Tip: Remember that the Controller proxies request to the model
Show user details
App.Router.map(function() {
this.resource("users", function () {
this.resource("user", {path: ':user_id'})
})
})
<script type="..." template-name="user">
<header> {{firstName}} </header>
<section> {{firstName}} {{lastName}} is .. </section>
</script>
show specific user II
<script type="..." template-name="users">
{{#each user in controller}}
{{user.name}}
{{/each}}
<section>
{{outlet}}
</section>
</script>
Extras
Extras
that's all folks
Thank you
ember
By shex
ember
- 6,420