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

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

Embed Size (px)

Citation preview

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

FrameworkFramework

@danielspecs@danielspecs

This is what we're trying to avoidThis is what we're trying to avoid

Instead we want thisInstead we want this

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

What are youWhat are yougetting yourselfgetting yourself

into?into?

Javascript. Wat.Javascript. Wat.> [] + {}

=> [object Object]

> {} + []

=> 0

Courtesy of Alex Matchneer

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!

Er, no.Er, no.

What do people want?What do people want?

Maintainable,Maintainable,sustainable,sustainable,performantperformantapplicationsapplications

But now that you'veBut now that you'vebeen warned...been warned...

Holy s**t canHoly s**t canyou make someyou make some

awesomeawesomeapplications.applications.

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

Now, let's dive inNow, let's dive in

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

The Frameworks:The Frameworks:

Now let's have a lookNow let's have a lookat Angularat Angular

Developed by GoogleDeveloped by Google

Two-way data bindingTwo-way data binding

Dependency InjectionDependency Injection

But... Angular 2But... Angular 2

First, let's get ourFirst, let's get ourRails project API readyRails project API ready

# 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

Ember! Yeah!Ember! Yeah!

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

# Gemfile

gem "active_model_serializers"gem "ember-cli-rails"

$ 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

Let's get setup withLet's get setup withour client-side codeour client-side code

$ 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

So let's talkSo let's talkabout React.about React.

Developed by Facebook

One-way data binding

Virtual DOM

Isomorphic Javascript

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

Use the react-rails gemUse the react-rails gem

# Gemfile

gem "react-rails"

$ rails g react:install

# 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

Thanks!Thanks!

Would love to answer any questions

Please feel free to tweet at me or get intouch in any other way.

@danielspecs

spector.io