Crossing the Bridge: Connecting Rails and your Front-end Framework
-
Upload
daniel-spector -
Category
Software
-
view
336 -
download
2
Transcript of Crossing the Bridge: Connecting Rails and your Front-end Framework
Crossing the Bridge:Crossing the Bridge:Connecting Rails and your Front-endConnecting Rails and your Front-end
FrameworkFramework
@danielspecs@danielspecs
A GameplanA GameplanUnderstand the tradeoffs you'll makeDeeply integrate your framework with RailsShare data in a consistent and maintainable way
Daniel SpectorDaniel SpectorSoftware Engineer at Lifebooker
@danielspecs
spector.io
Flatiron School
Rails/Javascript/Swift/Clojure
Always thinkAlways thinkabout theabout the
bigger picturebigger picture
You will encounter a lot ofYou will encounter a lot oftradeoffs.tradeoffs.
Some of the fun thatSome of the fun thatawaits...awaits...
Duplicated modelsDuplicated modelsSeparate codebasesSeparate codebasesComplexityComplexity
But myBut myclients/customers/formerclients/customers/formercat's owner demands it!cat's owner demands it!
What do people want?What do people want?
Maintainable,Maintainable,sustainable,sustainable,performantperformantapplicationsapplications
RecapRecap
Never lose sight of the ultimate goalNever lose sight of the ultimate goal
Understand the tradeoffs that willUnderstand the tradeoffs that willcomecome
There may be a solutionThere may be a solution
What we're going to be building:What we're going to be building:
TodoMVC on RailsTodoMVC on Rails
Scaffolding out the same application in each ofScaffolding out the same application in each ofthe frameworks makes it easy to referencethe frameworks makes it easy to reference
Developed by GoogleDeveloped by Google
Two-way data bindingTwo-way data binding
Dependency InjectionDependency Injection
But... Angular 2But... Angular 2
# 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) endend
# config/routes.rb
namespace :api, :defaults => {:format => :json} do resources :todos, only: [:index, :create]end
# app/models/todo.rb
class Todo < ActiveRecord::Baseend
There's no official AngularThere's no official Angularintegration with Rails... integration with Rails...
So that's a perfect opportunity toSo that's a perfect opportunity to
try out Bower.try out Bower.
Created by Twitter
One centralized location for packages
Can be integrated with Rails via the bower-rails gem
$ 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"
How can we manageHow can we manageour client-side dataour client-side datato make it easy toto make it easy to
work with?work with?
ngResource is an optional libraryngResource is an optional libraryto map basic CRUD actions toto map basic CRUD actions to
specific method calls.specific method calls.
Let's scaffold out a basicLet's scaffold out a basicAngular and see how we canAngular and see how we can
integrate ngResourceintegrate ngResource
// 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.rbconfig.assets.paths << "#{Rails.root}/app/assets/templates"
Now we can set up our factory toNow we can set up our factory tohold our resource and pass it tohold our resource and pass it toour controller and our templateour controller and our template
// 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 = ''; }) }});
// 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>
RecapRecap
1. Data binding in Angular is powerfulData binding in Angular is powerful2. ngResource makes requests easyngResource makes requests easy3. Multiple API calls to initialize application canMultiple API calls to initialize application can
get trickyget tricky
Created by Tom Dale andCreated by Tom Dale andYehuda KatzYehuda KatzMade for large, ambitiousMade for large, ambitiousapplicationsapplicationsFavors convention overFavors convention overconfigurationconfigurationEmber Data is absolutelyEmber Data is absolutelywonderfulwonderful
Ember-CLIEmber-CLIThe new standard for developing Ember apps
Integrates with Rails via the ember-cli-rails gem
What we'll beWhat we'll beworking withworking with
class User < ActiveRecord::Base has_many :todosend
class Todo < ActiveRecord::Base belongs_to :userend
class EmberController < ApplicationController def preload @todos = current_user.todos endend
Rails.application.routes.draw do root 'ember#preload'end
$ rails g serializer todo create app/serializers/todo_serializer.rb
class TodoSerializer < ActiveModel::Serializer embed :ids, include: true attributes :id, :nameend
{ "todos": [ { "id": 1, "name": "Milk" }, { "id": 2, "name": "Coffee" }, { "id": 3, "name": "Cupcakes" } ]}
Create a new serializer, set it up to workCreate a new serializer, set it up to workwith Emberwith Ember
Now that we're all set up, whatNow that we're all set up, whatare we trying to accomplish?are we trying to accomplish?
Instead of using JSON calls, weInstead of using JSON calls, wewant to preload Emberwant to preload Ember
Why?Why?Minimize round trips to the serverMinimize round trips to the server
Bootstrapping the app means a quickerBootstrapping the app means a quicker
experience for our usersexperience for our users
# 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) endend
We'll pass this to Ember via theWe'll pass this to Ember via thewindow.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
$ 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_send
$ ember new frontend --skip-gitversion: 0.2.3installing 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...
$ ember g resource todosversion: 0.2.3installing create app/models/todo.jsinstalling create tests/unit/models/todo-test.jsinstalling create app/routes/todos.js create app/templates/todos.hbsinstalling create tests/unit/routes/todos-test.js
$ ember g adapter applicationversion: 0.2.3installing create app/adapters/application.jsinstalling create tests/unit/adapters/application-test.js
$ ember g serializer applicationversion: 0.2.3installing create app/serializers/application.jsinstalling create tests/unit/serializers/application-test.js
// 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({});
// 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};
Ember will initializeEmber will initializeEmber Data objectsEmber Data objectsfor us, inferring thefor us, inferring the
correct type from thecorrect type from theroot of the JSONroot of the JSON
responseresponse
Now we can use our route to find the dataand 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>
RecapRecap
1. Don't fight Ember. Use conventionsDon't fight Ember. Use conventionslike AMSlike AMS
2. Preloading is extremely powerfulPreloading is extremely powerful3. Avoiding spinners and loading screens
means a great experience
No initial API call, noNo initial API call, nopreloading, renderpreloading, renderstraight from thestraight from the
server.server.
http://bensmithett.com/server-rendered-react-components-in-rails/
# 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) endend
# app/views/todos/index.html.erb
<%= react_component('Todos', {:load => @load.to_json}, {:prerender => true}) %>
Really nice viewReally nice viewhelpershelpers
The magic lives in {:prerender => true}
React is builtReact is builtaround componentsaround components
Each component should have one isolatedresponsibility.
# 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> ); }});
TodosList ComponentTodosList 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> ) }});
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> ) }});
RecapRecap1. Each component should have only oneEach component should have only one
responsibilityresponsibility2. Prerender on the server for SEO, usability andPrerender on the server for SEO, usability and
other benefitsother benefits3. UJS will mount your component and take careUJS will mount your component and take care
of the handoffof the handoff
IsomorphicIsomorphicJavascript is theJavascript is the
future.future.React
Ember 2.0 with FastBootAngular 2?
Where we've come from andWhere we've come from andwhere we are goingwhere we are going
1. Constructing API's that serve JSON to the clientConstructing API's that serve JSON to the client
2. Preload your data on startup to avoid spinners andPreload your data on startup to avoid spinners andloading screensloading screens
3. Server-side rendering for SEO, startup time and a greatServer-side rendering for SEO, startup time and a greatuser experienceuser experience