Rails and React « Tour of Heroes » tutorial

Rails and React – Plain Javascript – without npm

Ok, let’s see how it is going with React now. I tried to remain the closest as possible from Angular2, to compare them more easily.

For React we are going to use the very nice react-rails gem. I tried first the react_on_rails gem, which includes a lot of good tools and librairies (Webpack, Babel, React, Redux, React-Router), but after following the tutorial and installing npm, it tooks around 15 minutes to install all the required node modules, and the project weighted 600MB (!!!) before I even started to write a single line of code (hum… ok, I probably have all the modules I need for the next 200 years, but seriously…). So, I gave up: I want my project to remain light!

We are also going to use react-router javascript library, I will show you how to do that, it’s easy.

1 – Rails configuration

1.1 – Create a new rails project

rails new reactjs_project

1.2 – Update your Gemfile

In your Gemfile, uncomment ‘therubyracer’ gem and add the react-rails gem.

In Gemfile:

source 'https://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.5.2'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.1.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
gem 'therubyracer', platforms: :ruby

# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc

# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Unicorn as the app server
# gem 'unicorn'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug'
end

group :development do
  # Access an IRB console on exception pages or by using <%= console %> in views
  gem 'web-console', '~> 2.0'

  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
end

#Use reactJS (after bundle install, type: "rails g react:install")
gem 'react-rails'

And type the following in your console:

rails g react:install

As the react-rails gem documentation explains:

This will:

  • create a components.js manifest file and a app/assets/javascripts/components/ directory, where you will put your components
  • place the following in your application.js:

1.3 – Get ReactRouter Javascript library and put it in vendor directory

We want the routing to be managed by the client, not the server, so we need the ReactRouter Javascript library. You can find the link in the github project: react-router. And download it from here: https://npmcdn.com/react-router/umd/ReactRouter.min.js. You can reference it in your application.html.erb file, but I suggest to download it and put it in your vendor/assets/javascripts directory.

get https://npmcdn.com/react-router/umd/ReactRouter.min.js file and put it there: vendor/assets/javascripts/ReactRouter.min.js

1.4 – Update your routes

It is exactly the same thing as with angular2. So I quote myself:

The first line says to start the application with application controller and index action. The second one will be used to get the list of heroes from the server, show a hero, and update it. And the last line enables to redirect all other url to application controller index action.

In config/routes.rb:

Rails.application.routes.draw do
  root 'application#index'

  resources :heroes, only: [:index, :show, :update]

  match "*path", to: 'application#index', via: [ :get, :post ]
end

1.5 – Update your application controller

Here, we call the react_component method (from react-rails gem) to call the React StaticRoute component. It will be the starting point of React.

In app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  
  def index
    render inline: "<%= react_component('StaticRoute') %>", layout: 'application'
  end
end

1.6 – Do not change the application view

Note: we do not need to update the application view as we did with Angular2, because we have already configured the application starting point in application_controller.rb. So we leave application.html.erb unchanged. I show it here so we can compare all files changed in Angular2 and React.

In app/views/layouts/application.html.erb:

<!DOCTYPE html>
<html>
<head>
  <title>ReactJSWithReactRails</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>

1.7 – Create a hero controller

Here we use the same controller as with Angular2. I remind you that it is only for testing purposes, for a real application you have to use database and models (and do not forget to keep your controllers as light as possible!).

In app/controllers/heroes_controller.rb:

class HeroesController < ApplicationController

  def index 
    heroes_list_json = []
    #we get all the heroes from the file
    File.open(Rails.root.to_s + '/tmp/heroes_list', 'r').each_line { |l| heroes_list_json << Hash[*l.chomp.split(/,|:/)] }
    respond_to do |format|
      format.json { render json: heroes_list_json }
    end
  end
  
  def show
    hero = {}
    #we get only the hero which match the params[:id]
    File.open(Rails.root.to_s + '/tmp/heroes_list', 'r').each_line { |l| hero = Hash[*l.chomp.split(/,|:/)] if l.split(',')[0].split('id:')[1] == params[:id] }
    respond_to do |format|
      format.json { render json: hero }
    end
  end
  
  def update
    original_file_path = Rails.root.to_s + '/tmp/heroes_list'
    temp_file_path = Rails.root.to_s + '/tmp/heroes_list.tmp'
    
    #we create a temporary file in which we copy the line of the original file. we only change the line where the id is equal to params[:id]
    temp_file = File.open(temp_file_path, 'w')
    File.open(original_file_path, 'r').each_line do |line|
      if line.split(',')[0].split('id:')[1] == params[:id]
        temp_file.puts "id:#{params[:id]},name:#{params[:name]}"
      else
         temp_file.puts line
      end
    end
    temp_file.close
    FileUtils.mv(temp_file_path, original_file_path)
      
    respond_to do |format|
      format.json { render nothing: true }
    end
  end

end

1.8 – Create a heroes_list file (only as example! use database)

Same file here too.

In tmp/heroes_list:

id:11,name:Mr. Nice
id:12,name:Narco
id:13,name:Bombasto
id:14,name:Celeritas
id:15,name:Magneta
id:16,name:RubberMan
id:17,name:Dynama
id:18,name:Dr IQ
id:19,name:Magma
id:20,name:Tornado

And that’s all for the rails configuration. Let’s do the React one now…

2 – React configuration

Files tree:
 
(app/assets)
|
|_ (javascripts)
| |_ application.js
| |_ components.js
| |_ routes.js.jsx
| |
| |_ (components)
| | |_ app-component.js.jsx
| | |_ dashboard-component.js.jsx
| | |_ heroes-component.js.jsx
| | |_ hero-detail-component.js.jsx
| |
| |_ (services)
| |_ heroes-service.js.jsx
|
|_ (stylesheets)
| |_ style.css
| |_ application.css
| |_ app.component.css
| |_ dashboard.component.css
| |_ hero-detail.component.css

2.1 – javascripts configuration

2.1.1 – Update application.js

Do not forget to require the ReactRouter.min library, which will allow you to manage the routing on client side.

In app/assets/javascripts/application.js:

//= require jquery
//= require jquery_ujs

//= require react
//= require react_ujs
//= require ReactRouter.min
//= require components

2.1.2 – Update components.js

In app/assets/javascripts/components.js:

//= require routes
//= require_tree ./services
//= require_tree ./components

2.1.3 – Create routes.js.jsx

In app/assets/javascripts/routes.js.jsx:

var ReactRouter = window.ReactRouter
var Router = ReactRouter.Router
var Route = ReactRouter.Route
var Link = ReactRouter.Link
var Redirect = ReactRouter.Redirect
var browserHistory = ReactRouter.browserHistory

var StaticRoute = React.createClass({
  render: function() {
    return (
        <Router history={browserHistory}>
          <Redirect from="/" to="/dashboard" />
          <Route path="/" component={AppComponent}>            
            <Route path="/dashboard" component={DashboardComponent}></Route>
            <Route path="/heroes-list" component={HeroesComponent}></Route>
            <Route path="/hero-detail/:id" component={HeroDetailComponent} />
          </Route>
        </Router>
      );
  }
});

2.1.4 – Create components directory and following files

In app/assets/javascripts/application/components/app-component.js.jsx:

var AppComponent = React.createClass({
  render: function() {    
    var title = 'Tour of Heroes'

    return (
      <div>
        <h1>{title}</h1>
        <nav>
           <Link to='/heroes-list'>Heroes</Link>
           <Link to='/dashboard'>Dashboard</Link>
        </nav>
        {this.props.children}
      </div>
    )
  }
})

 

In app/assets/javascripts/application/components/dashboard-component.js.jsx:

var DashboardComponent = React.createClass({
  getInitialState: function() {
    return { heroes_list: [], heroservice: new HeroService() };
  },
  componentDidMount: function() {
    this.state.heroservice.getHeroes(this);
  },
  
  handleDetailClick: function (id) {
    browserHistory.push('/hero-detail/' + id);
  },
  
  render: function() {
    return (
      <div>
        <h3>Top Heroes</h3>
        <div className='grid grid-pad'>          
          { this.state.heroes_list.slice(0,4).map(
            function(hero) {                
              return (
                <div key={hero.id} className='col-1-4' >
                  <div className='module hero' onClick={this.handleDetailClick.bind(this,hero.id)}><h4>{hero.name}</h4></div>
                </div>    
              )
            }.bind(this)
          )}          
        </div>
      </div>      
    )
  }
})

In app/assets/javascripts/application/components/heroes-component.js.jsx:

var HeroesComponent = React.createClass({
  getInitialState: function() {
    return { heroes_list: [], heroSelected: '', heroservice: new HeroService() };
  },
  componentDidMount: function() {
    this.state.heroservice.getHeroes(this);
  },
  
  handleClick: function (hero) {
    this.setState( { heroSelected: hero });
  },
  handleDetailClick: function (id) {
    browserHistory.push('/hero-detail/' + id);
  },
  
  render: function() {
    return (
    <div>
      <h2>My Heroes</h2>
      <ul className='heroes'>      
        { this.state.heroes_list.map(
            function(hero) {
              var liClass = '';
              if(this.state.heroSelected && (this.state.heroSelected == hero)) { liClass = 'selected' };
              return (
                <li key={hero.id} className={liClass} onClick={this.handleClick.bind(this,hero)}>
                  <span className='badge'>{hero.id}</span> {hero.name}
                </li>
              )
            }.bind(this)
        )}
      </ul>
      { (function() {
          if(this.state.heroSelected) {
            return (
              <div>
                <h2>{this.state.heroSelected.name.toUpperCase()} is my hero</h2>
                <button onClick={this.handleDetailClick.bind(this,this.state.heroSelected.id)}>View Details</button>
              </div>
            )
          }
        }.bind(this))()
      }
    </div>
    )
  }
})

In app/assets/javascripts/application/components/hero-detail-component.js.jsx:

var HeroDetailComponent = React.createClass({
  getInitialState: function() {
    return { hero: '', heroservice: new HeroService() };
  },
  componentDidMount: function() {
    this.state.heroservice.getHero(this,this.props.params.id);
  },
  
  handleChange: function (e) {
    name = e.target.value;
    this.updateName(name);
  },
  updateName: function (name) {
     this.state.hero.name = name;
     this.setState( { hero: this.state.hero });
  },
  goBackClick: function (id) {
    window.history.back();
  },
  updateHeroClick: function () {
    this.state.heroservice.updateHero(this.state.hero.id,this.state.hero.name);
    alert("updated!");
  },
  
  render: function() {
    return (
      <div>
        <h2>{this.state.hero.name} details</h2>
        <div>
          <label>id: </label>{this.state.hero.id}
        </div>
        <div>
          <label>name: </label>
          <input type="text" value={this.state.hero.name} placeholder={this.state.hero.name} onChange={this.handleChange} />
        </div>
        <button onClick={this.goBackClick}>Back</button>
        <button onClick={this.updateHeroClick}>Update</button>
      </div>
    )
  }
})

2.1.5 – Create services directory and following file

In app/assets/javascripts/services/heroes-service.js.jsx:

var HeroService = function () {  
  this.getHeroes = function(component) {
    $.get('/heroes', function (results) { component.setState( { heroes_list: results } ) });
  }
  
  this.getHero = function(component,id)  {
    $.get('/heroes/' + id, function (result) { component.setState( { hero: result } ) });
  }
  
  this.updateHero = function(id,newname)  {
    //jquery does not have a "put" method, so we use ajax method:    
    $.ajax({ url: '/heroes/' + id, type: 'put', data: {id: id, name: newname}});
    
    //in future time, it will probably be wise to define a global "put" method in js so that we can use following method:
    //$.put('/heroes/' + id, {id: id, name: newname}, function (result) { return });
  }
};

/* 
other method:
  $.ajax({
    url: '/heroes',
    dataType: 'json',
    success: function(results) {
      component.setState( { heroes_list: results } );
    },
    error: function(xhr, status, err) {
      console.error('/heroes', status, err.toString());
    }
  });
*/

2.2 – stylesheets configuration

Exactly the same stylesheets configuration as with our Angular2 project.

2.2.1 – Update application.css if necessary

In app/assets/stylesheets/application.css:

/*
 *= require_tree .
 *= require_self
 */

2.2.2 – Create the following files

In app/assets/stylesheets/style.css:

h2 {
  color: #444;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: lighter;
}
body {
  margin: 2em;
}
body, input[text], button {
  color: #888;
  font-family: Cambria, Georgia;
}
button {
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  cursor: hand;
}
button:hover {
  background-color: #cfd8dc;
}
button:disabled {
  background-color: #eee;
  color: #aaa;
  cursor: auto;
}
/* everywhere else */
* {
  font-family: Arial, Helvetica, sans-serif;
}

In app/assets/stylesheets/app.component.css:

h1 {
  font-size: 1.2em;
  color: #999;
  margin-bottom: 0;
}
h2 {
  font-size: 2em;
  margin-top: 0;
  padding-top: 0;
}
nav a {
  padding: 5px 10px;
  text-decoration: none;
  margin-top: 10px;
  display: inline-block;
  background-color: #eee;
  border-radius: 4px;
}
nav a:visited, a:link {
  color: #607D8B;
}
nav a:hover {
  color: #039be5;
  background-color: #CFD8DC;
}
nav a.router-link-active {
  color: #039be5;
}

In app/assets/stylesheets/dashboard.component.css:

.selected {
  background-color: #CFD8DC !important;
  color: white;
}

.heroes {
  margin: 0 0 2em 0;
  list-style-type: none;
  padding: 0;
  width: 10em;
}

.heroes li {
  cursor: pointer;
  position: relative;
  left: 0;
  background-color: #EEE;
  margin: .5em;
  padding: .3em 0;
  height: 2em;
  border-radius: 4px;
}

.heroes li.selected:hover {
  background-color: #BBD8DC !important;
  color: white;
}

.heroes li:hover {
  color: #607D8B;
  background-color: #DDD;
  left: .1em;
}

.heroes .text {
  position: relative;
  top: -3px;
}

.heroes .badge {
  display: inline-block;
  font-size: small;
  color: white;
  padding: 0.8em 0.7em 0 0.7em;
  background-color: #607D8B;
  line-height: 1em;
  position: relative;
  left: -1px;
  top: -4px;
  height: 2.4em;
  margin-right: .8em;
  border-radius: 4px 0 0 4px;
}

[class*='col-'] {
  float: left;
}
*, *:after, *:before {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}
h3 {
  text-align: center; margin-bottom: 0;
}
[class*='col-'] {
  padding-right: 20px;
  padding-bottom: 20px;
}
[class*='col-']:last-of-type {
  padding-right: 0;
}
.grid {
  margin: 0;
}
.col-1-4 {
  width: 25%;
}
.module {
    padding: 20px;
    text-align: center;
    color: #eee;
    max-height: 120px;
    min-width: 120px;
    background-color: #607D8B;
    border-radius: 2px;
}
h4 {
  position: relative;
}
.module:hover {
  background-color: #EEE;
  cursor: pointer;
  color: #607d8b;
}
.grid-pad {
  padding: 10px 0;
}
.grid-pad [class*='col-']:last-of-type {
  padding-right: 20px;
}
@media (max-width: 600px) {
    .module {
      font-size: 10px;
      max-height: 75px; }
}
@media (max-width: 1024px) {
    .grid {
      margin: 0;
    }
    .module {
      min-width: 60px;
    }
}

In app/assets/stylesheets/hero-detail.component.css:

label {
  display: inline-block;
  width: 3em;
  margin: .5em 0;
  color: #607D8B;
  font-weight: bold;
}
input {
  height: 2em;
  font-size: 1em;
  padding-left: .4em;
}
button {
  margin-top: 20px;
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer; cursor: hand;
}
button:hover {
  background-color: #cfd8dc;
}
button:disabled {
  background-color: #eee;
  color: #ccc;
  cursor: auto;
}

3 – Start the server and see the results

rails s

And with your favorite browser go to: http://your-server

That’s all for rails with React…


Publicités

2 réflexions sur “Rails and React « Tour of Heroes » tutorial

  1. Hi, I have followed this tutorial and it worked well however I keep having the following error message in my rails development console: « Cannot render console from ! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 »
    Do you have any idea where it comes from?

    J'aime

    • Hello Hanz, this message comes from your web-console gem (which is by default in your Gemfile for your development environment). You can have some information here: https://github.com/rails/web-console
      As the documentation says, if you want to turn it off, you can add the following line in your config/environments/development.rb file:
      config.web_console.whiny_requests = false
      Then you restart your rails server and the message should not appear again.

      J'aime

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s