Globalcode  – Open4education Interfaces  ricas  com  Rails  e  React.JS Rodrigo  Urubatan @urubatan
Globalcode  – Open4education Quem? Programador desde 1997 Trabalha com Ruby na Brightwire Escreveu "Ruby On Rails: Desenvolvimento fácil e Rápido de aplicações web” Já trabalhou com diversas linguagens (C, C++, Delphi, PHP, ASP, ColdFusion, VisualBasic, C#, Python, Ruby, Assembly, …) Apaixonado por Agile e atualmente por trabalho remoto Patinador, Corredor, Ciclista e agora resolveu aprender Karate :D Pai de um Guri de 6 anos http://urubatan.com.br - Anda meio abandonado mas vai voltar http://sobrecodigo.com - idem
Globalcode  – Open4education Objetivo (O blog mais feio do mundo!)
Globalcode  – Open4education Objetivo Usar o Rails como backend da aplicação Toda a interação com o usuário será implementada no cliente Validações e regras de negócio serão implementadas no servidor (sim, eu poderia usar Sinatra mas sou preguiçoso)
Globalcode  – Open4education Cuidado! Nesta palestra vamos falar de uma SPA (Single Page Application) Isto tem vantagens e desvantagens Melhora muito a interação com o usuário sem duplicação de código, já que o código de renderização fica todo no JS Piora muito a indexação da sua aplicação por buscadores (adeus SEO - ou não…)
Globalcode  – Open4education Criando a aplicação Uma aplicação padrão Rails (rails new …) Gemfile updates gem 'backbone-on-rails' gem 'react-rails', github: 'reactjs/react-rails', ref: 'master' Environment update (development.rb para começar) config.react.variant = :development config.react.addons = true # defaults to false config.react.server_renderer = React::ServerRendering::SprocketsRenderer bundle install rails g react:install
Globalcode  – Open4education Componentes Componentes javascript vão ficar em app/assets/javascript/components Backbone.js vai facilitar a comunicação cliente/servidor arquivos .js.jsx tem uma facilidade extra, são compilados pelo react-rails via asset pipeline e permitem adicionar HTML inline
Globalcode  – Open4education Cadastrando um Usuário rails g scaffold post title:string slug:text content:text Apagar todas as views do post exceto index.html.erb Apagar todo o código de index.html.erb e mudar para: <%= react_component('Blog', {collection: @posts, item: @post}) %>
Globalcode  – Open4education Alterações no controller Fazer todos os métodos retornarem json, mais ou menos assim (não é a melhor tecnica, mas boa o suficiente para o exemplo) class PostsController < ApplicationController before_action :set_post, only: [:show, :edit, :update, :destroy] def index @posts = Post.all end def show @posts = Post.all render :index end # GET /posts/new def new @post = Post.new render :index end # POST /posts # POST /posts.json def create @post = Post.new(post_params) respond_to do |format| if @post.save format.json { render json: @post, status: :created, location: @post } else format.json { render json: @post.errors, status: :unprocessable_entity } end end end #  PATCH/PUT  /posts/1 #  PATCH/PUT  /posts/1.json def update respond_to do  |format| if  @post.update(post_params) format.json {  render  json:  @user,  status:  :ok,  location:  @post  } else format.json {  render  json:  @post.errors,  status:  :unprocessable_entity } end end end #  DELETE  /posts/1 #  DELETE  /posts/1.json def destroy @post.destroy respond_to do  |format| format.json {  head  :no_content } end end private #  Use  callbacks  to  share  common  setup  or  constraints  between  actions. def set_post @post  =  Post.find_by(slug:  params[:id])  ||  Post.find(params[:id]) end #  Never  trust  parameters  from  the  scary  internet,  only  allow  the  white  list  through. def post_params params.require(:post).permit(:title,  :slug,  :content) end end
Globalcode  – Open4education Agora mãos a obra já temos uma “API" em Rails, poderíamos ter o código em Sinatra que seria mais leve, mas eu gosto do asset pipeline e assim fica mais fácil para um iniciante Falta criar os componentes backbone para acessar o backend Criar os componentes react para a UI
Globalcode  – Open4education backbone.js app/assets/javascripts/collections/posts.js var Posts = Backbone.Collection.extend({ model: Post, url: '/posts' }); app/assets/jaascripts/models/post.js var Post = Backbone.Model.extend({ });
Globalcode  – Open4education blog.js.jsx var Blog = React.createClass({ getInitialState: function(){ var posts = null; var post = null; if (this.props.collection) { posts = new Posts(this.props.collection); } else { posts = new Posts(); posts.fetch({success:function(data){ this.forceUpdate(); }.bind(this)}); } if (this.props.item) { post = new Post(this.props.item); } else { post = new Post(); } return { collection: posts, model: post, editing: false }; }, componentDidMount:function(){ this.router = new PostRouter({blog: this}); Backbone.history.start({pushState: true, root: "/"}); }, editModel:function(model){ this.setState({ model: model, editing: true }) }, viewModel:function(model){ this.setState({ model:  model, editing:  false });; this.router.navigate("/posts/"  +  model.get("slug"),  {trigger:  true});; }, newModel:function(){ this.setState({ model:  new  Post(), editing:  true }) }, render:  function   ()  { if(this.state.editing)  { return  (<div> <div  id="blogList"> <PostList collection={this.state.collection}  viewModel={this.viewModel}   newModel={this.newModel}/> </div> <div  id="blogPost"> <PostForm collection={this.state.collection}  model={this.state.model}   viewModel={this.viewModel}/> </div> </div>);; }else{ return  (<div> <div  id="blogList"> <PostList collection={this.state.collection}  viewModel={this.viewModel}   newModel={this.newModel}/> </div> <div  id="blogPost"> <PostView model={this.state.model}  editModel={this.editModel}/> </div> </div>);; } } });;
Globalcode  – Open4education post_list.js.jsx var PostList = React.createClass({ componentDidMount: function () { this.props.collection.on("change", function () { this.forceUpdate() }, this); this.props.collection.on("reset", function () { this.forceUpdate() }, this); }, componentWillUnmount: function () { this.props.collection.off(null, null, this); }, render: function () { var users = this.props.collection.map(function (model) { var viewModel = function () { this.props.viewModel(model); }; return ( <tr key={model.get("id")}> <td><a href="#" onClick={viewModel.bind(this)}>{model.get("title")}</a></td> <td>{new Date(model.get("created_at")).toDateString()}</td> </tr> ); }.bind(this)); return ( <div> <table className="post-list"> <thead> <tr> <th>Post</th> <th>Published</th> </tr> </thead> <tbody> {users} </tbody> </table> <hr/> <a href="#" onClick={this.props.newModel}>Create post</a> </div> ); } });
Globalcode  – Open4education post_view.js.jsx var PostView = React.createClass({ editModel: function () { this.props.editModel(this.props.model); }, render: function () { var innerLines = null; if(this.props.model.get("content")) { innerLines=_.map(this.props.model.get("content").split("n"), function (txt, idx) { return <p key={idx}>{txt}</p> }); } return ( <div className="blog-post"> <h1><a href={this.props.model.get("slug")}>{this.props.model.get("title")}</a></h1> <div className="post-body"> {innerLines} </div> <hr/> <a href="#" onClick={this.editModel}>Edit post</a> </div> ); } });
Globalcode  – Open4education post_form.js.jsx var PostForm = React.createClass({ saveModel: function () { if (this.props.model.get("id")) { this.props.model.save(); } else { this.props.collection.create(this.props.model); } this.props.viewModel(this.props.model) }, render: function () { return ( <div className="blog-post"> <InputWithLabel model={this.props.model} label="Title" name="title" type="text"/> <InputWithLabel model={this.props.model} label="Body" name="content" type="textarea"/> <div className="form-field"> <button onClick={this.saveModel}>Save</button> </div> </div> ); } });
Globalcode  – Open4education input_with_label.js.jsx var InputWithLabel = React.createClass({ handleChange: function(event) { this.props.model.set(this.props.name,event.target.value) }, render: function() { return <div className="form-field"> <label htmlFor={this.props.name}>{this.props.label}</label> <div> <input id={this.props.name} type={this.props.type} name={this.props.name} ref="input" onChange={this.handleChange} value={this.props.model.get(this.props.name)}/> </div> </div>; } });
Globalcode  – Open4education O que, quando, onde e por que? Muitas aplicações hoje em dia exigem um nível alto de interação com o usuário Implementar isto usando bibliotecas mais baixo nível é muito fácil de causar uma grande confusão do código (PHP alguem?) Componentização evita duplicação de código e facilita a organização
Globalcode  – Open4education
Globalcode  – Open4education indexação? performance? renderização no servidor: <%= react_component('Blog', {collection: @posts, item: @post}, {prerender: true}) %> components.js //= require underscore //= require backbone //= require_tree ./models //= require_tree ./collections //= require_tree ./components
Globalcode  – Open4education Mas é só isto? React-router Backbone.Router Flux - arquitetura JS usada pelo Facebook Pré processamento com Gulp (sintaxe do ECMAScript 6 suportada)
Globalcode  – Open4education

Interfaces ricas com Rails e React.JS @ Rubyconf 2015

  • 1.
    Globalcode  – Open4education Interfaces ricas  com  Rails  e  React.JS Rodrigo  Urubatan @urubatan
  • 2.
    Globalcode  – Open4education Quem? Programadordesde 1997 Trabalha com Ruby na Brightwire Escreveu "Ruby On Rails: Desenvolvimento fácil e Rápido de aplicações web” Já trabalhou com diversas linguagens (C, C++, Delphi, PHP, ASP, ColdFusion, VisualBasic, C#, Python, Ruby, Assembly, …) Apaixonado por Agile e atualmente por trabalho remoto Patinador, Corredor, Ciclista e agora resolveu aprender Karate :D Pai de um Guri de 6 anos http://urubatan.com.br - Anda meio abandonado mas vai voltar http://sobrecodigo.com - idem
  • 3.
    Globalcode  – Open4education Objetivo(O blog mais feio do mundo!)
  • 4.
    Globalcode  – Open4education Objetivo Usaro Rails como backend da aplicação Toda a interação com o usuário será implementada no cliente Validações e regras de negócio serão implementadas no servidor (sim, eu poderia usar Sinatra mas sou preguiçoso)
  • 5.
    Globalcode  – Open4education Cuidado! Nestapalestra vamos falar de uma SPA (Single Page Application) Isto tem vantagens e desvantagens Melhora muito a interação com o usuário sem duplicação de código, já que o código de renderização fica todo no JS Piora muito a indexação da sua aplicação por buscadores (adeus SEO - ou não…)
  • 6.
    Globalcode  – Open4education Criandoa aplicação Uma aplicação padrão Rails (rails new …) Gemfile updates gem 'backbone-on-rails' gem 'react-rails', github: 'reactjs/react-rails', ref: 'master' Environment update (development.rb para começar) config.react.variant = :development config.react.addons = true # defaults to false config.react.server_renderer = React::ServerRendering::SprocketsRenderer bundle install rails g react:install
  • 7.
    Globalcode  – Open4education Componentes Componentesjavascript vão ficar em app/assets/javascript/components Backbone.js vai facilitar a comunicação cliente/servidor arquivos .js.jsx tem uma facilidade extra, são compilados pelo react-rails via asset pipeline e permitem adicionar HTML inline
  • 8.
    Globalcode  – Open4education Cadastrandoum Usuário rails g scaffold post title:string slug:text content:text Apagar todas as views do post exceto index.html.erb Apagar todo o código de index.html.erb e mudar para: <%= react_component('Blog', {collection: @posts, item: @post}) %>
  • 9.
    Globalcode  – Open4education Alteraçõesno controller Fazer todos os métodos retornarem json, mais ou menos assim (não é a melhor tecnica, mas boa o suficiente para o exemplo) class PostsController < ApplicationController before_action :set_post, only: [:show, :edit, :update, :destroy] def index @posts = Post.all end def show @posts = Post.all render :index end # GET /posts/new def new @post = Post.new render :index end # POST /posts # POST /posts.json def create @post = Post.new(post_params) respond_to do |format| if @post.save format.json { render json: @post, status: :created, location: @post } else format.json { render json: @post.errors, status: :unprocessable_entity } end end end #  PATCH/PUT  /posts/1 #  PATCH/PUT  /posts/1.json def update respond_to do  |format| if  @post.update(post_params) format.json {  render  json:  @user,  status:  :ok,  location:  @post  } else format.json {  render  json:  @post.errors,  status:  :unprocessable_entity } end end end #  DELETE  /posts/1 #  DELETE  /posts/1.json def destroy @post.destroy respond_to do  |format| format.json {  head  :no_content } end end private #  Use  callbacks  to  share  common  setup  or  constraints  between  actions. def set_post @post  =  Post.find_by(slug:  params[:id])  ||  Post.find(params[:id]) end #  Never  trust  parameters  from  the  scary  internet,  only  allow  the  white  list  through. def post_params params.require(:post).permit(:title,  :slug,  :content) end end
  • 10.
    Globalcode  – Open4education Agoramãos a obra já temos uma “API" em Rails, poderíamos ter o código em Sinatra que seria mais leve, mas eu gosto do asset pipeline e assim fica mais fácil para um iniciante Falta criar os componentes backbone para acessar o backend Criar os componentes react para a UI
  • 11.
    Globalcode  – Open4education backbone.js app/assets/javascripts/collections/posts.js varPosts = Backbone.Collection.extend({ model: Post, url: '/posts' }); app/assets/jaascripts/models/post.js var Post = Backbone.Model.extend({ });
  • 12.
    Globalcode  – Open4education blog.js.jsx varBlog = React.createClass({ getInitialState: function(){ var posts = null; var post = null; if (this.props.collection) { posts = new Posts(this.props.collection); } else { posts = new Posts(); posts.fetch({success:function(data){ this.forceUpdate(); }.bind(this)}); } if (this.props.item) { post = new Post(this.props.item); } else { post = new Post(); } return { collection: posts, model: post, editing: false }; }, componentDidMount:function(){ this.router = new PostRouter({blog: this}); Backbone.history.start({pushState: true, root: "/"}); }, editModel:function(model){ this.setState({ model: model, editing: true }) }, viewModel:function(model){ this.setState({ model:  model, editing:  false });; this.router.navigate("/posts/"  +  model.get("slug"),  {trigger:  true});; }, newModel:function(){ this.setState({ model:  new  Post(), editing:  true }) }, render:  function   ()  { if(this.state.editing)  { return  (<div> <div  id="blogList"> <PostList collection={this.state.collection}  viewModel={this.viewModel}   newModel={this.newModel}/> </div> <div  id="blogPost"> <PostForm collection={this.state.collection}  model={this.state.model}   viewModel={this.viewModel}/> </div> </div>);; }else{ return  (<div> <div  id="blogList"> <PostList collection={this.state.collection}  viewModel={this.viewModel}   newModel={this.newModel}/> </div> <div  id="blogPost"> <PostView model={this.state.model}  editModel={this.editModel}/> </div> </div>);; } } });;
  • 13.
    Globalcode  – Open4education post_list.js.jsx varPostList = React.createClass({ componentDidMount: function () { this.props.collection.on("change", function () { this.forceUpdate() }, this); this.props.collection.on("reset", function () { this.forceUpdate() }, this); }, componentWillUnmount: function () { this.props.collection.off(null, null, this); }, render: function () { var users = this.props.collection.map(function (model) { var viewModel = function () { this.props.viewModel(model); }; return ( <tr key={model.get("id")}> <td><a href="#" onClick={viewModel.bind(this)}>{model.get("title")}</a></td> <td>{new Date(model.get("created_at")).toDateString()}</td> </tr> ); }.bind(this)); return ( <div> <table className="post-list"> <thead> <tr> <th>Post</th> <th>Published</th> </tr> </thead> <tbody> {users} </tbody> </table> <hr/> <a href="#" onClick={this.props.newModel}>Create post</a> </div> ); } });
  • 14.
    Globalcode  – Open4education post_view.js.jsx varPostView = React.createClass({ editModel: function () { this.props.editModel(this.props.model); }, render: function () { var innerLines = null; if(this.props.model.get("content")) { innerLines=_.map(this.props.model.get("content").split("n"), function (txt, idx) { return <p key={idx}>{txt}</p> }); } return ( <div className="blog-post"> <h1><a href={this.props.model.get("slug")}>{this.props.model.get("title")}</a></h1> <div className="post-body"> {innerLines} </div> <hr/> <a href="#" onClick={this.editModel}>Edit post</a> </div> ); } });
  • 15.
    Globalcode  – Open4education post_form.js.jsx varPostForm = React.createClass({ saveModel: function () { if (this.props.model.get("id")) { this.props.model.save(); } else { this.props.collection.create(this.props.model); } this.props.viewModel(this.props.model) }, render: function () { return ( <div className="blog-post"> <InputWithLabel model={this.props.model} label="Title" name="title" type="text"/> <InputWithLabel model={this.props.model} label="Body" name="content" type="textarea"/> <div className="form-field"> <button onClick={this.saveModel}>Save</button> </div> </div> ); } });
  • 16.
    Globalcode  – Open4education input_with_label.js.jsx varInputWithLabel = React.createClass({ handleChange: function(event) { this.props.model.set(this.props.name,event.target.value) }, render: function() { return <div className="form-field"> <label htmlFor={this.props.name}>{this.props.label}</label> <div> <input id={this.props.name} type={this.props.type} name={this.props.name} ref="input" onChange={this.handleChange} value={this.props.model.get(this.props.name)}/> </div> </div>; } });
  • 17.
    Globalcode  – Open4education Oque, quando, onde e por que? Muitas aplicações hoje em dia exigem um nível alto de interação com o usuário Implementar isto usando bibliotecas mais baixo nível é muito fácil de causar uma grande confusão do código (PHP alguem?) Componentização evita duplicação de código e facilita a organização
  • 18.
  • 19.
    Globalcode  – Open4education indexação?performance? renderização no servidor: <%= react_component('Blog', {collection: @posts, item: @post}, {prerender: true}) %> components.js //= require underscore //= require backbone //= require_tree ./models //= require_tree ./collections //= require_tree ./components
  • 20.
    Globalcode  – Open4education Masé só isto? React-router Backbone.Router Flux - arquitetura JS usada pelo Facebook Pré processamento com Gulp (sintaxe do ECMAScript 6 suportada)
  • 21.