Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

AngularJS Rails Ajax

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

CSRF Token

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

In Rails • For all requests other than GET / HEAD 1) Check params[:authenticity_token](changeable) 2) Check headers['X-CSRF-Token'] (unchangeable)

Slide 8

Slide 8 text

In Angular 1) Find cookies['XSRF-TOKEN'] 2) When $http requesting,
 add headers['X-XSRF-TOKEN'] = thatToken Problem

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

X-Requested-With=?

Slide 12

Slide 12 text

$http.post("/posts") Problem

Slide 13

Slide 13 text

jQuery.post("/posts")

Slide 14

Slide 14 text

$http.post("/posts") Problem

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

respond_to do |format| format.json end Problem

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Dependency Injection v.s. Minifier

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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 }

Slide 22

Slide 22 text

Solution 1 controller: [ '$scope', '$http', '$q', function($scope, $http, $q) { ! $scope.test = 1; // OK }] Explicitly declare dependencies

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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.

Slide 26

Slide 26 text

Solution 3 Skip variable name compression # config/production.rb config.assets.js_compressor = Uglifier.new(mangle: false) Useful: having tons of code / FIX NOW

Slide 27

Slide 27 text

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.

Slide 28

Slide 28 text

$resource I feel $resource much easier than Restangular.

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

/posts/:id.json Required for AngularJS! Otherwise Rails will guess it wrong.

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

/posts/:id .json
 /posts/maar.json

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

var Post = $resource('/posts/:postId/:action.json', { postId: '@id' }); Add /:action segment Solution Hint: $resource squashes empty // segments for you.

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Posting a Form with Uploads

Slide 48

Slide 48 text

Easy?

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Not easy if you support oldIEs. • Fallback to iFrame-Transport instead. • Never, Never, Never try to polyfill XHR2 for oldIEs. • Never.

Slide 52

Slide 52 text

$http.get with { nested: { params: 1 } }

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

$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

Slide 55

Slide 55 text

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); };

Slide 56

Slide 56 text

model = [ ☑ A ☐ B ☑ C ] Bonus

Slide 57

Slide 57 text

model binding to check boxes Admin ! Support ! QA https://github.com/vitalets/checklist-model Use checklist-model Solution

Slide 58

Slide 58 text

AngularJS ❤️ Rails Ajax EOP