@markbates
@markbates
FLUENT2014
www.metacasts.tv
www.angularmasterclass.com
Angular
Fundamentals
Enough Angular
to be Dangerous!
What Will Cover?
What Will Cover?*
*hopefully
• Controllers!
• ngRoute!
• Templates!
• ngResource!
• Directives!
• Filters!
• Scope!
• Testing!
• Code Organization!
• Best Practices
Part 1
• Features/Why
Angular?!
• Getting Started/Setting
Up!
• Directives, Filters, and
Data Binding!
• Controllers,
Templates, and Scope!
• Modules!
• Routing!
• Custom Directives and
Event Handling!
• Testing
Part 2
We Code!
Part 1
Features
Features
• Plain JavaScript
• Data Binding
• Routing/PushState
• Testing
• Templates/Directives/
Controllers
• Modular
• Dependency Injection
• jqLite
• Lightweight
Why Angular?
Philosophies
Backbone.js
“minimal set of data-structure and view primitives 	

for building web application with JavaScript”
Ember
“framework for creating ambitious web applications”
AngularJS
“Toolset for building the framework
most suited to your application
development”
Weight
“production” versions (minified)	

w/ required dependencies
AngularJS Ember Backbone.js
base 109kb 264kb 6.5kb
templating
language
built-in
90kb	

(handlebars)
??
data adapter built-in
75kb	

(ember-data)
84kb	

(jQuery)
support N/A
84kb	

(jQuery)
17kb	

(json2.js)	

5.0kb	

(underscore.js
)
109kb 513kb 112.5kb
Mindshare
Google
AngularJS Backbone.js Ember
Watchers 2,155 1,442 824
Stars 21,408 17,291 9,570
Forks 6,670 3,783 2,044
Github
“Basic” Models
Backbone.jsclass	
  App.Beer	
  extends	
  
Backbone.Model	
  
	
  	
  
class	
  App.Beers	
  extends	
  
Backbone.Collection	
  
!
	
  model:	
  Beer
EmberApp.Beer	
  =	
  DS.Model.extend	
  
	
  	
  title:	
  DS.attr("string")	
  
	
  	
  abv:	
  DS.attr("number")	
  
	
  	
  country_id:	
  DS.attr("number")	
  
	
  	
  brewery_id:	
  DS.attr("number")	
  
	
  	
  brewery:	
  
DS.belongsTo("App.Brewery")	
  
	
  	
  country:	
  
DS.belongsTo("App.Country")
AngularJS
App.Beer	
  =	
  
{}
“Remote” Models
Backbone.jsclass	
  App.Beer	
  extends	
  Backbone.Model	
  
	
  	
  urlRoot:	
  "/api/v1/beers"	
  
	
  	
  	
  
class	
  App.Beers	
  extends	
  Backbone.Collection	
  
	
  	
  
	
  	
  url:	
  -­‐>	
  
	
  	
  	
  	
  if	
  @brewery_id?	
  
	
  	
  	
  	
  	
  	
  return	
  "/api/v1/breweries/
#{@brewery_id}/beers"	
  
	
  	
  	
  	
  else	
  
	
  	
  	
  	
  	
  	
  return	
  "/api/v1/beers"	
  
	
  	
  
	
  	
  model:	
  Beer
EmberApp.Beer	
  =	
  DS.Model.extend	
  
	
  	
  title:	
  DS.attr("string")	
  
	
  	
  abv:	
  DS.attr("number")	
  
	
  	
  country_id:	
  
DS.attr("number")	
  
	
  	
  brewery_id:	
  
DS.attr("number")	
  
	
  	
  brewery:	
  
DS.belongsTo("App.Brewery")	
  
	
  	
  country:	
  
DS.belongsTo("App.Country")
Ember
DS.RESTAdapter.reopen	
  
	
  	
  namespace:	
  'api/v1'	
  
	
  	
  
App.Store	
  =	
  DS.Store.extend	
  
	
  	
  revision:	
  14	
  
	
  	
  adapter:	
  DS.RESTAdapter.create()
AngularJS
App.factory	
  "Beer",	
  ($resource)	
  -­‐>	
  
	
  	
  return	
  $resource	
  "/api/v1/
beers/:id",	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  {id:	
  "@id"},	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  {update:	
  {method:	
  
"PUT"}}
Routers
Backbone.js@Router	
  =	
  Backbone.Router.extend	
  
	
  	
  
	
  	
  initialize:	
  -­‐>	
  
	
  	
  	
  	
  @countries	
  =	
  new	
  Countries()	
  
	
  	
  
	
  	
  routes:	
  
	
  	
  	
  	
  "breweries/:brewery_id":	
  "brewery"	
  
	
  	
  	
  	
  "breweries/:brewery_id/edit":	
  "breweryEdit"	
  
	
  	
  
	
  	
  brewery:	
  (brewery_id)	
  -­‐>	
  
	
  	
  	
  	
  @changeView(new	
  BreweryView(collection:	
  @countries,	
  model:	
  new	
  
Brewery(id:	
  brewery_id)))	
  
	
  	
  
	
  	
  breweryEdit:	
  (brewery_id)	
  -­‐>	
  
	
  	
  	
  	
  @changeView(new	
  BreweryEditView(collection:	
  @countries,	
  model:	
  new	
  
Brewery(id:	
  brewery_id)))	
  
	
  	
  
	
  	
  changeView:	
  (view)	
  =>	
  
	
  	
  	
  	
  @currentView?.remove()	
  
	
  	
  	
  	
  @currentView	
  =	
  view	
  
	
  	
  	
  	
  $("#outlet").html(@currentView.el)
Ember
App.Router.map	
  -­‐>	
  
	
  	
  @resource	
  "brewery",	
  {path:	
  "brewery/:brewery_id"}
EmberApp.BreweryRoute	
  =	
  
Ember.Route.extend	
  
	
  	
  model:	
  (params)-­‐>	
  
	
  	
  	
  	
  
App.Brewery.find(params.brewery_i
d)
AngularJSApp.config	
  ($routeProvider)	
  -­‐>	
  
	
  	
  
	
  	
  $routeProvider	
  
	
  	
  	
  	
  .when("/breweries/:id",	
  {	
  
	
  	
  	
  	
  	
  	
  templateUrl:	
  "/assets/
brewery.html",	
  
	
  	
  	
  	
  	
  	
  controller:	
  "BreweryController"	
  
	
  	
  	
  	
  })	
  
	
  	
  	
  	
  .when("/breweries/:id/edit",	
  {	
  
	
  	
  	
  	
  	
  	
  templateUrl:	
  "/assets/
edit_brewery.html",	
  
	
  	
  	
  	
  	
  	
  controller:	
  
"EditBreweryController"	
  
	
  	
  	
  	
  })
Controllers/Views
Backbone.jsclass	
  @BreweryEditView	
  extends	
  Backbone.View	
  
	
  	
  
	
  	
  template:	
  "brewery_edit"	
  
	
  	
  
	
  	
  events:	
  
	
  	
  	
  	
  "click	
  #save-­‐button":	
  "saveClicked"	
  
	
  	
  	
  	
  "keypress	
  #brewery-­‐title":	
  "titleEdited"	
  
	
  	
  
	
  	
  initialize:	
  -­‐>	
  
	
  	
  	
  	
  super	
  
	
  	
  	
  	
  @countriesView	
  =	
  new	
  
CountriesView(collection:	
  @collection)	
  
	
  	
  	
  	
  @$el.html(@countriesView.el)	
  
	
  	
  	
  	
  @model.on	
  "change",	
  @render	
  
	
  	
  	
  	
  @model.fetch()	
  
	
  	
  
	
  	
  render:	
  =>	
  
	
  	
  	
  	
  @$("#country-­‐
outlet").html(@renderTemplate())	
  
	
  	
  	
  	
  return	
  @
	
  	
  saveClicked:	
  (e)	
  =>	
  
	
  	
  	
  	
  e?.preventDefault()	
  
	
  	
  	
  	
  attrs	
  =	
  
	
  	
  	
  	
  	
  	
  title:	
  @$("#brewery-­‐title").val()	
  
	
  	
  	
  	
  	
  	
  synonyms:	
  @$("#brewery-­‐synonyms").val()	
  
	
  	
  	
  	
  	
  	
  address:	
  @$("#brewery-­‐address").val()	
  
	
  	
  	
  	
  @model.save	
  attrs,	
  
	
  	
  	
  	
  	
  	
  success:	
  (model,	
  response,	
  options)	
  =>	
  
	
  	
  	
  	
  	
  	
  	
  	
  App.navigate("/breweries/#{@model.id}",	
  
trigger:	
  true)	
  
	
  	
  	
  	
  	
  	
  error:	
  (model,	
  xhr,	
  options)	
  -­‐>	
  
	
  	
  	
  	
  	
  	
  	
  	
  errors	
  =	
  []	
  
	
  	
  	
  	
  	
  	
  	
  	
  for	
  key,	
  value	
  of	
  
xhr.responseJSON.errors	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  errors.push	
  "#{key}:	
  #{value.join(",	
  
")}"	
  
	
  	
  	
  	
  	
  	
  	
  	
  alert	
  errors.join("n")	
  
	
  	
  
	
  	
  titleEdited:	
  (e)	
  =>	
  
	
  	
  	
  	
  title	
  =	
  @$("#brewery-­‐title").val()	
  
	
  	
  	
  	
  @$("h2").text(title)	
  
!
	
  	
  #	
  further	
  code	
  omitted
Ember
App.BreweryController	
  =	
  Ember.ObjectController.extend	
  
	
  	
  
	
  save:	
  -­‐>	
  
	
  	
  	
  	
  @store.commit()	
  
	
  	
  
	
  	
  #	
  further	
  code	
  omitted
AngularJS@EditBreweryController	
  =	
  ($scope,	
  $routeParams,	
  $location,	
  
Brewery)	
  -­‐>	
  
	
  	
  
	
  	
  $scope.brewery	
  =	
  Brewery.get(id:	
  $routeParams.id)	
  
	
  	
  
	
  	
  $scope.save	
  =	
  -­‐>	
  
	
  	
  	
  	
  success	
  =	
  -­‐>	
  
	
  	
  	
  	
  	
  	
  $location.path("/breweries/#{$routeParams.id}")	
  
	
  	
  	
  	
  	
  	
  $scope.errors	
  =	
  null	
  
	
  	
  	
  	
  failure	
  =	
  (object)-­‐>	
  
	
  	
  	
  	
  	
  	
  $scope.errors	
  =	
  object.data.errors	
  
	
  	
  	
  	
  $scope.brewery.$update	
  {},	
  success,	
  failure
Templates
Backbone.js
<h2><%=	
  @model.displayName()	
  %></h2>	
  
	
  	
  
<form>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="title">Title</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  value='<%=	
  @model.get("title")	
  
%>'id='brewery-­‐title'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  value='<%=	
  @model.get("synonyms")	
  
%>'id='brewery-­‐synonyms'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="address">Address</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <textarea	
  class='input-­‐xxlarge'	
  id='brewery-­‐address'><%=	
  @model.get("address")	
  
%></textarea>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <button	
  class='btn	
  btn-­‐primary'	
  id='save-­‐button'>Save</button>	
  
	
  	
  <a	
  href="/breweries/<%=	
  @model.id	
  %>"	
  class='btn'>Cancel</a>	
  
	
  	
  
</form>
Backbone.js
<h2><%=	
  @model.displayName()	
  %></h2>	
  
	
  	
  
<form>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="title">Title</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  
value='<%=	
  @model.get("title")	
  %>'id='brewery-­‐
title'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  value='<%=	
  @model.get("synonyms")	
  %>'id='brewery-­‐synonyms'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="address">Address</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <textarea	
  class='input-­‐xxlarge'	
  id='brewery-­‐address'><%=	
  @model.get("address")	
  %></textarea>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <button	
  class='btn	
  btn-­‐primary'	
  id='save-­‐button'>Save</button>	
  
	
  	
  <a	
  href="/breweries/<%=	
  @model.id	
  %>"	
  class='btn'>Cancel</a>	
  
	
  	
  
</form>
Backbone.js<h2><%=	
  @model.displayName()	
  %></h2>	
  
	
  	
  
<form>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="title">Title</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  value='<%=	
  @model.get("title")	
  %>'id='brewery-­‐title'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  value='<%=	
  @model.get("synonyms")	
  %>'id='brewery-­‐synonyms'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="address">Address</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <textarea	
  class='input-­‐xxlarge'	
  id='brewery-­‐address'><
%=	
  @model.get("address")	
  %></textarea>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <button	
  class='btn	
  btn-­‐primary'	
  id='save-­‐button'>Save</button>	
  
	
  	
  <a	
  href="/breweries/<%=	
  @model.id	
  %>"	
  class='btn'>Cancel</a>	
  
	
  	
  
</form>
<div	
  class='span12'>	
  
	
  	
  <h2>{{displayName}}</h2>	
  
	
  	
  <h3>	
  
	
  	
  	
  	
  {{cityState}}	
  
	
  	
  	
  	
  {{#linkTo	
  "country"	
  country}}	
  
	
  	
  	
  	
  	
  	
  {{country.title}}	
  
	
  	
  	
  	
  {{/linkTo}}	
  
	
  	
  </h3>	
  
	
  	
  {{#if	
  isEditing}}	
  
	
  	
  	
  	
  <form>	
  
	
  	
  
	
  	
  	
  	
  	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="title">Title</label>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  {{view	
  Ember.TextField	
  valueBinding="title"	
  class='input-­‐xxlarge'}}	
  
	
  	
  	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  
	
  	
  	
  	
  	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  {{view	
  Ember.TextField	
  valueBinding="synonyms"	
  class='input-­‐xxlarge'}}	
  
	
  	
  	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  
	
  	
  	
  	
  	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  {{view	
  Ember.TextArea	
  valueBinding="address"	
  class='input-­‐xxlarge'}}	
  
	
  	
  	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  
	
  	
  	
  	
  	
  	
  <button	
  class='btn	
  btn-­‐primary'	
  {{action	
  "save"}}>Save</button>	
  
	
  	
  
	
  	
  	
  	
  </form>	
  
	
  	
  {{	
  else	
  }}	
  
	
  	
  	
  	
  {{	
  partial	
  "brewery/show"	
  }}	
  
	
  	
  {{/if}}	
  
</div>
Ember
<div	
  class='span12'>	
  
	
  	
  <h2>{{displayName}}</h2>	
  
	
  	
  <h3>	
  
	
  	
  	
  	
  {{cityState}}	
  
	
  	
  	
  	
  {{#linkTo	
  "country"	
  country}}	
  
	
  	
  	
  	
  	
  	
  {{country.title}}	
  
	
  	
  	
  	
  {{/linkTo}}	
  
	
  	
  </h3>	
  
	
  	
  {{#if	
  isEditing}}	
  
	
  	
  	
  	
  <form>	
  
	
  	
  
	
  	
  	
  	
  	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="title">Title</label>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  {{view	
  Ember.TextField	
  valueBinding="title"	
  
class='input-­‐xxlarge'}}	
  
	
  	
  	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  
	
  	
  	
  	
  	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  {{view	
  Ember.TextField	
  valueBinding="synonyms"	
  class='input-­‐xxlarge'}}	
  
	
  	
  	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  
	
  	
  	
  	
  	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  {{view	
  Ember.TextArea	
  valueBinding="address"	
  class='input-­‐xxlarge'}}	
  
	
  	
  	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  
	
  	
  	
  	
  	
  	
  <button	
  class='btn	
  btn-­‐primary'	
  {{action	
  "save"}}>Save</button>	
  
	
  	
  
	
  	
  	
  	
  </form>	
  
	
  	
  {{	
  else	
  }}	
  
	
  	
  	
  	
  {{	
  partial	
  "brewery/show"	
  }}	
  
	
  	
  {{/if}}	
  
</div>
Ember
<div	
  class='span12'>	
  
	
  	
  <h2>{{displayName}}</h2>	
  
	
  	
  <h3>	
  
	
  	
  	
  	
  {{cityState}}	
  
	
  	
  	
  	
  {{#linkTo	
  "country"	
  country}}	
  
	
  	
  	
  	
  	
  	
  {{country.title}}	
  
	
  	
  	
  	
  {{/linkTo}}	
  
	
  	
  </h3>	
  
	
  	
  {{#if	
  isEditing}}	
  
	
  	
  	
  	
  <form>	
  
	
  	
  
	
  	
  	
  	
  	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="title">Title</label>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  {{view	
  Ember.TextField	
  valueBinding="title"	
  class='input-­‐xxlarge'}}	
  
	
  	
  	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  
	
  	
  	
  	
  	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  {{view	
  Ember.TextField	
  valueBinding="synonyms"	
  class='input-­‐xxlarge'}}	
  
	
  	
  	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  
	
  	
  	
  	
  	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  {{view	
  Ember.TextArea	
  valueBinding="address"	
  class='input-­‐xxlarge'}}	
  
	
  	
  	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  	
  	
  	
  	
  </div>	
  
	
  	
  	
  	
  	
  	
  <button	
  class='btn	
  btn-­‐primary'	
  {{action	
  
"save"}}>Save</button>	
  
	
  	
  	
  	
  </form>	
  
	
  	
  {{	
  else	
  }}	
  
	
  	
  	
  	
  {{	
  partial	
  "brewery/show"	
  }}	
  
	
  	
  {{/if}}	
  
</div>
Ember
<form>	
  
	
  	
  <h3>{{brewery.title}}</h3>	
  
	
  	
  <div	
  ng-­‐include='"/assets/_errors.html"'></div>	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="title">Title</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  ng-­‐
model='brewery.title'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  ng-­‐
model='brewery.synonyms'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="address">Address</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <textarea	
  class='input-­‐xxlarge'	
  ng-­‐model='brewery.address'></
textarea>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <button	
  class='btn	
  btn-­‐primary'	
  ng-­‐click='save()'>Save</button>	
  
	
  	
  
</form>
AngularJS
AngularJS<form>	
  
	
  	
  <h3>{{brewery.title}}</h3>	
  
	
  	
  <div	
  ng-­‐include='"/assets/_errors.html"'></div>	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="title">Title</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  ng-­‐
model='brewery.title'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  ng-­‐model='brewery.synonyms'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="address">Address</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <textarea	
  class='input-­‐xxlarge'	
  ng-­‐model='brewery.address'></textarea>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <button	
  class='btn	
  btn-­‐primary'	
  ng-­‐click='save()'>Save</button>	
  
	
  	
  
</form>
<form>	
  
	
  	
  <h3>{{brewery.title}}</h3>	
  
	
  	
  <div	
  ng-­‐include='"/assets/_errors.html"'></div>	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="title">Title</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  ng-­‐model='brewery.title'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="synonyms">Synonyms</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <input	
  type='text'	
  class='input-­‐xxlarge'	
  ng-­‐model='brewery.synonyms'>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  
	
  	
  <div	
  class="control-­‐group">	
  
	
  	
  	
  	
  <label	
  class="control-­‐label"	
  for="address">Address</label>	
  
	
  	
  	
  	
  <div	
  class="controls">	
  
	
  	
  	
  	
  	
  	
  <textarea	
  class='input-­‐xxlarge'	
  ng-­‐model='brewery.address'></textarea>	
  
	
  	
  	
  	
  </div>	
  
	
  	
  </div>	
  
	
  	
  <button	
  class='btn	
  btn-­‐primary'	
  ng-­‐
click='save()'>Save</button>	
  
	
  </form>
AngularJS
Pros/Cons
Backbone.js
• Too simple	

• Not opinionated enough	

• “Memory” management	

• Unstructured	

• Spaghetti code
• Lightweight	

• Not opinionated	

• Simple	

• Easy to read source	

• “widget” development
Pros Cons
Ember
• Too complex	

• Overly opinionated	

• Heavyweight	

• ember-data - not production ready (very buggy)	

• Little to no mind-share outside of Rails	

• Difficult to read source code
• Structured	

• Highly opinionated	

• “less” code	

• “large” apps
Pros Cons
AngularJS
• Difficult to read source
code	

• jQuery plugins require
custom directives	

• Large apps requiring self-
imposed structure
• Lightly structured	

• Lightly opinionated	

• “less” code	

• Plain JavaScript	

• Simple/Powerful	

• Easy to test	

• Lightweight	

• small, medium, or large apps
Pros Cons
Getting Started
5 Minute Break
Directives, Filters,
and Data Binding
Directives
<body ng-app>!
<ng-view></ng-view>!
<ul>!
<li ng-repeat='comment in comments'>!
{{comment.body}}!
</li>!
</ul>!
</body>
Directives
<body ng-app>!
<ng-view></ng-view>!
<ul>!
<li ng-repeat='comment in comments'>!
{{comment.body}}!
</li>!
</ul>!
</body>
Directives
<body ng-app>!
<ng-view></ng-view>!
<ul>!
<li ng-repeat='comment in comments'>!
{{comment.body}}!
</li>!
</ul>!
</body>
Directives
<body ng-app>!
<ng-view></ng-view>!
<ul>!
<li ng-repeat='comment in comments'>!
{{comment.body}}!
</li>!
</ul>!
</body>
Directives
<body ng-app>!
<ng-view></ng-view>!
<ul>!
<li ng-repeat='comment in comments'>!
{{comment.body}}!
</li>!
</ul>!
</body>
Demo
Filters
<ul>!
<li ng-repeat='c in comments |
orderBy:"date"'>!
{{c.author | uppercase}}!
</li>!
</ul>
Filters
<ul>!
<li ng-repeat='c in comments |
orderBy:"date"'>!
{{c.author | uppercase}}!
</li>!
</ul>
Filters
<ul>!
<li ng-repeat='c in comments |
orderBy:"date"'>!
{{c.author | uppercase}}!
</li>!
</ul>
<ul>!
<li ng-repeat='c in comments |
orderBy:"date"'>!
{{c.author | uppercase}}!
</li>!
</ul>
Filters
Demo
Controllers,
Templates, and Scope
Controllers, Templates and
Scope
Template
Controllers, Templates and
Scope
Template Controller
Controllers, Templates and
Scope
Template Controller
Controllers, Templates and
Scope
Template Controller
Demo
Modules
Module
angular.module('app', []);
Module
Config
angular.module('app', []);
Module
Config Controller
angular.module('app', []);
Module
Config Controller Factories
angular.module('app', []);
Module
Config Controller Factories Directives
angular.module('app', []);
Module
Config Controller Factories Directives Filters
angular.module('app', []);
Module
Config Controller Factories Directives Filters
Routes
angular.module('app', []);
Demo
Routing
App.config(function($routeProvider, $locationProvider) {!
$locationProvider.html5Mode(true);!
!
$routeProvider.when('/posts', {!
controller: 'PostsIndexController',!
templateUrl: 'posts/index.html'!
})!
.when('/posts/:id',{!
controller: 'PostsShowController',!
templateUrl: 'posts/show.html'!
})!
.otherwise({!
redirectTo: '/posts'!
});!
});
Routing
App.config(function($routeProvider, $locationProvider) {!
$locationProvider.html5Mode(true);!
!
$routeProvider.when('/posts', {!
controller: 'PostsIndexController',!
templateUrl: 'posts/index.html'!
})!
.when('/posts/:id',{!
controller: 'PostsShowController',!
templateUrl: 'posts/show.html'!
})!
.otherwise({!
redirectTo: '/posts'!
});!
});
Routing
App.config(function($routeProvider, $locationProvider) {!
$locationProvider.html5Mode(true);!
!
$routeProvider.when('/posts', {!
controller: 'PostsIndexController',!
templateUrl: 'posts/index.html'!
})!
.when('/posts/:id',{!
controller: 'PostsShowController',!
templateUrl: 'posts/show.html'!
})!
.otherwise({!
redirectTo: '/posts'!
});!
});
Routing
App.config(function($routeProvider, $locationProvider) {!
$locationProvider.html5Mode(true);!
!
$routeProvider.when('/posts', {!
controller: 'PostsIndexController',!
templateUrl: 'posts/index.html'!
})!
.when('/posts/:id',{!
controller: 'PostsShowController',!
templateUrl: 'posts/show.html'!
})!
.otherwise({!
redirectTo: '/posts'!
});!
});
Routing
App.config(function($routeProvider, $locationProvider) {!
$locationProvider.html5Mode(true);!
!
$routeProvider.when('/posts', {!
controller: 'PostsIndexController',!
templateUrl: 'posts/index.html'!
})!
.when('/posts/:id',{!
controller: 'PostsShowController',!
templateUrl: 'posts/show.html'!
})!
.otherwise({!
redirectTo: '/posts'!
});!
});
Routing
App.config(function($routeProvider, $locationProvider) {!
$locationProvider.html5Mode(true);!
!
$routeProvider.when('/posts', {!
controller: 'PostsIndexController',!
templateUrl: 'posts/index.html'!
})!
.when('/posts/:id',{!
controller: 'PostsShowController',!
templateUrl: 'posts/show.html'!
})!
.otherwise({!
redirectTo: '/posts'!
});!
});
Routing
App.config(function($routeProvider, $locationProvider) {!
$locationProvider.html5Mode(true);!
!
$routeProvider.when('/posts', {!
controller: 'PostsIndexController',!
templateUrl: 'posts/index.html'!
})!
.when('/posts/:id',{!
controller: 'PostsShowController',!
templateUrl: 'posts/show.html'!
})!
.otherwise({!
redirectTo: '/posts'!
});!
});
Routing
App.config(function($routeProvider, $locationProvider) {!
$locationProvider.html5Mode(true);!
!
$routeProvider.when('/posts', {!
controller: 'PostsIndexController',!
templateUrl: 'posts/index.html'!
})!
.when('/posts/:id',{!
controller: 'PostsShowController',!
templateUrl: 'posts/show.html'!
})!
.otherwise({!
redirectTo: '/posts'!
});!
});
Routing
App.config(function($routeProvider, $locationProvider) {!
$locationProvider.html5Mode(true);!
!
$routeProvider.when('/posts', {!
controller: 'PostsIndexController',!
templateUrl: 'posts/index.html'!
})!
.when('/posts/:id',{!
controller: 'PostsShowController',!
templateUrl: 'posts/show.html'!
})!
.otherwise({!
redirectTo: '/posts'!
});!
});
Routing
App.config(function($routeProvider, $locationProvider) {!
$locationProvider.html5Mode(true);!
!
$routeProvider.when('/posts', {!
controller: 'PostsIndexController',!
templateUrl: 'posts/index.html'!
})!
.when('/posts/:id',{!
controller: 'PostsShowController',!
templateUrl: 'posts/show.html'!
})!
.otherwise({!
redirectTo: '/posts'!
});!
});
Routing
App.config(function($routeProvider, $locationProvider) {!
$locationProvider.html5Mode(true);!
!
$routeProvider.when('/posts', {!
controller: 'PostsIndexController',!
templateUrl: 'posts/index.html'!
})!
.when('/posts/:id',{!
controller: 'PostsShowController',!
templateUrl: 'posts/show.html'!
})!
.otherwise({!
redirectTo: '/posts'!
});!
});
Routing
Demo
Custom Directives
and Event Handling
App.directive("upCaser", function() {!
return {!
link: function(scope, el, attrs) {!
$(el).find('p').each(function(i, p) {!
p = $(p);!
p.text(p.text().toUpperCase());!
});!
}!
};!
});
Directives
DirectivesApp.directive("upCaser", function() {!
return {!
link: function(scope, el, attrs) {!
$(el).find('p').each(function(i, p) {!
p = $(p);!
p.text(p.text().toUpperCase());!
});!
}!
};!
});
App.directive("upCaser", function() {!
return {!
link: function(scope, el, attrs) {!
$(el).find('p').each(function(i, p) {!
p = $(p);!
p.text(p.text().toUpperCase());!
});!
}!
};!
});
Directives
App.directive("upCaser", function() {!
return {!
link: function(scope, el, attrs) {!
$(el).find('p').each(function(i, p) {!
p = $(p);!
p.text(p.text().toUpperCase());!
});!
}!
};!
});
Directives
App.directive("upCaser", function() {!
return {!
link: function(scope, el, attrs) {!
$(el).find('p').each(function(i, p) {!
p = $(p);!
p.text(p.text().toUpperCase());!
});!
}!
};!
});
Directives
App.directive("upCaser", function() {!
return {!
link: function(scope, el, attrs) {!
$(el).find('p').each(function(i, p) {!
p = $(p);!
p.text(p.text().toUpperCase());!
});!
}!
};!
});
Directives
App.directive("upCaser", function() {!
return {!
link: function(scope, el, attrs) {!
$(el).find('p').each(function(i, p) {!
p = $(p);!
p.text(p.text().toUpperCase());!
});!
}!
};!
});
Directives
App.directive("upCaser", function() {!
return {!
link: function(scope, el, attrs) {!
$(el).find('p').each(function(i, p) {!
p = $(p);!
p.text(p.text().toUpperCase());!
});!
}!
};!
});
Directives
<div up-caser>!
<p>some text</p>!
<p>some other text</p>!
</div>
Directives
Directives
<up-caser>!
<p>some text</p>!
<p>some other text</p>!
</up-caser>
App.directive("upCaser", function() {!
return {!
restrict: 'E',!
link: function(scope, el, attrs) {!
$(el).find('p').each(function(i, p) {!
p = $(p);!
p.text(p.text().toUpperCase());!
});!
}!
};!
});
Directives
Directives
<up-caser>!
<p>some text</p>!
<p>some other text</p>!
</up-caser>
App.directive("upCaser", function() {!
return {!
restrict: 'AEC',!
link: function(scope, el, attrs) {!
$(el).find('p').each(function(i, p) {!
p = $(p);!
p.text(p.text().toUpperCase());!
});!
}!
};!
});
Directives
Demo
Events
Scope
Events
$broadcast
$emit
App.directive("alerter", function() {!
return {!
restrict: 'E',!
link: function(scope, el, attrs) {!
setTimeout(function() {!
scope.$emit("up", "Hello Up!");!
scope.$broadcast("down", "Hello Down!");!
scope.$apply();!
}, 3000);!
}!
};!
});
Events
App.directive("alerter", function() {!
return {!
restrict: 'E',!
link: function(scope, el, attrs) {!
setTimeout(function() {!
scope.$emit("up", "Hello Up!");!
scope.$broadcast("down", "Hello Down!");!
scope.$apply();!
}, 3000);!
}!
};!
});
Events
App.directive("alerter", function() {!
return {!
restrict: 'E',!
link: function(scope, el, attrs) {!
setTimeout(function() {!
scope.$emit("up", "Hello Up!");!
scope.$broadcast("down", "Hello Down!");!
scope.$apply();!
}, 3000);!
}!
};!
});
Events
App.directive("alerter", function() {!
return {!
restrict: 'E',!
link: function(scope, el, attrs) {!
setTimeout(function() {!
scope.$emit("up", "Hello Up!");!
scope.$broadcast("down", "Hello Down!");!
scope.$apply();!
}, 3000);!
}!
};!
});
Events
App.directive("alerter", function() {!
return {!
restrict: 'E',!
link: function(scope, el, attrs) {!
setTimeout(function() {!
scope.$emit("up", "Hello Up!");!
scope.$broadcast("down", "Hello Down!");!
scope.$apply();!
}, 3000);!
}!
};!
});
Events
App.directive("alerter", function() {!
return {!
restrict: 'E',!
link: function(scope, el, attrs) {!
setTimeout(function() {!
scope.$emit("up", "Hello Up!");!
scope.$broadcast("down", "Hello Down!");!
scope.$apply();!
}, 3000);!
}!
};!
});
Events
App.controller('SomeController', function($scope) {!
$scope.$on("up", function(data, message) {!
$scope.up_message = message;!
});!
});
Events
App.controller('SomeController', function($scope) {!
$scope.$on("up", function(data, message) {!
$scope.up_message = message;!
});!
});
Events
App.controller('SomeController', function($scope) {!
$scope.$on("up", function(data, message) {!
$scope.up_message = message;!
});!
});
Events
App.controller('SomeController', function($scope) {!
$scope.$on("up", function(data, message) {!
$scope.up_message = message;!
});!
});
Events
App.controller('SomeController', function($scope) {!
$scope.$on("up", function(data, message) {!
$scope.up_message = message;!
});!
});
Events
App.controller('SomeController', function($scope) {!
$scope.$on("up", function(data, message) {!
$scope.up_message = message;!
});!
});
Events
Demo
App.controller('Main', function($scope) {!
$scope.clicker = function() {!
$scope.pressed = true;!
};!
});
Events
App.directive("alerter", function() {!
return {!
restrict: 'E',!
link: function(scope, el, attrs) {!
scope.$watch('pressed', function() {!
if (scope.pressed) {!
$(el).text('Oi!');!
}!
});!
}!
};!
});
Events
App.directive("alerter", function() {!
return {!
restrict: 'E',!
link: function(scope, el, attrs) {!
scope.$watch('pressed', function() {!
if (scope.pressed) {!
$(el).text('Oi!');!
}!
});!
}!
};!
});
Events
Demo
Testing
App.controller('FooController', function($scope) {!
!
$scope.setFoo = function(val) {!
$scope.foo = val;!
};!
!
});
Testing
describe('FooController', function() {!
!
beforeEach(function() {module('app')});!
!
beforeEach(inject(function($rootScope, $controller)
{!
this.scope = $rootScope.$new();!
$controller('FooController', {$scope: this.scope})!
}));!
!
describe('setFoo()', function() {!
!
it('sets the foo value', function() {!
expect(this.scope.foo).not.toBeDefined();!
this.scope.setFoo('Bar');!
expect(this.scope.foo).toEqual('Bar');!
});!
});!
});
Testing
describe('FooController', function() {!
!
beforeEach(function() {module('app')});!
!
beforeEach(inject(function($rootScope, $controller)
{!
this.scope = $rootScope.$new();!
$controller('FooController', {$scope: this.scope})!
}));!
!
describe('setFoo()', function() {!
!
it('sets the foo value', function() {!
expect(this.scope.foo).not.toBeDefined();!
this.scope.setFoo('Bar');!
expect(this.scope.foo).toEqual('Bar');!
});!
});!
});
Testing
describe('FooController', function() {!
!
beforeEach(function() {module('app')});!
!
beforeEach(inject(function($rootScope, $controller)
{!
this.scope = $rootScope.$new();!
$controller('FooController', {$scope: this.scope})!
}));!
!
describe('setFoo()', function() {!
!
it('sets the foo value', function() {!
expect(this.scope.foo).not.toBeDefined();!
this.scope.setFoo('Bar');!
expect(this.scope.foo).toEqual('Bar');!
});!
});!
});
Testing
describe('FooController', function() {!
!
beforeEach(function() {module('app')});!
!
beforeEach(inject(function($rootScope, $controller)
{!
this.scope = $rootScope.$new();!
$controller('FooController', {$scope: this.scope})!
}));!
!
describe('setFoo()', function() {!
!
it('sets the foo value', function() {!
expect(this.scope.foo).not.toBeDefined();!
this.scope.setFoo('Bar');!
expect(this.scope.foo).toEqual('Bar');!
});!
});!
});
Testing
describe('FooController', function() {!
!
beforeEach(function() {module('app')});!
!
beforeEach(inject(function($rootScope, $controller)
{!
this.scope = $rootScope.$new();!
$controller('FooController', {$scope: this.scope})!
}));!
!
describe('setFoo()', function() {!
!
it('sets the foo value', function() {!
expect(this.scope.foo).not.toBeDefined();!
this.scope.setFoo('Bar');!
expect(this.scope.foo).toEqual('Bar');!
});!
});!
});
Testing
describe('FooController', function() {!
!
beforeEach(function() {module('app')});!
!
beforeEach(inject(function($rootScope, $controller)
{!
this.scope = $rootScope.$new();!
$controller('FooController', {$scope: this.scope})!
}));!
!
describe('setFoo()', function() {!
!
it('sets the foo value', function() {!
expect(this.scope.foo).not.toBeDefined();!
this.scope.setFoo('Bar');!
expect(this.scope.foo).toEqual('Bar');!
});!
});!
});
Testing
describe('FooController', function() {!
!
beforeEach(function() {module('app')});!
!
beforeEach(inject(function($rootScope, $controller)
{!
this.scope = $rootScope.$new();!
$controller('FooController', {$scope: this.scope})!
}));!
!
describe('setFoo()', function() {!
!
it('sets the foo value', function() {!
expect(this.scope.foo).not.toBeDefined();!
this.scope.setFoo('Bar');!
expect(this.scope.foo).toEqual('Bar');!
});!
});!
});
Testing
describe('FooController', function() {!
!
beforeEach(function() {module('app')});!
!
beforeEach(inject(function($rootScope, $controller)
{!
this.scope = $rootScope.$new();!
$controller('FooController', {$scope: this.scope})!
}));!
!
describe('setFoo()', function() {!
!
it('sets the foo value', function() {!
expect(this.scope.foo).not.toBeDefined();!
this.scope.setFoo('Bar');!
expect(this.scope.foo).toEqual('Bar');!
});!
});!
});
Testing
describe('FooController', function() {!
!
beforeEach(function() {module('app')});!
!
beforeEach(inject(function($rootScope, $controller)
{!
this.scope = $rootScope.$new();!
$controller('FooController', {$scope: this.scope})!
}));!
!
describe('setFoo()', function() {!
!
it('sets the foo value', function() {!
expect(this.scope.foo).not.toBeDefined();!
this.scope.setFoo('Bar');!
expect(this.scope.foo).toEqual('Bar');!
});!
});!
});
Testing
describe('FooController', function() {!
!
beforeEach(function() {module('app')});!
!
beforeEach(inject(function($rootScope, $controller)
{!
this.scope = $rootScope.$new();!
$controller('FooController', {$scope: this.scope})!
}));!
!
describe('setFoo()', function() {!
!
it('sets the foo value', function() {!
expect(this.scope.foo).not.toBeDefined();!
this.scope.setFoo('Bar');!
expect(this.scope.foo).toEqual('Bar');!
});!
});!
});
Testing
describe('FooController', function() {!
!
beforeEach(function() {module('app')});!
!
beforeEach(inject(function($rootScope, $controller)
{!
this.scope = $rootScope.$new();!
$controller('FooController', {$scope: this.scope})!
}));!
!
describe('setFoo()', function() {!
!
it('sets the foo value', function() {!
expect(this.scope.foo).not.toBeDefined();!
this.scope.setFoo('Bar');!
expect(this.scope.foo).toEqual('Bar');!
});!
});!
});
Testing
Demo
Part 2
Setup!
Base Project!
github.com/markbates/
fluent-2014
Node.js!
http://nodejs.org
Lineman.js!
npm install -g
lineman@0.27.2
Install Modules!
npm install
Code Time!!
Thanks!
@markbates	

www.angularmasterclass.com

Angular.js Fundamentals

  • 1.
  • 2.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
    • Controllers! • ngRoute! •Templates! • ngResource! • Directives! • Filters! • Scope! • Testing! • Code Organization! • Best Practices
  • 12.
    Part 1 • Features/Why Angular?! •Getting Started/Setting Up! • Directives, Filters, and Data Binding! • Controllers, Templates, and Scope! • Modules! • Routing! • Custom Directives and Event Handling! • Testing
  • 13.
  • 14.
  • 15.
  • 16.
    Features • Plain JavaScript •Data Binding • Routing/PushState • Testing • Templates/Directives/ Controllers • Modular • Dependency Injection • jqLite • Lightweight
  • 17.
  • 18.
  • 19.
    Backbone.js “minimal set ofdata-structure and view primitives for building web application with JavaScript”
  • 20.
    Ember “framework for creatingambitious web applications”
  • 21.
    AngularJS “Toolset for buildingthe framework most suited to your application development”
  • 24.
  • 25.
  • 26.
    AngularJS Ember Backbone.js base109kb 264kb 6.5kb templating language built-in 90kb (handlebars) ?? data adapter built-in 75kb (ember-data) 84kb (jQuery) support N/A 84kb (jQuery) 17kb (json2.js) 5.0kb (underscore.js ) 109kb 513kb 112.5kb
  • 27.
  • 28.
  • 29.
    AngularJS Backbone.js Ember Watchers2,155 1,442 824 Stars 21,408 17,291 9,570 Forks 6,670 3,783 2,044 Github
  • 30.
  • 31.
    Backbone.jsclass  App.Beer  extends   Backbone.Model       class  App.Beers  extends   Backbone.Collection   !  model:  Beer
  • 32.
    EmberApp.Beer  =  DS.Model.extend      title:  DS.attr("string")      abv:  DS.attr("number")      country_id:  DS.attr("number")      brewery_id:  DS.attr("number")      brewery:   DS.belongsTo("App.Brewery")      country:   DS.belongsTo("App.Country")
  • 33.
  • 34.
  • 35.
    Backbone.jsclass  App.Beer  extends  Backbone.Model      urlRoot:  "/api/v1/beers"         class  App.Beers  extends  Backbone.Collection          url:  -­‐>          if  @brewery_id?              return  "/api/v1/breweries/ #{@brewery_id}/beers"          else              return  "/api/v1/beers"          model:  Beer
  • 36.
    EmberApp.Beer  =  DS.Model.extend      title:  DS.attr("string")      abv:  DS.attr("number")      country_id:   DS.attr("number")      brewery_id:   DS.attr("number")      brewery:   DS.belongsTo("App.Brewery")      country:   DS.belongsTo("App.Country")
  • 37.
    Ember DS.RESTAdapter.reopen      namespace:  'api/v1'       App.Store  =  DS.Store.extend      revision:  14      adapter:  DS.RESTAdapter.create()
  • 38.
    AngularJS App.factory  "Beer",  ($resource)  -­‐>      return  $resource  "/api/v1/ beers/:id",                                        {id:  "@id"},                                        {update:  {method:   "PUT"}}
  • 39.
  • 40.
    Backbone.js@Router  =  Backbone.Router.extend          initialize:  -­‐>          @countries  =  new  Countries()          routes:          "breweries/:brewery_id":  "brewery"          "breweries/:brewery_id/edit":  "breweryEdit"          brewery:  (brewery_id)  -­‐>          @changeView(new  BreweryView(collection:  @countries,  model:  new   Brewery(id:  brewery_id)))          breweryEdit:  (brewery_id)  -­‐>          @changeView(new  BreweryEditView(collection:  @countries,  model:  new   Brewery(id:  brewery_id)))          changeView:  (view)  =>          @currentView?.remove()          @currentView  =  view          $("#outlet").html(@currentView.el)
  • 41.
    Ember App.Router.map  -­‐>      @resource  "brewery",  {path:  "brewery/:brewery_id"}
  • 42.
    EmberApp.BreweryRoute  =   Ember.Route.extend      model:  (params)-­‐>           App.Brewery.find(params.brewery_i d)
  • 43.
    AngularJSApp.config  ($routeProvider)  -­‐>          $routeProvider          .when("/breweries/:id",  {              templateUrl:  "/assets/ brewery.html",              controller:  "BreweryController"          })          .when("/breweries/:id/edit",  {              templateUrl:  "/assets/ edit_brewery.html",              controller:   "EditBreweryController"          })
  • 44.
  • 45.
    Backbone.jsclass  @BreweryEditView  extends  Backbone.View          template:  "brewery_edit"          events:          "click  #save-­‐button":  "saveClicked"          "keypress  #brewery-­‐title":  "titleEdited"          initialize:  -­‐>          super          @countriesView  =  new   CountriesView(collection:  @collection)          @$el.html(@countriesView.el)          @model.on  "change",  @render          @model.fetch()          render:  =>          @$("#country-­‐ outlet").html(@renderTemplate())          return  @    saveClicked:  (e)  =>          e?.preventDefault()          attrs  =              title:  @$("#brewery-­‐title").val()              synonyms:  @$("#brewery-­‐synonyms").val()              address:  @$("#brewery-­‐address").val()          @model.save  attrs,              success:  (model,  response,  options)  =>                  App.navigate("/breweries/#{@model.id}",   trigger:  true)              error:  (model,  xhr,  options)  -­‐>                  errors  =  []                  for  key,  value  of   xhr.responseJSON.errors                      errors.push  "#{key}:  #{value.join(",   ")}"                  alert  errors.join("n")          titleEdited:  (e)  =>          title  =  @$("#brewery-­‐title").val()          @$("h2").text(title)   !    #  further  code  omitted
  • 46.
    Ember App.BreweryController  =  Ember.ObjectController.extend        save:  -­‐>          @store.commit()          #  further  code  omitted
  • 47.
    AngularJS@EditBreweryController  =  ($scope,  $routeParams,  $location,   Brewery)  -­‐>          $scope.brewery  =  Brewery.get(id:  $routeParams.id)          $scope.save  =  -­‐>          success  =  -­‐>              $location.path("/breweries/#{$routeParams.id}")              $scope.errors  =  null          failure  =  (object)-­‐>              $scope.errors  =  object.data.errors          $scope.brewery.$update  {},  success,  failure
  • 48.
  • 49.
    Backbone.js <h2><%=  @model.displayName()  %></h2>       <form>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="title">Title</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'  value='<%=  @model.get("title")   %>'id='brewery-­‐title'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="synonyms">Synonyms</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'  value='<%=  @model.get("synonyms")   %>'id='brewery-­‐synonyms'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="address">Address</label>          <div  class="controls">              <textarea  class='input-­‐xxlarge'  id='brewery-­‐address'><%=  @model.get("address")   %></textarea>          </div>      </div>          <button  class='btn  btn-­‐primary'  id='save-­‐button'>Save</button>      <a  href="/breweries/<%=  @model.id  %>"  class='btn'>Cancel</a>       </form>
  • 50.
    Backbone.js <h2><%=  @model.displayName()  %></h2>       <form>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="title">Title</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'   value='<%=  @model.get("title")  %>'id='brewery-­‐ title'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="synonyms">Synonyms</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'  value='<%=  @model.get("synonyms")  %>'id='brewery-­‐synonyms'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="address">Address</label>          <div  class="controls">              <textarea  class='input-­‐xxlarge'  id='brewery-­‐address'><%=  @model.get("address")  %></textarea>          </div>      </div>          <button  class='btn  btn-­‐primary'  id='save-­‐button'>Save</button>      <a  href="/breweries/<%=  @model.id  %>"  class='btn'>Cancel</a>       </form>
  • 51.
    Backbone.js<h2><%=  @model.displayName()  %></h2>       <form>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="title">Title</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'  value='<%=  @model.get("title")  %>'id='brewery-­‐title'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="synonyms">Synonyms</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'  value='<%=  @model.get("synonyms")  %>'id='brewery-­‐synonyms'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="address">Address</label>          <div  class="controls">              <textarea  class='input-­‐xxlarge'  id='brewery-­‐address'>< %=  @model.get("address")  %></textarea>          </div>      </div>          <button  class='btn  btn-­‐primary'  id='save-­‐button'>Save</button>      <a  href="/breweries/<%=  @model.id  %>"  class='btn'>Cancel</a>       </form>
  • 52.
    <div  class='span12'>      <h2>{{displayName}}</h2>      <h3>          {{cityState}}          {{#linkTo  "country"  country}}              {{country.title}}          {{/linkTo}}      </h3>      {{#if  isEditing}}          <form>                  <div  class="control-­‐group">                  <label  class="control-­‐label"  for="title">Title</label>                  <div  class="controls">                      {{view  Ember.TextField  valueBinding="title"  class='input-­‐xxlarge'}}                  </div>              </div>                  <div  class="control-­‐group">                  <label  class="control-­‐label"  for="synonyms">Synonyms</label>                  <div  class="controls">                      {{view  Ember.TextField  valueBinding="synonyms"  class='input-­‐xxlarge'}}                  </div>              </div>                  <div  class="control-­‐group">                  <label  class="control-­‐label"  for="synonyms">Synonyms</label>                  <div  class="controls">                      {{view  Ember.TextArea  valueBinding="address"  class='input-­‐xxlarge'}}                  </div>              </div>                  <button  class='btn  btn-­‐primary'  {{action  "save"}}>Save</button>              </form>      {{  else  }}          {{  partial  "brewery/show"  }}      {{/if}}   </div> Ember
  • 53.
    <div  class='span12'>      <h2>{{displayName}}</h2>      <h3>          {{cityState}}          {{#linkTo  "country"  country}}              {{country.title}}          {{/linkTo}}      </h3>      {{#if  isEditing}}          <form>                  <div  class="control-­‐group">                  <label  class="control-­‐label"  for="title">Title</label>                  <div  class="controls">                      {{view  Ember.TextField  valueBinding="title"   class='input-­‐xxlarge'}}                  </div>              </div>                  <div  class="control-­‐group">                  <label  class="control-­‐label"  for="synonyms">Synonyms</label>                  <div  class="controls">                      {{view  Ember.TextField  valueBinding="synonyms"  class='input-­‐xxlarge'}}                  </div>              </div>                  <div  class="control-­‐group">                  <label  class="control-­‐label"  for="synonyms">Synonyms</label>                  <div  class="controls">                      {{view  Ember.TextArea  valueBinding="address"  class='input-­‐xxlarge'}}                  </div>              </div>                  <button  class='btn  btn-­‐primary'  {{action  "save"}}>Save</button>              </form>      {{  else  }}          {{  partial  "brewery/show"  }}      {{/if}}   </div> Ember
  • 54.
    <div  class='span12'>      <h2>{{displayName}}</h2>      <h3>          {{cityState}}          {{#linkTo  "country"  country}}              {{country.title}}          {{/linkTo}}      </h3>      {{#if  isEditing}}          <form>                  <div  class="control-­‐group">                  <label  class="control-­‐label"  for="title">Title</label>                  <div  class="controls">                      {{view  Ember.TextField  valueBinding="title"  class='input-­‐xxlarge'}}                  </div>              </div>                  <div  class="control-­‐group">                  <label  class="control-­‐label"  for="synonyms">Synonyms</label>                  <div  class="controls">                      {{view  Ember.TextField  valueBinding="synonyms"  class='input-­‐xxlarge'}}                  </div>              </div>                  <div  class="control-­‐group">                  <label  class="control-­‐label"  for="synonyms">Synonyms</label>                  <div  class="controls">                      {{view  Ember.TextArea  valueBinding="address"  class='input-­‐xxlarge'}}                  </div>              </div>              <button  class='btn  btn-­‐primary'  {{action   "save"}}>Save</button>          </form>      {{  else  }}          {{  partial  "brewery/show"  }}      {{/if}}   </div> Ember
  • 55.
    <form>      <h3>{{brewery.title}}</h3>      <div  ng-­‐include='"/assets/_errors.html"'></div>      <div  class="control-­‐group">          <label  class="control-­‐label"  for="title">Title</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'  ng-­‐ model='brewery.title'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="synonyms">Synonyms</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'  ng-­‐ model='brewery.synonyms'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="address">Address</label>          <div  class="controls">              <textarea  class='input-­‐xxlarge'  ng-­‐model='brewery.address'></ textarea>          </div>      </div>          <button  class='btn  btn-­‐primary'  ng-­‐click='save()'>Save</button>       </form> AngularJS
  • 56.
    AngularJS<form>      <h3>{{brewery.title}}</h3>      <div  ng-­‐include='"/assets/_errors.html"'></div>      <div  class="control-­‐group">          <label  class="control-­‐label"  for="title">Title</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'  ng-­‐ model='brewery.title'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="synonyms">Synonyms</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'  ng-­‐model='brewery.synonyms'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="address">Address</label>          <div  class="controls">              <textarea  class='input-­‐xxlarge'  ng-­‐model='brewery.address'></textarea>          </div>      </div>          <button  class='btn  btn-­‐primary'  ng-­‐click='save()'>Save</button>       </form>
  • 57.
    <form>      <h3>{{brewery.title}}</h3>      <div  ng-­‐include='"/assets/_errors.html"'></div>      <div  class="control-­‐group">          <label  class="control-­‐label"  for="title">Title</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'  ng-­‐model='brewery.title'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="synonyms">Synonyms</label>          <div  class="controls">              <input  type='text'  class='input-­‐xxlarge'  ng-­‐model='brewery.synonyms'>          </div>      </div>          <div  class="control-­‐group">          <label  class="control-­‐label"  for="address">Address</label>          <div  class="controls">              <textarea  class='input-­‐xxlarge'  ng-­‐model='brewery.address'></textarea>          </div>      </div>      <button  class='btn  btn-­‐primary'  ng-­‐ click='save()'>Save</button>    </form> AngularJS
  • 58.
  • 59.
    Backbone.js • Too simple •Not opinionated enough • “Memory” management • Unstructured • Spaghetti code • Lightweight • Not opinionated • Simple • Easy to read source • “widget” development Pros Cons
  • 60.
    Ember • Too complex •Overly opinionated • Heavyweight • ember-data - not production ready (very buggy) • Little to no mind-share outside of Rails • Difficult to read source code • Structured • Highly opinionated • “less” code • “large” apps Pros Cons
  • 61.
    AngularJS • Difficult toread source code • jQuery plugins require custom directives • Large apps requiring self- imposed structure • Lightly structured • Lightly opinionated • “less” code • Plain JavaScript • Simple/Powerful • Easy to test • Lightweight • small, medium, or large apps Pros Cons
  • 62.
  • 69.
  • 70.
  • 71.
    Directives <body ng-app>! <ng-view></ng-view>! <ul>! <li ng-repeat='commentin comments'>! {{comment.body}}! </li>! </ul>! </body>
  • 72.
    Directives <body ng-app>! <ng-view></ng-view>! <ul>! <li ng-repeat='commentin comments'>! {{comment.body}}! </li>! </ul>! </body>
  • 73.
    Directives <body ng-app>! <ng-view></ng-view>! <ul>! <li ng-repeat='commentin comments'>! {{comment.body}}! </li>! </ul>! </body>
  • 74.
    Directives <body ng-app>! <ng-view></ng-view>! <ul>! <li ng-repeat='commentin comments'>! {{comment.body}}! </li>! </ul>! </body>
  • 75.
    Directives <body ng-app>! <ng-view></ng-view>! <ul>! <li ng-repeat='commentin comments'>! {{comment.body}}! </li>! </ul>! </body>
  • 76.
  • 77.
    Filters <ul>! <li ng-repeat='c incomments | orderBy:"date"'>! {{c.author | uppercase}}! </li>! </ul>
  • 78.
    Filters <ul>! <li ng-repeat='c incomments | orderBy:"date"'>! {{c.author | uppercase}}! </li>! </ul>
  • 79.
    Filters <ul>! <li ng-repeat='c incomments | orderBy:"date"'>! {{c.author | uppercase}}! </li>! </ul>
  • 80.
    <ul>! <li ng-repeat='c incomments | orderBy:"date"'>! {{c.author | uppercase}}! </li>! </ul> Filters
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
    Module Config Controller FactoriesDirectives angular.module('app', []);
  • 94.
    Module Config Controller FactoriesDirectives Filters angular.module('app', []);
  • 95.
    Module Config Controller FactoriesDirectives Filters Routes angular.module('app', []);
  • 96.
  • 97.
  • 98.
    App.config(function($routeProvider, $locationProvider) {! $locationProvider.html5Mode(true);! ! $routeProvider.when('/posts',{! controller: 'PostsIndexController',! templateUrl: 'posts/index.html'! })! .when('/posts/:id',{! controller: 'PostsShowController',! templateUrl: 'posts/show.html'! })! .otherwise({! redirectTo: '/posts'! });! }); Routing
  • 99.
    App.config(function($routeProvider, $locationProvider) {! $locationProvider.html5Mode(true);! ! $routeProvider.when('/posts',{! controller: 'PostsIndexController',! templateUrl: 'posts/index.html'! })! .when('/posts/:id',{! controller: 'PostsShowController',! templateUrl: 'posts/show.html'! })! .otherwise({! redirectTo: '/posts'! });! }); Routing
  • 100.
    App.config(function($routeProvider, $locationProvider) {! $locationProvider.html5Mode(true);! ! $routeProvider.when('/posts',{! controller: 'PostsIndexController',! templateUrl: 'posts/index.html'! })! .when('/posts/:id',{! controller: 'PostsShowController',! templateUrl: 'posts/show.html'! })! .otherwise({! redirectTo: '/posts'! });! }); Routing
  • 101.
    App.config(function($routeProvider, $locationProvider) {! $locationProvider.html5Mode(true);! ! $routeProvider.when('/posts',{! controller: 'PostsIndexController',! templateUrl: 'posts/index.html'! })! .when('/posts/:id',{! controller: 'PostsShowController',! templateUrl: 'posts/show.html'! })! .otherwise({! redirectTo: '/posts'! });! }); Routing
  • 102.
    App.config(function($routeProvider, $locationProvider) {! $locationProvider.html5Mode(true);! ! $routeProvider.when('/posts',{! controller: 'PostsIndexController',! templateUrl: 'posts/index.html'! })! .when('/posts/:id',{! controller: 'PostsShowController',! templateUrl: 'posts/show.html'! })! .otherwise({! redirectTo: '/posts'! });! }); Routing
  • 103.
    App.config(function($routeProvider, $locationProvider) {! $locationProvider.html5Mode(true);! ! $routeProvider.when('/posts',{! controller: 'PostsIndexController',! templateUrl: 'posts/index.html'! })! .when('/posts/:id',{! controller: 'PostsShowController',! templateUrl: 'posts/show.html'! })! .otherwise({! redirectTo: '/posts'! });! }); Routing
  • 104.
    App.config(function($routeProvider, $locationProvider) {! $locationProvider.html5Mode(true);! ! $routeProvider.when('/posts',{! controller: 'PostsIndexController',! templateUrl: 'posts/index.html'! })! .when('/posts/:id',{! controller: 'PostsShowController',! templateUrl: 'posts/show.html'! })! .otherwise({! redirectTo: '/posts'! });! }); Routing
  • 105.
    App.config(function($routeProvider, $locationProvider) {! $locationProvider.html5Mode(true);! ! $routeProvider.when('/posts',{! controller: 'PostsIndexController',! templateUrl: 'posts/index.html'! })! .when('/posts/:id',{! controller: 'PostsShowController',! templateUrl: 'posts/show.html'! })! .otherwise({! redirectTo: '/posts'! });! }); Routing
  • 106.
    App.config(function($routeProvider, $locationProvider) {! $locationProvider.html5Mode(true);! ! $routeProvider.when('/posts',{! controller: 'PostsIndexController',! templateUrl: 'posts/index.html'! })! .when('/posts/:id',{! controller: 'PostsShowController',! templateUrl: 'posts/show.html'! })! .otherwise({! redirectTo: '/posts'! });! }); Routing
  • 107.
    App.config(function($routeProvider, $locationProvider) {! $locationProvider.html5Mode(true);! ! $routeProvider.when('/posts',{! controller: 'PostsIndexController',! templateUrl: 'posts/index.html'! })! .when('/posts/:id',{! controller: 'PostsShowController',! templateUrl: 'posts/show.html'! })! .otherwise({! redirectTo: '/posts'! });! }); Routing
  • 108.
    App.config(function($routeProvider, $locationProvider) {! $locationProvider.html5Mode(true);! ! $routeProvider.when('/posts',{! controller: 'PostsIndexController',! templateUrl: 'posts/index.html'! })! .when('/posts/:id',{! controller: 'PostsShowController',! templateUrl: 'posts/show.html'! })! .otherwise({! redirectTo: '/posts'! });! }); Routing
  • 109.
  • 110.
  • 111.
    App.directive("upCaser", function() {! return{! link: function(scope, el, attrs) {! $(el).find('p').each(function(i, p) {! p = $(p);! p.text(p.text().toUpperCase());! });! }! };! }); Directives
  • 112.
    DirectivesApp.directive("upCaser", function() {! return{! link: function(scope, el, attrs) {! $(el).find('p').each(function(i, p) {! p = $(p);! p.text(p.text().toUpperCase());! });! }! };! });
  • 113.
    App.directive("upCaser", function() {! return{! link: function(scope, el, attrs) {! $(el).find('p').each(function(i, p) {! p = $(p);! p.text(p.text().toUpperCase());! });! }! };! }); Directives
  • 114.
    App.directive("upCaser", function() {! return{! link: function(scope, el, attrs) {! $(el).find('p').each(function(i, p) {! p = $(p);! p.text(p.text().toUpperCase());! });! }! };! }); Directives
  • 115.
    App.directive("upCaser", function() {! return{! link: function(scope, el, attrs) {! $(el).find('p').each(function(i, p) {! p = $(p);! p.text(p.text().toUpperCase());! });! }! };! }); Directives
  • 116.
    App.directive("upCaser", function() {! return{! link: function(scope, el, attrs) {! $(el).find('p').each(function(i, p) {! p = $(p);! p.text(p.text().toUpperCase());! });! }! };! }); Directives
  • 117.
    App.directive("upCaser", function() {! return{! link: function(scope, el, attrs) {! $(el).find('p').each(function(i, p) {! p = $(p);! p.text(p.text().toUpperCase());! });! }! };! }); Directives
  • 118.
    App.directive("upCaser", function() {! return{! link: function(scope, el, attrs) {! $(el).find('p').each(function(i, p) {! p = $(p);! p.text(p.text().toUpperCase());! });! }! };! }); Directives
  • 119.
    <div up-caser>! <p>some text</p>! <p>someother text</p>! </div> Directives
  • 120.
  • 121.
    App.directive("upCaser", function() {! return{! restrict: 'E',! link: function(scope, el, attrs) {! $(el).find('p').each(function(i, p) {! p = $(p);! p.text(p.text().toUpperCase());! });! }! };! }); Directives
  • 122.
  • 123.
    App.directive("upCaser", function() {! return{! restrict: 'AEC',! link: function(scope, el, attrs) {! $(el).find('p').each(function(i, p) {! p = $(p);! p.text(p.text().toUpperCase());! });! }! };! }); Directives
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
    App.directive("alerter", function() {! return{! restrict: 'E',! link: function(scope, el, attrs) {! setTimeout(function() {! scope.$emit("up", "Hello Up!");! scope.$broadcast("down", "Hello Down!");! scope.$apply();! }, 3000);! }! };! }); Events
  • 129.
    App.directive("alerter", function() {! return{! restrict: 'E',! link: function(scope, el, attrs) {! setTimeout(function() {! scope.$emit("up", "Hello Up!");! scope.$broadcast("down", "Hello Down!");! scope.$apply();! }, 3000);! }! };! }); Events
  • 130.
    App.directive("alerter", function() {! return{! restrict: 'E',! link: function(scope, el, attrs) {! setTimeout(function() {! scope.$emit("up", "Hello Up!");! scope.$broadcast("down", "Hello Down!");! scope.$apply();! }, 3000);! }! };! }); Events
  • 131.
    App.directive("alerter", function() {! return{! restrict: 'E',! link: function(scope, el, attrs) {! setTimeout(function() {! scope.$emit("up", "Hello Up!");! scope.$broadcast("down", "Hello Down!");! scope.$apply();! }, 3000);! }! };! }); Events
  • 132.
    App.directive("alerter", function() {! return{! restrict: 'E',! link: function(scope, el, attrs) {! setTimeout(function() {! scope.$emit("up", "Hello Up!");! scope.$broadcast("down", "Hello Down!");! scope.$apply();! }, 3000);! }! };! }); Events
  • 133.
    App.directive("alerter", function() {! return{! restrict: 'E',! link: function(scope, el, attrs) {! setTimeout(function() {! scope.$emit("up", "Hello Up!");! scope.$broadcast("down", "Hello Down!");! scope.$apply();! }, 3000);! }! };! }); Events
  • 134.
    App.controller('SomeController', function($scope) {! $scope.$on("up",function(data, message) {! $scope.up_message = message;! });! }); Events
  • 135.
    App.controller('SomeController', function($scope) {! $scope.$on("up",function(data, message) {! $scope.up_message = message;! });! }); Events
  • 136.
    App.controller('SomeController', function($scope) {! $scope.$on("up",function(data, message) {! $scope.up_message = message;! });! }); Events
  • 137.
    App.controller('SomeController', function($scope) {! $scope.$on("up",function(data, message) {! $scope.up_message = message;! });! }); Events
  • 138.
    App.controller('SomeController', function($scope) {! $scope.$on("up",function(data, message) {! $scope.up_message = message;! });! }); Events
  • 139.
    App.controller('SomeController', function($scope) {! $scope.$on("up",function(data, message) {! $scope.up_message = message;! });! }); Events
  • 140.
  • 141.
    App.controller('Main', function($scope) {! $scope.clicker= function() {! $scope.pressed = true;! };! }); Events
  • 142.
    App.directive("alerter", function() {! return{! restrict: 'E',! link: function(scope, el, attrs) {! scope.$watch('pressed', function() {! if (scope.pressed) {! $(el).text('Oi!');! }! });! }! };! }); Events
  • 143.
    App.directive("alerter", function() {! return{! restrict: 'E',! link: function(scope, el, attrs) {! scope.$watch('pressed', function() {! if (scope.pressed) {! $(el).text('Oi!');! }! });! }! };! }); Events
  • 144.
  • 145.
  • 147.
    App.controller('FooController', function($scope) {! ! $scope.setFoo= function(val) {! $scope.foo = val;! };! ! }); Testing
  • 148.
    describe('FooController', function() {! ! beforeEach(function(){module('app')});! ! beforeEach(inject(function($rootScope, $controller) {! this.scope = $rootScope.$new();! $controller('FooController', {$scope: this.scope})! }));! ! describe('setFoo()', function() {! ! it('sets the foo value', function() {! expect(this.scope.foo).not.toBeDefined();! this.scope.setFoo('Bar');! expect(this.scope.foo).toEqual('Bar');! });! });! }); Testing
  • 149.
    describe('FooController', function() {! ! beforeEach(function(){module('app')});! ! beforeEach(inject(function($rootScope, $controller) {! this.scope = $rootScope.$new();! $controller('FooController', {$scope: this.scope})! }));! ! describe('setFoo()', function() {! ! it('sets the foo value', function() {! expect(this.scope.foo).not.toBeDefined();! this.scope.setFoo('Bar');! expect(this.scope.foo).toEqual('Bar');! });! });! }); Testing
  • 150.
    describe('FooController', function() {! ! beforeEach(function(){module('app')});! ! beforeEach(inject(function($rootScope, $controller) {! this.scope = $rootScope.$new();! $controller('FooController', {$scope: this.scope})! }));! ! describe('setFoo()', function() {! ! it('sets the foo value', function() {! expect(this.scope.foo).not.toBeDefined();! this.scope.setFoo('Bar');! expect(this.scope.foo).toEqual('Bar');! });! });! }); Testing
  • 151.
    describe('FooController', function() {! ! beforeEach(function(){module('app')});! ! beforeEach(inject(function($rootScope, $controller) {! this.scope = $rootScope.$new();! $controller('FooController', {$scope: this.scope})! }));! ! describe('setFoo()', function() {! ! it('sets the foo value', function() {! expect(this.scope.foo).not.toBeDefined();! this.scope.setFoo('Bar');! expect(this.scope.foo).toEqual('Bar');! });! });! }); Testing
  • 152.
    describe('FooController', function() {! ! beforeEach(function(){module('app')});! ! beforeEach(inject(function($rootScope, $controller) {! this.scope = $rootScope.$new();! $controller('FooController', {$scope: this.scope})! }));! ! describe('setFoo()', function() {! ! it('sets the foo value', function() {! expect(this.scope.foo).not.toBeDefined();! this.scope.setFoo('Bar');! expect(this.scope.foo).toEqual('Bar');! });! });! }); Testing
  • 153.
    describe('FooController', function() {! ! beforeEach(function(){module('app')});! ! beforeEach(inject(function($rootScope, $controller) {! this.scope = $rootScope.$new();! $controller('FooController', {$scope: this.scope})! }));! ! describe('setFoo()', function() {! ! it('sets the foo value', function() {! expect(this.scope.foo).not.toBeDefined();! this.scope.setFoo('Bar');! expect(this.scope.foo).toEqual('Bar');! });! });! }); Testing
  • 154.
    describe('FooController', function() {! ! beforeEach(function(){module('app')});! ! beforeEach(inject(function($rootScope, $controller) {! this.scope = $rootScope.$new();! $controller('FooController', {$scope: this.scope})! }));! ! describe('setFoo()', function() {! ! it('sets the foo value', function() {! expect(this.scope.foo).not.toBeDefined();! this.scope.setFoo('Bar');! expect(this.scope.foo).toEqual('Bar');! });! });! }); Testing
  • 155.
    describe('FooController', function() {! ! beforeEach(function(){module('app')});! ! beforeEach(inject(function($rootScope, $controller) {! this.scope = $rootScope.$new();! $controller('FooController', {$scope: this.scope})! }));! ! describe('setFoo()', function() {! ! it('sets the foo value', function() {! expect(this.scope.foo).not.toBeDefined();! this.scope.setFoo('Bar');! expect(this.scope.foo).toEqual('Bar');! });! });! }); Testing
  • 156.
    describe('FooController', function() {! ! beforeEach(function(){module('app')});! ! beforeEach(inject(function($rootScope, $controller) {! this.scope = $rootScope.$new();! $controller('FooController', {$scope: this.scope})! }));! ! describe('setFoo()', function() {! ! it('sets the foo value', function() {! expect(this.scope.foo).not.toBeDefined();! this.scope.setFoo('Bar');! expect(this.scope.foo).toEqual('Bar');! });! });! }); Testing
  • 157.
    describe('FooController', function() {! ! beforeEach(function(){module('app')});! ! beforeEach(inject(function($rootScope, $controller) {! this.scope = $rootScope.$new();! $controller('FooController', {$scope: this.scope})! }));! ! describe('setFoo()', function() {! ! it('sets the foo value', function() {! expect(this.scope.foo).not.toBeDefined();! this.scope.setFoo('Bar');! expect(this.scope.foo).toEqual('Bar');! });! });! }); Testing
  • 158.
    describe('FooController', function() {! ! beforeEach(function(){module('app')});! ! beforeEach(inject(function($rootScope, $controller) {! this.scope = $rootScope.$new();! $controller('FooController', {$scope: this.scope})! }));! ! describe('setFoo()', function() {! ! it('sets the foo value', function() {! expect(this.scope.foo).not.toBeDefined();! this.scope.setFoo('Bar');! expect(this.scope.foo).toEqual('Bar');! });! });! }); Testing
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.