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

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."

Yu-Cheng Chuang

May 13, 2014
Tweet

More Decks by Yu-Cheng Chuang

Other Decks in Programming

Transcript

  1. In Rails • For all requests other than GET /

    HEAD 1) Check params[:authenticity_token](changeable) 2) Check headers['X-CSRF-Token'] (unchangeable)
  2. 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
  3. 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
  4. 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 }
  5. Solution 1 controller: [ '$scope', '$http', '$q', function($scope, $http, $q)

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

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

    '$q' ($scope, $http, $q) -> ! $scope.test = 1; // OK ] No worry for missing comma
  8. Solution 3 Skip variable name compression # config/production.rb config.assets.js_compressor =

    Uglifier.new(mangle: false) Useful: having tons of code / FIX NOW
  9. 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.
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. var Post = $resource('/posts/:postId/:action.json', { postId: '@id' }); Add /:action

    segment Solution Hint: $resource squashes empty // segments for you.
  19. 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
  20. 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
  21. 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
  22. 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
  23. // 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.
  24. 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
  25. 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
  26. Not easy if you support oldIEs. • Fallback to iFrame-Transport

    instead. • Never, Never, Never try to polyfill XHR2 for oldIEs. • Never.
  27. 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
  28. $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
  29. 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); };
  30. 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