Pro Yearly is on sale from $80 to $50! »

AngularJS ♥ Rails Ajax: Pitfalls and Solutions

AngularJS ♥ Rails Ajax: Pitfalls and Solutions

AngularJS ♥ Rails Ajax: Pitfalls and Solutions

AngularJS 是近來很熱門的 JavaScript Front-End MVC Framework ,雖然學習曲線很陡,但靈活度非常高,做為前端 Framework 相當稱職。

然而因為是純前端的 Framework ,自然不會考慮到後端 Server 。講者做為一個 Rails Developer ,在實作 AngularJS 與 Rails 的 Ajax 整合時,遇到不少問題,這次會分享一些實際上解過的問題。

---

AngularJS has been a popular JavaScript Front-End Framework recently. It's hard to learn, but very flexible.

However, as a Front-End Framework, it itself does not consider a lot of backend things. The author of this slides, as a Rails developer, hit some pitfalls when integrating AngularJS and Rails backend APIs. In this slides I'll show some solutions for those pitfalls.

Note: It is almost in English except page 3, which means "a lot of pitfalls."

36b1f565fc83d9b67588123f2171b896?s=128

Yu-Cheng Chuang

May 13, 2014
Tweet

Transcript

  1. AngularJS ❤️ Rails Ajax Yu-Cheng Chuangč⇯௾Ď@yorkxin ‧ github=chitsaou KKBOX Sharing

    ‧ Rails Tuesday Sharing #5
  2. AngularJS Rails Ajax

  3. None
  4. CSRF Token

  5. For Rails UJS (link_to remote: true) For regular Form Post

  6. None
  7. In Rails • For all requests other than GET /

    HEAD 1) Check params[:authenticity_token](changeable) 2) Check headers['X-CSRF-Token'] (unchangeable)
  8. In Angular 1) Find cookies['XSRF-TOKEN'] 2) When $http requesting,
 add

    headers['X-XSRF-TOKEN'] = thatToken Problem
  9. Solution # application_controller.rb ! after_action :drop_csrf_cookie_for_angular, :if => :protect_against_forgery? !

    private ! def drop_csrf_cookie_for_angular cookies['XSRF-TOKEN'] = form_authenticity_token end drop Cookie for XSRF Token
  10. Solution # application_controller.rb ! after_action :drop_csrf_cookie_for_angular, :if => :protect_against_forgery? !

    private ! def drop_csrf_cookie_for_angular cookies['XSRF-TOKEN'] = form_authenticity_token end drop Cookie for XSRF Token def verified_request? super || form_authenticity_token == request.headers['X-XSRF-TOKEN'] end check for Angular's XSRF Token header
  11. X-Requested-With=?

  12. $http.post("/posts") Problem

  13. jQuery.post("/posts")

  14. $http.post("/posts") Problem

  15. $http.post("/posts") Feature, not bug: https://github.com/angular/angular.js/issues/1004 missing X-Requested-With=XMLHttpRequest Problem

  16. respond_to do |format| format.json end Problem

  17. Solution 1 Always add .json suffix $http.post("/posts.json")

  18. Solution app.config([ '$httpProvider', function($httpProvider) { $httpProvider.defaults.headers .common["X-Requested-With"] = "XMLHttpRequest"; }

    ]); Add to default headers
  19. Dependency Injection v.s. Minifier

  20. Dependency Injection controller: function($scope, $http, $q) { $scope.test = 1;

    } Problem
  21. Dependency Injection controller: function($scope, $http, $q) { $scope.test = 1;

    } Problem JS Minifier ↓ ↓ controller: function(a, b, c) { $scope.test = 1; // ReferenceError: $scope is not defined }
  22. Solution 1 controller: [ '$scope', '$http', '$q', function($scope, $http, $q)

    { ! $scope.test = 1; // OK }] Explicitly declare dependencies
  23. Solution 1+ More Beautiful in CoffeeScript controller: [ '$scope', '$http',

    '$q' ($scope, $http, $q) -> ! $scope.test = 1; // OK ]
  24. Solution 1+ More Beautiful in CoffeeScript controller: [ '$scope', '$http',

    '$q' ($scope, $http, $q) -> ! $scope.test = 1; // OK ] No worry for missing comma
  25. Solution 2 https://github.com/btford/ngmin https://github.com/jasonm/ngmin-rails (Rails Integration) Let machine (Assets Pipeline)

    do this translation for you Also provides integrations for Grunt / Gulp / etc.
  26. Solution 3 Skip variable name compression # config/production.rb config.assets.js_compressor =

    Uglifier.new(mangle: false) Useful: having tons of code / FIX NOW
  27. Notes for UI-Router $stateProvider.state({ name: "show", url: "/posts/:postId", templateUrl: "show.html",

    controller: "PostShowCtrl", resolve: { order: function($stateParams, Post) { Post.get(id: $stateParams.postId).$promise; } } }); Failure without Error! hint: capture $stateChangeError event.
  28. $resource I feel $resource much easier than Restangular.

  29. resources :posts Action HTTP Request Notes index GET /posts List

    All Posts show GET /posts/:id Get a Single Post new GET /posts/new New Post Form (HTML-only) create POST /posts Create a New Post edit GET /posts/:id/edit Edit Post Form (HTML-only) update PATCH /posts/:id
 PUT /posts/:id Update a Single Post destroy DELETE /posts/:id Remove a Single Post CRUD cheat sheet for non-Rails developers
  30. resources :posts Action HTTP Request Notes index GET /posts List

    All Posts show GET /posts/:id Get a Single Post new GET /posts/new New Post Form (HTML-only) create POST /posts Create a New Post edit GET /posts/:id/edit Edit Post Form (HTML-only) update PATCH /posts/:id
 PUT /posts/:id Update a Single Post destroy DELETE /posts/:id Remove a Single Post CRUD cheat sheet for non-Rails developers We don't need new and edit for JSON APIs
  31. resources :posts CRUD path structure for non-Rails developers /posts/:id/:action(.:format) for

    new and edit forms not used by APIs for single resource absence for collection actions Optional Rails will guess format if not given
  32. resources :posts CRUD path structure for non-Rails developers /posts/:id/:action(.:format) for

    new and edit forms not used by APIs for single resource absence for collection actions Optional Rails will guess format if not given
  33. /posts/:id.json Required for AngularJS! Otherwise Rails will guess it wrong.

  34. resources :posts var Post = $resource('/posts/:postId.json', { postId: "@id" });

    ! OK except for Update
  35. resources :posts var Post = $resource('/posts/:postId.json', { postId: "@id" },

    { update: { method: "PUT" } }); Using PUT because we're actually repeating all the params. Working Example Solution
  36. Action $resource Request index Post.query() GET /posts.json show Post.get({ postId:

    123 }) GET /posts/123.json create (new Post(params)).$save() POST /posts.json with body params update post.name = "Something";
 post.$update() PUT /posts.json with body params destroy post.$delete() DELETE /posts/123.json
  37. Action $resource Request index Post.query() GET /posts.json show Post.get({ postId:

    123 }) GET /posts/123.json create (new Post(params)).$save() POST /posts.json with body params update post.name = "Something";
 post.$update() PUT /posts.json with body params destroy post.$delete() DELETE /posts/123.json $ for instances
  38. Custom Actions resources :posts do patch :like, :on => :member

    patch :maar, :on => :collection end Action HTTP Request Notes like PATCH /posts/1/like.json Like a Single Post maar PATCH /posts/maar.json Mark All Posts as Read
  39. /posts/:id .json
 /posts/maar.json

  40. Parameter Overloading! /posts/:id .json
 /posts/maar.json

  41. var Post = $resource('/posts/:postId/:action.json', { postId: '@id' }); Add /:action

    segment Solution Hint: $resource squashes empty // segments for you.
  42. resources :posts do patch :like, :on => :member patch :maar,

    :on => :collection end var Post = $resource('/posts/:postId/:action.json', { postId: '@id' }, { like: { method: "PATCH", params: { action: "like" } } }); Define new method and assign :action param Solution
  43. var Post = $resource('/posts/:postId/:action.json', { postId: '@id' }, { maar:

    { method: "PATCH", params: { action: "maar", postId: null }, isArray: false } }); resources :posts do patch :like, :on => :member patch :maar, :on => :collection end Solution Define new method and assign :action param
  44. var Post = $resource('/posts/:postId/:action.json', { postId: '@id' }, { maar:

    { method: "PATCH", params: { action: "maar", postId: null }, isArray: false } }); resources :posts do patch :like, :on => :member patch :maar, :on => :collection end Solution Define new method and assign :action param Remove :postId param
  45. var Post = $resource('/posts/:postId/:action.json', { postId: '@id' }, { maar:

    { method: "PATCH", params: { action: "maar", postId: null }, isArray: false } }); resources :posts do patch :like, :on => :member patch :maar, :on => :collection end Solution Define new method and assign :action param Remove :postId param Set isArray: true if it returns array of objects
  46. // Like a single article (instance) var post = Post.get({

    postId: 1 }); post.$like(); // PATCH /posts/1/like.json ! // Like a single Post without instance Post.like({ postId: 1 }, {}); // PATCH /posts/1/like.json // Mark all posts as read Post.maar(); // PATCH /posts/maar.json Force $resource to use the first param for URL structuring.
  47. Posting a Form with Uploads

  48. Easy?

  49. Easy! With FormData For more: Using FormData Objects on MDN


    https://developer.mozilla.org/en-US/docs/Web/Guide/Using_FormData_Objects var form = document.getElementById('the-form'); var formData = new FormData(form); ! $http.post("/posts", formData, { headers: { 'Content-Type': undefined }, transformRequest: function(data) { return data; } }); code sample via https://groups.google.com/forum/#!topic/angular/MBf8qvBpuVE Solution need some hacks
  50. Not easy if you support IEs. • IE10+ supports XHR2

    FormData, with a bug: • When the last input is checkbox or radio and is not checked, • Then the post data is corrupted and Rails cannot parse it. • Workaround: add a dummy hidden input at the bottom of form. http://blog.yorkxin.org/posts/2014/02/06/ajax-with-formdata-is-broken-on-ie10-ie11
  51. Not easy if you support oldIEs. • Fallback to iFrame-Transport

    instead. • Never, Never, Never try to polyfill XHR2 for oldIEs. • Never.
  52. $http.get with { nested: { params: 1 } }

  53. jQuery.get("/legislators/search", { search: "Wego", filter: { age: { gte: 50

    }, party: { any: ['kmt', 'dpp'] }, is_appendectomy_ready: { eq: true } }, sort: [ { age: "desc" }, { city_id: "asc" } ], page: 1, perPage: 20 }); jQuery: everything is OK, Rails is happy to parse
  54. $http.get("/legislators/search", { params: { search: "Wego", filter: { age: {

    gte: 50 }, party: { any: ['kmt', 'dpp'] }, is_appendectomy_ready: { eq: true } }, sort: [ { age: "desc" }, { city_id: "asc" } ], page: 1, perPage: 20 } }); $http: Converted to JSON after layer 2. Hard to parse. Problem
  55. Solution var sendSearchRequest = function(url, params) { var dfd =

    $q.defer(); ! jQuery.get(url, params) .done(dfd.resolve) .fail(dfd.reject); ! return dfd.promise; }; Use jQuery for Ajax instead. (Wrap with $q) $scope.submit = function() { sendSearchRequest("/legislators/search", $scope.params) .then(okCallback, failCallback); };
  56. model = [ ☑ A ☐ B ☑ C ]

    Bonus
  57. model binding to check boxes <input type="checkbox" checklist-model="user.roles" checklist-value="admin"> Admin

    ! <input type="checkbox" checklist-model="user.roles" checklist-value="support"> Support ! <input type="checkbox" checklist-model="user.roles" checklist-value="qa"> QA https://github.com/vitalets/checklist-model Use checklist-model Solution
  58. AngularJS ❤️ Rails Ajax EOP