Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AngularJS and Ember.js Side by Side

Daniel Wanja
January 16, 2014

AngularJS and Ember.js Side by Side

In the spirit of Xmas (I mean the holidays) we will show the same application built once with AngularJS (by @danielwanja) and once with Ember (@theaboutbox) and try to do an honest comparison of the strength and weakness of each the frameworks. We have lots of code and aspects that we will check out side by side and which ever camp you are in you will come out of this talk with a better understanding of what the other framework provides and how well it embraces Rails.

Daniel Wanja

January 16, 2014
Tweet

More Decks by Daniel Wanja

Other Decks in Programming

Transcript

  1. Rails Defaults • Was RailsUJS+jQuery • The future? Turbolinks •

    or Javascript MVC? • Question to ask Why Javascript MVC and not just plain Rails with Turbolinks? http://www.dwellable.com/blog/Rails-Rumble-Gem-Teardown
  2. • Ember.js is an open-source client-side JavaScript web application framework

    based on the model-view-controller software architectural pattern • A framework for creating ambitious web applications What is ember.js (According to Google)
  3. What is Ember.js • Client-side Javascript framework • Template-based HTML

    generation with Handlebars • Two way data binding • Computed properties and observers • Integrated with jQuery • Powerful component model
  4. Ember-Data • Data Persistence library for Ember • Central data

    store • Adapters for REST and Fixtures • Follows active_model_serializer conventions “out of the box”
  5. Differences vs. Angular • Ember imposes its own object model

    on Javascript. • For example, need to call .get() and .set() to access properties: fullName = this.get('firstName') + ' ' + this.get('lastName')
  6. Differences vs. Angular • But with the inconvenience comes power.

    • Computed Freaking Properties! • Having explicit hooks into Ember’s object model makes it easier to integrate third- party javascript libraries.
  7. Small Ember App http://jsfiddle.net/theaboutbox/c9x94/ <script type="text/coffeescript">! App = Ember.Application.create {}!

    ! App.IndexRoute = Ember.Route.extend! model: -> ! firstName: '', lastName: ''! ! App.IndexController = Ember.ObjectController.extend! greeting: ( ->! "Hello #{@get('firstName')} #{@get('lastName')}"! ).property('firstName','lastName')! </script>! ! <script type="text/x-handlebars" data-template- name="index">! <h2>What is your name?</h2>! {{input value=firstName}} {{input value=lastName}}! <p>{{greeting}}</p>! </script>!
  8. Template <script type="text/x-handlebars" data-template-name="index">! <h2>What is your name?</h2>! {{input value=firstName}}!

    {{input value=lastName}}! <p>{{fullName}}</p>! </script>! firstName and lastName are model properties fullName is a computed property
  9. Route App.IndexRoute = Ember.Route.extend! model: -> ! firstName: '', lastName:

    '' • The route is responsible for wiring the model, the controller and the template together • If no controller is specified, it will default to IndexController. • If no template is specified it will use index
  10. Test Example test 'Search box on home page', ->! visit('/').then

    -> ok(find('.search').length, 'Search box not present')! ! test 'Has Recipes', ->! visit('/').then ->! ok(find('.recipe').length, 'There are no recipes on the page')! ! test 'Links to recipes', ->! visit('/').then ->! ok(find('a[href="/recipe/1"]').length, 'No recipe link')!
  11. Rails Integration • ember-rails gem is really good • ember-data

    works great with active_model_serializers • Keep to the golden path and everything will be fine.
  12. Recipe Integration Tests module 'Home Page Tests',! setup: -> !

    App.Recipe.FIXTURES = [! { id: 1, title: 'Taco', description: 'Crunchy and delicious', image_url: 'http://placekitten.com/72/72'}! ]! App.reset()! ! test 'Search box on home page', ->! visit('/').then -> ok(find('.search').length, 'Search box not present')! ! test 'Has Recipes', ->! visit('/').then ->! ok(find('.recipe').length, 'There are no recipes on the page')! ! test 'Links to recipes', ->! visit('/').then ->! ok(find('a[href="/recipe/1"]').length, 'No recipe link')!
  13. Ember Recipe Models App.Recipe = DS.Model.extend! title: DS.attr 'string'! description:

    DS.attr 'string'! image_url: DS.attr 'string'! created_at: DS.attr 'date'! updated_at: DS.attr 'date'! ! user: DS.belongsTo('user', async: true)! ingredients: DS.hasMany('ingredient', async: true)! steps: DS.hasMany('step', async: true) App.Ingredient = DS.Model.extend! amount: DS.attr 'string'! unit: DS.attr 'string'! description: DS.attr 'string'! recipe: DS.belongsTo('recipe')! App.Step = DS.Model.extend! position: DS.attr 'number'! description: DS.attr 'string'!
  14. Implement the recipe page in Ember <table class="table table-striped table-bordered">!

    <tr>! <th>Twitter Handle</th>! <th>Image</th>! <th>Title</th>! <th>Description</th>! <th>Actions</th>! </tr>! {{#each recipe in controller}}! <tr class="recipe">! <td></td>! <td><img class="img-thumbnail" style="width: 64px;" ! {{bindAttr src=recipe.image_url}}/></td>! <td>{{recipe.title}}</td>! <td>{{recipe.description}}</td>! <td>{{#linkTo 'recipe' recipe}}Show{{/linkTo}}</td>! </tr>! {{/each}}! </table>!
  15. What is AngularJS  according to google • Superheroic JavaScript

    MVW Framework • HTML enhanced for web apps! • lets you extend HTML vocabulary for your application • is extraordinarily expressive, readable, and quick to develop.
  16. What is AngularJS • 100% client side framework • DOM

    based HTML generation • Two-way Binding • Model mutation observation • Promise • <3 jQuery DOM manipulation (but doesn’t require it) • Maintainability (dependency injection)
  17. •Easy to get started with •Little code, big results •Two

    way data bindings •Bind to base JavaScript Objects (not extending a base class) •Promising extensions and ecosystem •Fully tested Why AngularJS ? according to Daniel
  18. Directives App and controller ngApp ngController ngInit ! Model ngModel

    ! HTML generation ngInclude ngRepeat ngSwitch Event Handling ngChange ngClick ngDblclick ngMousedown ngMouseenter ngMouseleave ngMousemove ngMouseover ngMouseup Dynamic urls ngHref ngSrc Input manipulation ngSelected ngReadonly ngChecked ngDisabled ngList ngMultiple Form ngForm ngSubmit HTML Style changes ngClass ngClassEven ngClassOdd ngShow ngHide ngStyle Binding ngBind ngBindHtmlUnsafe ngBindTemplate Miscelleanous ngCloak ngCsp ngNonBindable ngPluralize
  19. Minimal AngularJS App <html ng-app>! <head>! <title>01.01-binding</title>! <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.5/ angular.min.js"></script>!

    </head>! <body>! <label>Name:</label>! <input type="text" ng-model="yourName" placeholder="Enter a name here">! <h1>Hello {{yourName}}!</h1>! </body>! </html>!
  20. Code Structure app/assets/javascripts/ "## angularjs $ "## angular-animate.js $ "##

    angular-resource.js $ "## angular-route.js $ "## angular.js $ "## ui-bootstrap-0.7.0.js $ &## ui-bootstrap-tpls-0.7.0.js "## angularjs.js "## application.js "## config.js.coffee "## recipe_controller.js.coffee &## recipes_controller.js.coffee gem 'angularjs-rails-resource', '~> 0.2.5' app/views/templates app.html.haml index.html.haml pagination.html.haml recipe.html.haml Oops, not the best example
  21. How to assemble the App ! • Application Initialization •

    Server calls • Routing • Model • Creation (POJO or not POJO) • Defining a Controller • Views • Templates (DOM based) • Dom manipulation • UI Events
  22. Configure App app = angular.module("recipe-project", ['ngRoute', 'rails', 'ui.bootstrap'])! ! app.config

    ['$routeProvider', "$httpProvider", ($routeProvider, $httpProvider) ->! $routeProvider.when("/",! controller: RecipesController! templateUrl: "/templates/index.html"! ).when("/recipe/:id",! controller: RecipeController! templateUrl: "/templates/recipe.html"! ).otherwise redirectTo: "/"! ! # protect_from_forgery! authToken = angular.element(document.querySelector('meta[name="csrf-token"]')).attr("content")! $httpProvider.defaults.headers.common["X-CSRF-TOKEN"] = authToken! ]!
  23. @RecipesController @RecipesController = ($scope, $rootScope, Recipe, $location, $route, $routeParams) ->!

    ! $scope.load = (page)->! Recipe.query().then (pagination) ->! $scope.pagination = pagination! $scope.pagination.hasPrevious = pagination.currentPage>1! $scope.pagination.hasNext = pagination.currentPage<pagination.totalPages! $scope.recipes = pagination.entries! ! $scope.showRecipe = (recipe)->! $location.path "/recipe/#{recipe.id}"! index.json.jbuilder json.current_page @recipes.current_page json.total_pages @recipes.total_pages json.per_page @recipes.limit_value json.entries do json.array!(@recipes) do |recipe| json.extract! recipe, :user_id, :id, :title, :description, :image_url json.url recipe_url(recipe, format: :json) end end
  24. app/views/templates/ index.html.haml %table.table.table-striped.table-bordered! %tr! %th! %th Title! %th! %tr{ 'ng-repeat'

    => "recipe in recipes" }! %td! %img{ 'ng-src' => "{{recipe.imageUrl}}", class: 'img-thumbnail'}! %td {{ recipe.title }}! %td! %a{ 'ng-click' => "showRecipe(recipe)"} Show!
  25. Search $rootScope.search = (criteria)->! $rootScope.searchCriteria = if criteria? and criteria

    != "" then criteria else null! $rootScope.$broadcast('searchEvent', criteria);! $location.path "/"! @RecipesController $scope.$on 'searchEvent', (event,criteria)->! $scope.load()! $scope.load = (page)->! options = null! if page?! options ?= {}! options['page'] = page! if $rootScope.searchCriteria? and $rootScope.searchCriteria != ""! options ?= {}! options['search'] = $rootScope.searchCriteria! call = Recipe.query(options)!
  26. @RecipeController $scope.loadRecipe = (id)->! Recipe.get(id).then (recipe)->! $scope.selectedRecipe = recipe! $scope.loadRecipe($routeParams.id)!

    # Initialization! $scope.save = ->! $scope.editing = false! call = $scope.selectedRecipe.save({userId: $scope.selectedRecipe.userId})! call.then (recipe)->! $scope.editing = false! $scope.loadRecipe($scope.selectedRecipe.id)!
  27. How to get data in and out of the App

    • CRUD resource • nested resource • nested attributes • bulk operations
  28. Nested Attributes class Recipe < ActiveRecord::Base! belongs_to :user! has_many :ingredients,

    -> { order "id" }! has_many :steps, -> { order "position" }! ! accepts_nested_attributes_for :ingredients, :steps, allow_destroy: true! acts_as_taggable! ! validates_presence_of :title! end! class RecipesController < InheritedResources::Base! respond_to :html, :json! belongs_to :user, :optional => true! ! protected! ! def permitted_params! params.permit(recipe: [:title, :description, :image_url,! :ingredients_attributes => ["id", "amount", "unit", "description", "recipe_id", "_destroy"],! :steps_attributes => ["id", "position", "description", "recipe_id", "_destroy"]])! end! ! end!
  29. Nested Attributes @RecipeController = ($scope, $rootScope, Recipe, $location, $routeParams) ->!

    $scope.addIngredient = ->! $scope.selectedRecipe.ingredients.push {! childIndex: new Date().getTime(), ! recipeId: $scope.selectedRecipe.id ! }! ! $scope.deleteIngredient = (ingredient)->! ingredient._destroy = !ingredient._destroy! app.factory "Recipe", ["railsResourceFactory", "railsSerializer",! (railsResourceFactory, railsSerializer) ->! railsResourceFactory! url: "/recipes"! name: "recipe"! pluralName: "recipes"! serializer: railsSerializer(->! @nestedAttribute "ingredients", "steps"! )! ]!
  30. Form validation (client-side) %form#recipeForm{ name: "recipeForm", "novalidate" => "true" }!

    .field{ 'ng-show' => 'editing', "ng-class" => "{'has-error': recipeForm.title.$invalid}"}! %label Title! %input#title.form-control{ "name" => "title", ! "ng-model" => "selectedRecipe.title", type: "text", ! "required" => true, "ng-minlength" => 5}! %span.help-block{"ng-show" => "recipeForm.title.$dirty && recipeForm.title. $error.required"} ! Required! %span.help-block{"ng-show" => "recipeForm.title.$dirty && recipeForm.title. $error.minlength”}! At least 5 characters! %a.btn.btn-primary{ 'ng-click' => "save()", "ng-disabled" => "!recipeForm.$valid" } Save!
  31. Form validation Nested Forms (client-side) %table! %tr{ 'ng-repeat' => "ingredient

    in selectedRecipe.ingredients", 'ng-class' => "{strike: ingredient._destroy}" }! %td! %ng-form{ name: "ingredientForm" }! %input{ 'name' => 'description', ! 'ng-show' => 'editing', 'type' => "text", ! 'ng-model' => "ingredient.description", 'placeholder' => "Description", ! 'required' => true, "ng-minlength" => 3}! %span.help-block{"ng-show" => "ingredientForm.description.$dirty && ingredientForm.description.$error.required"} Required! %span.help-block{"ng-show" => "ingredientForm.description.$dirty && ingredientForm.description.$error.minlength"} At least 3 characters!
  32. Form validation (server-side) Not provided by AngularJSRailsResource class Recipe <

    ActiveRecord::Base! validates_presence_of :title! end! @RecipeController = ($scope, $rootScope, Recipe, $location, $routeParams) ->! $scope.save = ->! $scope.errors = {}! ! call = $scope.selectedRecipe.save({userId: $scope.selectedRecipe.userId})! ! call.catch (result)->! angular.forEach result.data.errors, (errors, field) ->! $scope.recipeForm[field].$setValidity('server', false)! $scope.errors[field] = errors.join(', ') app.directive 'serverError', ->! restrict: 'A'! require: 'ngModel'! link: (scope, element, attrs, ctrl) ->! scope.$watch "selectedRecipe.title", ->! ctrl.$setValidity('server', true)! config.js.coffee From http://codetunes.com/2013/server-form-validation-with-angular/
  33. Form validation (server-side) .field{ 'ng-show' => 'editing', "ng-class" => "{'has-error':

    recipeForm.title.$invalid}"}! %label Title! %input#title.form-control{ "name" => "title", ! "ng-model" => "selectedRecipe.title", type: "text", ! ! ! ! ! ! "required" => false, "ng-minlength" => 5, ! "server-error" => true }! %span.help-block{"ng-show" => "recipeForm.title.$dirty && recipeForm.title.$error.minlength”}! ! At least 5 characters! %span.help-block{"ng-show" => "recipeForm.title.$error.server"}! {{ errors.title }}!
  34. Custom Action resources :recipes, only: [:index, :show] do! get 'tags',

    on: :collection! end! routes.rb class RecipesController < InheritedResources::Base! respond_to :html, :json! belongs_to :user, :optional => true! ! def tags! respond_with(Recipe.tag_counts)! end! end recipes_controller.rb
  35. Custom Action app.factory "Recipe", ["railsResourceFactory", "railsSerializer", (railsResourceFactory, railsSerializer) ->! Recipe

    = railsResourceFactory! url: "/recipes"! name: "recipe"! pluralName: "recipes"! serializer: railsSerializer(->! @nestedAttribute "ingredients", "steps"! )! Recipe.getTags = ->! @$get @$url() + "/tags.json"! Recipe! ]! config.js.coffee $scope.loadCloud = ->! Recipe.getTags().then (tags) ->! $scope.tags = tags! recipes_controller.js.coffee
  36. Adding Tag Editor class RecipesController < InheritedResources::Base! ! protected! !

    def permitted_params! params.permit(recipe: [:title, :description, :image_url,! :tag_list => [],! :ingredients_attributes => ["id", "amount", "unit", "description", "recipe_id", "_destroy"],! :steps_attributes => ["id", "position", "description", "recipe_id", "_destroy"]])! end! class Recipe < ActiveRecord::Base! acts_as_taggable! end! recipe.rb recipes_controller.rb json.extract! @recipe, :id, :user_id, :title, :description, :tag_list, :created_at, :updated_at! show.json.jbuilder
  37. Adding Tag Editor http://ngmodules.org/modules/ngTagsInput app = angular.module("recipe-project", ['ngRoute', 'rails', 'ui.bootstrap',

    'ngTagsInput'])! config.js.coffee recipe.html.haml %p! %span{ 'ng-hide' => 'editing' } {{selectedRecipe.tagList.join(", ")}}! %tags-input{ "ng-model" => "selectedRecipe.tagList", 'ng-show' => 'editing' }!