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

Crossing the Bridge: Connecting Rails and your ...

Crossing the Bridge: Connecting Rails and your Front-end Framework

Presented at Railsconf 2015 by Daniel Spector, @danielspecs.

Crossing the Bridge explores tools, patterns and best practices to connect your Javascript MVC framework to Rails in the most seamless way possible. The talk progresses from demonstrating the standard API request cycle to preloading data to your client-side framework to rendering your javascript on the server. It explores Isomorphic Javascript and ways of implementing it with Rails.

Daniel Spector

April 22, 2015
Tweet

Other Decks in Programming

Transcript

  1. Crossing the Bridge: Crossing the Bridge: Connecting Rails and your

    Front-end Connecting Rails and your Front-end Framework Framework @danielspecs @danielspecs
  2. A Gameplan A Gameplan Understand the tradeoffs you'll make Deeply

    integrate your framework with Rails Share data in a consistent and maintainable way
  3. Always think Always think about the about the bigger picture

    bigger picture You will encounter a lot of You will encounter a lot of tradeoffs. tradeoffs.
  4. Some of the fun that Some of the fun that

    awaits... awaits... Duplicated models Duplicated models Separate codebases Separate codebases Complexity Complexity
  5. What do people want? What do people want? Maintainable, Maintainable,

    sustainable, sustainable, performant performant applications applications
  6. Holy s**t can Holy s**t can you make some you

    make some awesome awesome applications. applications.
  7. Recap Recap Never lose sight of the ultimate goal Never

    lose sight of the ultimate goal Understand the tradeoffs that will Understand the tradeoffs that will come come There may be a solution There may be a solution
  8. What we're going to be building: What we're going to

    be building: TodoMVC on Rails TodoMVC on Rails Scaffolding out the same application in each of Scaffolding out the same application in each of the frameworks makes it easy to reference the frameworks makes it easy to reference
  9. Developed by Google Developed by Google Two-way data binding Two-way

    data binding Dependency Injection Dependency Injection But... Angular 2 But... Angular 2
  10. # app/controllers/api/todos_controller.rb class Api::TodosController < ApplicationController respond_to :json def index

    @todos = Todo.all render json: @todos end def create @todo = Todo.create(todo_params) render json: @todo end private def todo_params params.require(:todo).permit(:item) end end # config/routes.rb namespace :api, :defaults => {:format => :json} do resources :todos, only: [:index, :create] end # app/models/todo.rb class Todo < ActiveRecord::Base end
  11. There's no official Angular There's no official Angular integration with

    Rails... integration with Rails... So that's a perfect opportunity to So that's a perfect opportunity to try out Bower. try out Bower.
  12. Created by Twitter One centralized location for packages Can be

    integrated with Rails via the bower-rails gem
  13. $ npm install -g bower # Gemfile gem "bower-rails", "~>

    0.9.2" $ rails g bower_rails:initialize # Bowerfile # Puts to ./vendor/assets/bower_components asset "angular" asset "angular-resource" asset "angular-route"
  14. How can we manage How can we manage our client-side

    data our client-side data to make it easy to to make it easy to work with? work with?
  15. ngResource is an optional library ngResource is an optional library

    to map basic CRUD actions to to map basic CRUD actions to specific method calls. specific method calls. Let's scaffold out a basic Let's scaffold out a basic Angular and see how we can Angular and see how we can integrate ngResource integrate ngResource
  16. // app/assets/main.js // This is the main entry point for

    our application. var Todo = angular .module('todo', ['ngResource', 'ngRoute']) .config(['$routeProvider', '$httpProvider', function($routeProvider, $httpProvider) { // We need to add this for Rails CSRF token protection $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content'); // Right now we have one route but we could have as many as we want $routeProvider .when("/", {templateUrl: "../assets/index.html", controller: "TodoCtrl"}) }]); # config/application.rb config.assets.paths << "#{Rails.root}/app/assets/templates"
  17. Now we can set up our factory to Now we

    can set up our factory to hold our resource and pass it to hold our resource and pass it to our controller and our template our controller and our template
  18. // app/assets/factories/todoFactory.js Todo.factory("Todo", function($resource){ return $resource("/api/todos/:id", { id: "@id" });

    }); // app/assets/controllers/todoCtrl.js Todo.controller("TodoCtrl", function($scope, Todo){ $scope.todos = Todo.query(); $scope.addTodo = function() { $scope.data = new Todo(); $scope.data.name = $scope.newTodo.trim(); Todo.save($scope.data, function(){ $scope.todos = Todo.query(); $scope.newTodo = ''; }) } });
  19. // app/assets/templates/index.html <p>Hello RailsConf!</p> <ul> <li ng-repeat="t in todos"> {{t.name}}

    </li> </ul> <form ng-submit="addTodo()"> <input placeholder="I need to..." ng-model="newTodo" autofocus> </form>
  20. Recap Recap 1. Data binding in Angular is powerful Data

    binding in Angular is powerful 2. ngResource makes requests easy ngResource makes requests easy 3. Multiple API calls to initialize application can Multiple API calls to initialize application can get tricky get tricky
  21. Created by Tom Dale and Created by Tom Dale and

    Yehuda Katz Yehuda Katz Made for large, ambitious Made for large, ambitious applications applications Favors convention over Favors convention over configuration configuration Ember Data is absolutely Ember Data is absolutely wonderful wonderful
  22. What we'll be What we'll be working with working with

    class User < ActiveRecord::Base has_many :todos end class Todo < ActiveRecord::Base belongs_to :user end class EmberController < ApplicationController def preload @todos = current_user.todos end end Rails.application.routes.draw do root 'ember#preload' end
  23. $ rails g serializer todo create app/serializers/todo_serializer.rb class TodoSerializer <

    ActiveModel::Serializer embed :ids, include: true attributes :id, :name end { "todos": [ { "id": 1, "name": "Milk" }, { "id": 2, "name": "Coffee" }, { "id": 3, "name": "Cupcakes" } ] } Create a new serializer, set it up to work Create a new serializer, set it up to work with Ember with Ember
  24. Now that we're all set up, what Now that we're

    all set up, what are we trying to accomplish? are we trying to accomplish? Instead of using JSON calls, we Instead of using JSON calls, we want to preload Ember want to preload Ember
  25. Why? Why? Minimize round trips to the server Minimize round

    trips to the server Bootstrapping the app means a quicker Bootstrapping the app means a quicker experience for our users experience for our users
  26. # app/controllers/ember_controller.rb class EmberController < ApplicationController def preload @todos =

    current_user.todos preload! @todos, serializer: TodoSerializer end def preload!(data, opts = {}) @preload ||= [] data = prepare_data(data, opts) @preload << data unless data.nil? end def prepare_data(data, opts = {}) data = data.to_a if data.respond_to? :to_ary data = [data] unless data.is_a? Array return if data.empty? options[:root] ||= data.first.class.to_s.underscore.pluralize options[:each_serializer] = options[:serializer] if options[:serializer] ActiveModel::ArraySerializer.new(data, options) end end
  27. We'll pass this to Ember via the We'll pass this

    to Ember via the window. window. # app/views/layouts/application.html.haml = stylesheet_link_tag :frontend :javascript window.preloadEmberData = #{(@preload || []).to_json}; = include_ember_script_tags :frontend %body = yield github.com/hummingbird-me/hummingbird
  28. $ rails g ember-cli:init create config/initializers/ember.rb # config/initializer/ember.rb EmberCLI.configure do

    |config| config.app :frontend, path: Rails.root.join('frontend').to_s end $ ember new frontend --skip-git version: 0.2.3 installing create .bowerrc create .editorconfig create .ember-cli create .jshintrc create .travis.yml create Brocfile.js create README.md create app/app.js create app/components/.gitkeep ...
  29. $ ember g resource todos version: 0.2.3 installing create app/models/todo.js

    installing create tests/unit/models/todo-test.js installing create app/routes/todos.js create app/templates/todos.hbs installing create tests/unit/routes/todos-test.js $ ember g adapter application version: 0.2.3 installing create app/adapters/application.js installing create tests/unit/adapters/application-test.js $ ember g serializer application version: 0.2.3 installing create app/serializers/application.js installing create tests/unit/serializers/application-test.js
  30. // frontend/app/models/todo.js import DS from 'ember-data'; var Todo = DS.Model.extend({

    name: DS.attr('string') }); export default Todo; // frontend/app/adapters/application.js import DS from 'ember-data'; export default DS.ActiveModelAdapter.extend({ });
  31. // frontend/app/initializers/preload.js export function initialize(container) { if (window.preloadEmberData) { var

    store = container.lookup('store:main'); window.preloadEmberData.forEach(function(item) { store.pushPayload(item); }); } } export default { name: 'preload', after: 'store', initialize: initialize };
  32. Ember will initialize Ember will initialize Ember Data objects Ember

    Data objects for us, inferring the for us, inferring the correct type from the correct type from the root of the JSON root of the JSON response response
  33. Now we can use our route to find the data

    and render it via a template // frontend/app/router.js export default Router.map(function() { this.resource('todos', { path: '/' }, function() {}); }); // frontend/app/routes/todos/index.js export default Ember.Route.extend({ model: function() { return this.store.all('todo') } }); // frontend/app/templates/todos/index.hbs <h2>Todo:</h2> <ul> {{#each todo in model}} <li>{{todo.name}}</li> {{/each}} </ul>
  34. Recap Recap 1. Don't fight Ember. Use conventions Don't fight

    Ember. Use conventions like AMS like AMS 2. Preloading is extremely powerful Preloading is extremely powerful 3. Avoiding spinners and loading screens means a great experience
  35. No initial API call, no No initial API call, no

    preloading, render preloading, render straight from the straight from the server. server. http://bensmithett.com/server-rendered-react-components-in-rails/
  36. # app/controllers/todos_controller.rb class TodosController < ApplicationController def index @load =

    { :todos => current_user.todos, :form => { :action => todos_path, :csrf_param => request_forgery_protection_token, :csrf_token => form_authenticity_token } } end def create @todo = Todo.create(todo_params) render json: @todo end def todo_params params.require(:todo).permit(:name) end end
  37. Use the react-rails gem Use the react-rails gem # Gemfile

    gem "react-rails" $ rails g react:install
  38. # app/views/todos/index.html.erb <%= react_component('Todos', {:load => @load.to_json}, {:prerender => true})

    %> Really nice view Really nice view helpers helpers The magic lives in {:prerender => true}
  39. React is built React is built around components around components

    Each component should have one isolated responsibility.
  40. # app/assets/javascripts/components/_todos.js.jsx var Todos = React.createClass({ getInitialState: function () {

    return JSON.parse(this.props.load); }, newTodo: function ( formData, action ) { $.ajax({ data: formData, url: action, type: "POST", dataType: "json", success: function (data) { this.setState({todos: this.state.todos.concat([data]}); }.bind(this) }); }, render: function () { return ( <div> <ul> <TodosList todos={this.state.todos} /> </ul> <TodoForm form={this.state.form} onNewTodo={this.newTodo} /> </div> ); } });
  41. TodosList Component TodosList Component // app/assets/javascripts/components/_todos_list.js.jsx var TodosList = React.createClass({

    render: function () { var allTodos = this.props.todos.map(function (todo) { return <Todo name={todo.name} /> }); return ( <div> { allTodos } </div> ) } }); // app/assets/javascripts/components/_todo.js.jsx var Todo = React.createClass({ render: function (){ return ( <div> <li>{this.props.name}</li> </div> ) } });
  42. And now the form... And now the form... // app/assets/javascripts/components/_todo_form.js.jsx

    var TodoForm = React.createClass({ handleSubmit: function (e) { e.preventDefault(); var formData = $(this.refs.form.getDOMNode()).serialize(); this.props.onNewTodo(formData, this.props.form.action); this.refs.name.getDOMNode().value = ""; }, render: function () { return ( <form ref="form"action={this.props.form.action} method="post" onSubmit={this.handleSubmit}> <input type="hidden" name={this.props.form.csrf_param } value={this.props.form.csrf_token}/> <input ref="name" name="todo[name]" placeholder="I need to do..." /> <button type="submit">New Todo</button> </form> ) } });
  43. Recap Recap 1. Each component should have only one Each

    component should have only one responsibility responsibility 2. Prerender on the server for SEO, usability and Prerender on the server for SEO, usability and other benefits other benefits 3. UJS will mount your component and take care UJS will mount your component and take care of the handoff of the handoff
  44. Where we've come from and Where we've come from and

    where we are going where we are going 1. Constructing API's that serve JSON to the client Constructing API's that serve JSON to the client 2. Preload your data on startup to avoid spinners and Preload your data on startup to avoid spinners and loading screens loading screens 3. Server-side rendering for SEO, startup time and a great Server-side rendering for SEO, startup time and a great user experience user experience
  45. Thanks! Thanks! Would love to answer any questions Please feel

    free to tweet at me or get in touch in any other way. @danielspecs spector.io