Rails and React – Full tutorial

1 – Introduction

Ok, now that I have been studying React environment for the past few months, I think that it is the right time to present a complete example of what we can do, and how to do it cleanly.

1.1 – The « what »

Right below is what we are going to do. It is completely inspired from « The Tour of Heroes » example created by the Angular2 team, so all credits for the idea go to them. It is a simple example with a public home page, and a private part displaying a list of video games and the list of the best ones (if you do not agree with the ranking, let me know 😉 ):

The home page

It is the only public page of our application. This page will be prerendered (rendered on server side) so that it will generate HTML, and therefore will be easier to be crawled by search engines (Google seems to be pretty good at crawling javascript, but we have to care about the other ones).

home_page

The login page

If you click on the « Video Games » button on the page before, you will be asked to log in. This page is managed by Devise (rails gem). If you are already logged in, you will automatically be redirected to the list of the best video games. I did not take the time to make a link to the « sign up » page, but you can get to it by changing « sign_in » with « sign_up » in the url.

login_page

Best Video Games list page

You will see a title « Video Games », 2 links and a list of 4 video games (and an image). The part with the title and the links will be on all your private pages. If you click on « Video Games List » you will get to the list of all the video games (and if you click on « Best Video Games » you will go back to this page). If you click on one of the video game, you will get to the « Video Game Detail » page.

best_videogames_page

Video games list page

Here you will see the list of all the video games recorded in your database.

videogame_list_page

If you click on one of them, the name will be displayed below, with a link to the « Video Game Detail » page.

videogame_selected_page

Video game detail page

This page will show the id and name of the selected video game (either from the best video games list or the full video games list). If you modify the name in the text field, it will automatically and instantly modify the title. If you click on the « Update » button, it will modify the name in the database, and display a pop-up saying « updated! ».

videogame_detail_page

1.2 – The « how »

And here are the technologies that we are going to use to do it:

  • Ruby on Rails (and Devise)
  • React (with react-rails gem)
  • React-router
  • Alt
  • Containers (with Alt Container)
  • Immutable (and PureRenderMixin)

We do not use npm so that our project remains light and easy (I have had some bad experience with npm, which downloaded 500Mb of node modules before I even started to write a single line of code).

Ruby on Rails

No need to speak a lot about it. It is a mature and efficient technology to develop Web sites, which helps you to do it fast and clean.

You may want to read more here: http://rubyonrails.org/

React

It is a javascript library built by Facebook team, to manage the views (and only the views!) of your website. It displays the DOM very quickly and enables to split up your code to improve visibility and maintenance.

The official website: https://facebook.github.io/react/

We are going to use the great react-rails gem (rails library), which contains everything we need: https://github.com/reactjs/react-rails

React-Router

React-Router enables you to handle routing on client side. Even though Rails has a great routing system, I found it pretty useful to manage all my private links with react-router. One of the benefit is that you spare some server requests as your client do not ask the server for every route.

Official website: https://github.com/reactjs/react-router

Alt

Alt is a flux compliant library. If you do not know why to use a flux architecture, you may want to take a look here: https://facebook.github.io/flux/docs/overview.html. I had a quick view at different flux implementations, and Alt was exactly what I was looking for: light, easy to use, with a good documentation and community, and with a lot of automatic mechanisms.

Do not hesitate to visit the official website: http://alt.js.org/

Containers

Containers are very important to structure your code, and force you to develop cleanly. I found this article quite interesting: https://medium.com/@learnreact/container-components-c0e67432e005#.oiysew4cr

We are going to use Alt containers as they offer a few tricks to lighten our code.

Immutable

Immutable enables to compare deeply and easily two Javascript Objects. It will be very useful to improve performance, as it will enable to render React components only if required (thanks to the PureRenderMixin React add on).

You can find the official documentation of Immutable here: https://facebook.github.io/immutable-js/, and you can read an explanation of the functioning in one of my previous post: React and Immutable.js

 

We are now ready, so let’s start…

 

1 – Server configuration (Rails)

Modified files tree:
/
|
|_config
| |
| |_environments
| | |
| | |_development.rb
| | |
| | |_production.rb
| |
| |_applications.rb
| |
| |_routes.rb
| |
| |_database.yml
|
|_db
| |
| |_migrate
| | |
| | |_xxxxxxxxxxxx_create_video_games.rb
| |
| |_seeds.rb
|
|_app
| |
| |_controllers
| | |
| | |_home_controller.rb
| | |
| | |_private_controller.rb
| | |
| | |_video_games_controller.rb
|
|_vendor
| |
| |_assets
| | |
| | |_javascripts
| | | |
| | | |_ReactRouter.min.js
| | | |
| | | |_alt.js
| | | |
| | | |_alt-container.js
| | | |
| | | |_immutable.min.js
|
|_Gemfile

1.1 – Create a new rails project

rails new react_full_example

1.2 – Update Gemfile

We need a javascript runtime (to manage javascript), so we are going to use « therubyracer« . I have seen different point of view whether nodejs or therubyracer is better, but it seems that therubyracer is now more efficient (if you prefer nodejs, do not uncomment the following line, and install nodejs with: apt-get install nodejs). For therubyracer, we uncomment this line in our Gemfile:

gem 'therubyracer', platforms: :ruby

We are going to use mysql database. The installation of mysql is out of this scope. We will consider that mysql has already been installed, and that a database named « react_tests » was created with user/password equal to react_test/react_test. Now we add the following line in our Gemfile:

gem 'mysql2'

We are going to have a « private » part, that is to say a part which requires authentication. As authentication mechanism I suggest we use devise. Let’s add it in our Gemfile:

gem 'devise'

To include React js in our project, there is a great gem called react-rails. It contains everything we need such as the React library, the addons libraries, and a .jsx transformer (currently Babel). So, we add this line:

gem 'react-rails'

Finally, we are going to use an es6 transpiler as we want all our javascript code to be in ES6 langage. So we add this line:

gem 'sprockets-es6'

Here is how the Gemfile should look like:

/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'
gem 'mysql2'
# 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'

#Enables ES6 (otherwise you may encounter javascript errors with the current es6 transpiler)
gem 'sprockets-es6'

gem 'devise'

Do not forget to run:

bundle install

1.3 – Generate gem files

Now, we have to run the following commands, to create the files associated to devise and react gems. You can check official documentations if necessary to understand what it is doing exactly:

rails generate devise:install
rails generate devise User
rails g react:install

1.4 – Configure the gems (mysql and React)

mysql

The configuration is made in database.yml file. Below is an example, but adapt it to fit your own database configuration.

/config/database.yml

development:
  adapter: mysql2
  encoding: utf8
  collation: utf8_general_ci
  database: react_tests
  host: 127.0.0.1
  pool: 5
  username: react_test
  password: react_test

react

First, we modify application.rb by adding config.react.server_renderer_options  and config.react.addons. The first variable enables to configure server rendering, and the second one activates React add ons (in our case we are mainly interested by the PureRenderMixin add on).

/config/application.rb

require File.expand_path('../boot', __FILE__)

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module ReactJSWithReactRails
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
    # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
    # config.time_zone = 'Central Time (US & Canada)'

    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
    # config.i18n.default_locale = :de

    # Do not swallow errors in after_commit/after_rollback callbacks.
    config.active_record.raise_in_transactional_callbacks = true
    
    config.react.server_renderer_options = {
      files: ["react-server.js", "alt_init.js", "routes_init.js", "prerender.js"], # files to load for prerendering
      replay_console: true,                 # if true, console.* will be replayed client-side
    }
    
    #reactjs
    config.react.addons = true
  end
end

Then we only add a line (config.react.variant initialization), in the two following files to optimize the react library depending whether we are on development or production environment.

/config/environments/development.rb

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # In the development environment your application's code is reloaded on
  # every request. This slows down response time but is perfect for development
  # since you don't have to restart the web server when you make code changes.
  config.cache_classes = false

  # Do not eager load code on boot.
  config.eager_load = false

  # Show full error reports and disable caching.
  config.consider_all_requests_local       = true
  config.action_controller.perform_caching = false

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  # Print deprecation notices to the Rails logger.
  config.active_support.deprecation = :log

  # Raise an error on page load if there are pending migrations.
  config.active_record.migration_error = :page_load

  # Debug mode disables concatenation and preprocessing of assets.
  # This option may cause significant delays in view rendering with a large
  # number of complex assets.
  config.assets.debug = true

  # Asset digests allow you to set far-future HTTP expiration dates on all assets,
  # yet still be able to expire them through the digest params.
  config.assets.digest = true

  # Adds additional error checking when serving assets at runtime.
  # Checks for improperly declared sprockets dependencies.
  # Raises helpful error messages.
  config.assets.raise_runtime_errors = true

  # Raises error for missing translations
  # config.action_view.raise_on_missing_translations = true
  
    
  #to hide the following error message: "Cannot render console from 31.34.231.93! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255"
  config.web_console.whiny_requests = false
  
  #reactjs
  config.react.variant = :development  
end

/config/environments/production.rb

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Code is not reloaded between requests.
  config.cache_classes = true

  # Eager load code on boot. This eager loads most of Rails and
  # your application in memory, allowing both threaded web servers
  # and those relying on copy on write to perform better.
  # Rake tasks automatically ignore this option for performance.
  config.eager_load = true

  # Full error reports are disabled and caching is turned on.
  config.consider_all_requests_local       = false
  config.action_controller.perform_caching = true

  # Enable Rack::Cache to put a simple HTTP cache in front of your application
  # Add `rack-cache` to your Gemfile before enabling this.
  # For large-scale production use, consider using a caching reverse proxy like
  # NGINX, varnish or squid.
  # config.action_dispatch.rack_cache = true

  # Disable serving static files from the `/public` folder by default since
  # Apache or NGINX already handles this.
  config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?

  # Compress JavaScripts and CSS.
  config.assets.js_compressor = :uglifier
  # config.assets.css_compressor = :sass

  # Do not fallback to assets pipeline if a precompiled asset is missed.
  config.assets.compile = false

  # Asset digests allow you to set far-future HTTP expiration dates on all assets,
  # yet still be able to expire them through the digest params.
  config.assets.digest = true

  # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb

  # Specifies the header that your server uses for sending files.
  # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
  # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  # config.force_ssl = true

  # Use the lowest log level to ensure availability of diagnostic information
  # when problems arise.
  config.log_level = :debug

  # Prepend all log lines with the following tags.
  # config.log_tags = [ :subdomain, :uuid ]

  # Use a different logger for distributed setups.
  # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)

  # Use a different cache store in production.
  # config.cache_store = :mem_cache_store

  # Enable serving of images, stylesheets, and JavaScripts from an asset server.
  # config.action_controller.asset_host = 'http://assets.example.com'

  # Ignore bad email addresses and do not raise email delivery errors.
  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
  # config.action_mailer.raise_delivery_errors = false

  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
  # the I18n.default_locale when a translation cannot be found).
  config.i18n.fallbacks = true

  # Send deprecation notices to registered listeners.
  config.active_support.deprecation = :notify

  # Use default logging formatter so that PID and timestamp are not suppressed.
  config.log_formatter = ::Logger::Formatter.new

  # Do not dump schema after migrations.
  config.active_record.dump_schema_after_migration = false
  
  #reactjs
  config.react.variant = :production
end

1.5 – Get ReactRouter, Alt and Immutable Javascript Libraries

ReactRouter

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

Alt

The Alt guide says that you can get a browser build of alt here. So download this file and copy it in your vendor/assets/javascripts directory.

get https://cdn.rawgit.com/goatslacker/alt/master/dist/alt.js file and put it there: vendor/assets/javascripts/alt.js

Alt-container

For this one, I have not been able to find a plain javascript library. So I took an ES6 version here: https://github.com/altjs/container/tree/master/src, and transformed it in a plain javascript file so that I could put in my vendor directory (I could probably have used the ES6 version but it does not work in vendor directories and I did not want to mix it with my other javascript files).

/vendor/assets/javascripts/alt-container.js

'use strict';

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }


var object_assign = function( O, dictionary ) {
  var target, src;

  // Let target be ToObject(O).
  target = Object( O );

  // Let src be ToObject(dictionary).
  src = Object( dictionary );

  // For each own property of src, let key be the property key
  // and desc be the property descriptor of the property.
  Object.getOwnPropertyNames( src ).forEach(function( key ) {
    target[ key ] = src[ key ];
  });

  return target;
};

var id = function id(it) {
  return it;
};
var getStateFromStore = function getStateFromStore(store, props) {
  return typeof store === 'function' ? store(props).value : store.getState();
};
var getStateFromKey = function getStateFromKey(actions, props) {
  return typeof actions === 'function' ? actions(props) : actions;
};

var getStateFromActions = function getStateFromActions(props) {
  if (props.actions) {
    return getStateFromKey(props.actions, props);
  } else {
    return {};
  }
};

var getInjected = function getInjected(props) {
  if (props.inject) {
    return Object.keys(props.inject).reduce(function (obj, key) {
      obj[key] = getStateFromKey(props.inject[key], props);
      return obj;
    }, {});
  } else {
    return {};
  }
};

var reduceState = function reduceState(props) {
  return object_assign({}, getStateFromStores(props), getStateFromActions(props), getInjected(props));
};

var getStateFromStores = function getStateFromStores(props) {
  var stores = props.stores;
  if (props.store) {
    return getStateFromStore(props.store, props);
  } else if (props.stores) {
    // If you pass in an array of stores then we are just listening to them
    // it should be an object then the state is added to the key specified
    if (!Array.isArray(stores)) {
      return Object.keys(stores).reduce(function (obj, key) {
        obj[key] = getStateFromStore(stores[key], props);
        return obj;
      }, {});
    }
  } else {
    return {};
  }
};

// TODO need to copy some other contextTypes maybe?
// what about propTypes?

var AltContainer = (function (_React$Component) {
  _inherits(AltContainer, _React$Component);
  
  AltContainer.contextTypes = {
    flux: React.PropTypes.object,
  }

  AltContainer.childContextTypes =  {
    flux: React.PropTypes.object,
  }

  _createClass(AltContainer, [{
    key: 'getChildContext',
    value: function getChildContext() {
      var flux = this.props.flux || this.context.flux;
      return flux ? { flux: flux } : {};
    }
  }]);

  function AltContainer(props) {
    _classCallCheck(this, AltContainer);

    _get(Object.getPrototypeOf(AltContainer.prototype), 'constructor', this).call(this, props);

    if (props.stores && props.store) {
      throw new ReferenceError('Cannot define both store and stores');
    }

    this.storeListeners = [];

    this.state = reduceState(props);
  }

  _createClass(AltContainer, [{
    key: 'componentWillReceiveProps',
    value: function componentWillReceiveProps(nextProps) {
      this._destroySubscriptions();
      this.setState(reduceState(nextProps));
      this._registerStores(nextProps);
      if (this.props.onWillReceiveProps) {
        this.props.onWillReceiveProps(nextProps, this.props, this.context);
      }
    }
  }, {
    key: 'componentDidMount',
    value: function componentDidMount() {
      this._registerStores(this.props);
      if (this.props.onMount) this.props.onMount(this.props, this.context);
    }
  }, {
    key: 'componentWillUnmount',
    value: function componentWillUnmount() {
      this._destroySubscriptions();
      if (this.props.onWillUnmount) {
        this.props.onWillUnmount(this.props, this.context);
      }
    }
  }, {
    key: '_registerStores',
    value: function _registerStores(props) {
      var _this = this;

      var stores = props.stores;

      if (props.store) {
        this._addSubscription(props.store);
      } else if (props.stores) {
        if (Array.isArray(stores)) {
          stores.forEach(function (store) {
            return _this._addSubscription(store);
          });
        } else {
          Object.keys(stores).forEach(function (formatter) {
            _this._addSubscription(stores[formatter]);
          });
        }
      }
    }
  }, {
    key: '_destroySubscriptions',
    value: function _destroySubscriptions() {
      this.storeListeners.forEach(function (storeListener) {
        return storeListener();
      });
    }
  }, {
    key: '_addSubscription',
    value: function _addSubscription(getStore) {
      var store = typeof getStore === 'function' ? getStore(this.props).store : getStore;

      this.storeListeners.push(store.listen(this.altSetState.bind(this)));
    }
  }, {
    key: 'altSetState',
    value: function altSetState() {
      this.setState(reduceState(this.props));
    }
  }, {
    key: 'getProps',
    value: function getProps() {
      var flux = this.props.flux || this.context.flux;
      var transform = typeof this.props.transform === 'function' ? this.props.transform : id;
      return transform(object_assign(flux ? { flux: flux } : {}, this.state));
    }
  }, {
    key: 'shouldComponentUpdate',
    value: function shouldComponentUpdate(nextProps, nextState) {
      return this.props.shouldComponentUpdate ? this.props.shouldComponentUpdate(this.getProps(), nextProps, nextState) : true;
    }
  }, {
    key: 'render',
    value: function render() {
      var _this2 = this;

      var Node = 'div';
      var children = this.props.children;

      // Custom rendering function
      if (typeof this.props.render === 'function') {
        return this.props.render(this.getProps());
      } else if (this.props.component) {
        return React.createElement(this.props.component, this.getProps());
      }

      // Does not wrap child in a div if we don't have to.
      if (Array.isArray(children)) {
        return React.createElement(Node, null, children.map(function (child, i) {
          return React.cloneElement(child, object_assign({ key: i }, _this2.getProps()));
        }));
      } else if (children) {
        return React.cloneElement(children, this.getProps());
      } else {
        return React.createElement(Node, this.getProps());
      }
    }
  }]);

  return AltContainer;
})(React.Component);

Note: For those who do not want to use alt-container, I still recommend to use the « containers » structure because it helps coding cleanly, but I will show them how to do it without the « alt-container.js » file: the code will be commented at the end of each « container » file.

Immutable

Finally, we download the Immutable javascript file and put it in our vendor directory. The immutable documentation « Getting started » page gives you two links, I took this one: https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.6/immutable.min.js.

get https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.6/immutable.min.js file and put it there: vendor/assets/javascripts/immutable.min.js

1.6 – Create Controllers

An important advise that can save you a lot of troubles: always keep your controllers light! Export all you can in models or helpers so that you will be able to reuse the code if necessary. It follows one of Rails principle: to have a DRY code (see here for more information). It does not really apply to our case as we do very little here, but it is good to always keep that in mind.

home_controller

This controller manages our home page. As the home page is public, we want it to be rendered on server side, so that html is given back to the browser.

/app/controllers/home_controller.rb

class HomeController < ApplicationController

  def index
    render inline: "<%= react_component('HomeComponent', {}, {prerender: true}) %>", layout: 'application'
  end

end

private_controller

The private_controller handles all private pages. Therefore we ask it to render a ReactRouter component called « PrivateRoute » that will manage all the other private components.

/app/controllers/private_controller.rb

class PrivateController < ApplicationController
  before_action :authenticate_user!

  def index
    render inline: "<%= react_component('PrivateRoute') %>", layout: 'application'
  end

end

Notice the « before_action :authenticate_user! » at the top of the file: it is a devise method that checks if the user is authenticated, and if it is not the case it redirects to the authentication page.

video_games_controller

This controller manages the « video games » object with CRUD operations.

/app/controllers/video_games_controller.rb

class VideoGamesController < ApplicationController
  before_action :authenticate_user!

  def index
    respond_to do |format|
      format.json { render json: VideoGame.all.order(:id) }
    end
  end
  
  def show
    respond_to do |format|
      format.json { render json: VideoGame.find(params[:id]) }
    end
  end
  
  def update
    videogame = VideoGame.find(params[:id])
    videogame.update(name: params[:name])
      
    respond_to do |format|
      format.json { render json: videogame }
    end
  end

end

Here too we have the « before_action :authenticate_user! » method as we do not want non authenticated users to be able to call these actions.

1.7 – Update the routes

In our Rails routes file, we have 4 different parts: the usual « root » line, the « devise » routes (managed with « devise_for » command), one route for each controller (managed with « ressources » command), and finally 2 routes (managed with a « match » command).

/config/routes.rb

Rails.application.routes.draw do
  root 'home#index'
  
  devise_for :users

  resources :video_games, only: [:index, :show, :update]
  resources :home, only: :index
  resources :private, only: :index

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

The 3 first parts are quite classical, but let’s stop a little while on the last one. As we are using ReactRouter, most routing will be handled on client side, which means that your browser will know, for example, how to  go from http://<your server>/private/video-game-list to http://<your server>/private/video-game-best without asking the server. However, your server will not be able to understand these URLs! Thus, if you put the URL directly into your navigation bar, the request will be sent to the server who will return an error. Therefore, the last part enables to manage routes that are sent directly to the server (which is useful for all external links).

1.8 – Create and fill the database

Last thing to do on server side: prepare the database.

Create Video Games table

Lets create a Video Games table in our database:

rails generate migration CreateVideoGames

We fill it with this content:

/db/migrate/xxxxxxxxx_create_video_games.rb

class CreateVideoGames < ActiveRecord::Migration
  def change
    create_table :video_games do |t|
      t.string   :name
    end
    
    add_index :video_games, :id, unique: true
    add_index :video_games, :name
  end
end

And run:

rake db:migrate

Seed data into Video Games table

Now we are going to « seed » a few data in it.

Modify your seeds.rb file:

/db/seeds.rb

[{ id: 1, name: "Monkey Island"}, { id: 2, name: "Fallout2"}, { id: 3, name: "Prince of Persia"}, 
  { id: 4, name: "Flashback"}, { id: 5, name: "MaxPayne2"}, { id: 6, name: "Dune"}, 
  { id: 7, name: "Lemmings"}, { id: 8, name: "Ultima 7"}, { id: 9, name: "Sherman M4"}, 
  { id: 10, name: "Tetris"}].each do |videogame|
  VideoGame.create(videogame)
end

and run:

rake db:seed

 

We are done with the server part, let’s do (what I call) the Client configuration…

 

2 – Client configuration (React)

Modified files tree:

/
|
|_app
| |
| |_assets
| | |
| | |_javascripts
| | | |
| | | |_prerender
| | | | |
| | | | |_home-component.jsx.erb
| | | |
| | | |_actions
| | | | |
| | | | |_VideoGameActions.es6
| | | |
| | | |_sources
| | | | |
| | | | |_VideoGameSource.es6
| | | |
| | | |_stores
| | | | |
| | | | |_VideoGameStore.es6
| | | |
| | | |_containers
| | | | |
| | | | |_videogame-best-container.jsx.erb
| | | | |
| | | | |_videogame-list-container.jsx.erb
| | | | |
| | | | |_videogame-detail-container.jsx.erb
| | | |
| | | |_components
| | | | |
| | | | |_app-component.jsx.erb
| | | | |
| | | | |_videogame-best-component.jsx.erb
| | | | |
| | | | |_videogame-list-component.jsx.erb
| | | | |
| | | | |_videogame-detail-component.jsx.erb
| | | | |
| | | | |_videogame-selected-component.jsx.erb
| | | |
| | | |_applications.js
| | | |
| | | |_prerender.js
| | | |
| | | |_components.js
| | | |
| | | |_alt_init.js
| | | |
| | | |_addons_init.js
| | | |
| | | |_routes_init.js
| | | |
| | | |_routes.jsx
| | |
| | |_stylesheets
| | | |
| | | |_style.css
| | | |
| | | |_app.component.css
| | | |
| | | |_video-game-best.component.css
| | | |
| | | |_video-game-detail.component.css
| | |
| | |_images
| | | |
| | | |_my-image.png

2.1 – Javascripts configuration

2.1.1 – Initialization scripts

Update applications.js

In application.js, we add the react and immutable libraries, as well as the initialization files that we are going create right away. We also add the « routes » file, which will manage routing on client side, and the « components » file, which will include all react files.

/app/assets/javascripts/application.js

//= require jquery
//= require jquery_ujs

//= require react
//= require react_ujs

//= require immutable.min

//= require alt_init
//= require routes_init
//= require addons_init

//= require routes
//= require components

Create alt_init.js

In alt.js, we include alt and alt-container libraries, and initialize Alt.

/app/assets/javascripts/alt_init.js

//= require alt
//= require alt-container

var alt = new Alt();

Create addons_init.js

In this file we initialize PureRenderMixin, which will enable us to improve performances in React components.

/app/assets/javascripts/addons_int.js

var PureRenderMixin = React.addons.PureRenderMixin;

Configure routes_init.js

In routes_init.js, we initialize all variables that we will need to manage our routes.

/app/assets/javascripts/routes_init.js

//= require ReactRouter.min

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

Create components.js

Here, we include all React files, that is to say:

  • files that will be prerendered on server side (included in prerender.js file)
  • actions files
  • data sources files
  • stores files
  • containers files
  • components files

/app/assets/javascripts/components.js

//= require prerender
//= require_tree ./actions
//= require_tree ./sources
//= require_tree ./stores
//= require_tree ./containers
//= require_tree ./components

Create prerender.js

We only include all files that will be created in prerender directory. These files are the one that will be rendered on server side, to generate HTML, so that it is better crawled by search engine.

/app/assets/javascripts/prerender.js

/* 
This file is used in application.rb for prerendering react on server side.
It must also be included in components.js otherwise we get an error on client side.
*/
//= require_tree ./prerender

2.1.2 – Application scripts

Create routes.jsx

routes.jsx file contains the PrivateRoute component, which is, as you may recall, the component that is called in private_controller.rb (index action). It is also called in any case an URL starting with « private » is sent to the server (as described by « match private/*path » in routes.rb).

/app/assets/javascripts/routes.jsx

class PrivateRoute extends React.Component {
  render () {
    return (
        <Router history={browserHistory}>
          <Redirect from="/private" to="/private/video-game-best" />
          <Route path="/private" component={AppComponent}>
            <Route path="/private/video-game-list" component={VideoGameListContainer}></Route>
            <Route path="/private/video-game-best" component={VideoGameBestContainer}></Route>            
            <Route path="/private/video-game-detail/:id" component={VideoGameDetailContainer} />
          </Route>
        </Router>
      );
  }
}

A brief explanation:

  • in Router element, browserHistory enables the « browserHistory.push » function that tells the browser to go on a specific URL
  • the Redirect element redirects « /private » url to « /private/video-game-best ».
  • finally, the tree tells the browser to display the AppComponent at the top, and one of the following container below: VideoGameListContainer, VideoGameBestContainer, VideoGameDetailContainer.

prerender directory

Create /app/assets/javascripts/prerender directory, and create a home-component.jsx.erb file in it, with the following content:

/app/assets/javascripts/prerender/home-component.jsx.erb

class HomeComponent extends React.Component {
  render () {  
    return (
      <div>
        <h3>Home page</h3>
        <div>
          This is the home page!
          
          <nav>
            <a href='/private/video-game-best'>Video Games</a>
          </nav>
        </div>
      </div>      
    )
  }
}

This component is our home page. It contains only one link redirecting to the private part.

actions directory

Now, create /app/assets/javascripts/prerender directory, with a VideoGameActions.es6 file in it (the « es6 » extension enables the file to be translated by « sprockets-es6 » gem). Every time a React component needs to do an action on Video Games, it will go through this file.

/app/assets/javascripts/prerender/VideoGameActions.es6

class VideoGameActions {
  
  constructor() {
    //the following line enables to create actions that only 'return' parameters
    this.generateActions('updateVideoGames', 'updateVideoGameDetail', 'updateVideoGameName', 'selectVideoGame', 'updateVideoGameNameServer', 'videoGameFailed', 'videoGameLoading');
  }

}

var videoGameActions = alt.createActions(VideoGameActions);

Note: we can do a lot of operations in this file, for example fetching data from the server, but I prefer to do the fetching in the « data source » file. Therefore, here, we only return parameters to the store, so we can use the generateActions Alt function (it keeps us from creating a method for each action).

A brief description of each action:

  • updateVideoGames: this action updates the « video games list » and the « best video games list ». It is called when displaying the « video games list » page and the « best video games list »page.
  • updateVideoGameDetail: this action updates the video game details. It is called when displaying the video game details page.
  • updateVideoGameName: this action updates the video game name in the video game details page. It is called every time a key is typed in the video game name field, and it updates instantly the video game name displayed above.
  • selectVideoGame: this action displays a block with the selected video game below the list of video games. It is called when the user clicks on a video game in the video games list.
  • updateVideoGameNameServer: this actions updates the name of the video game in the database. It is called when the user clicks on the « update » button on the video game details page.
  • videoGameFailed: this action is called if there is a failure when fetching data (the list of video games, the detail of a video game) from the server or updating data (the name of the video game) on the server.
  • videoGameLoading: this action is called when fetching the video game detail from the server. It allows to display a « loading div » while the data is being fetch.

sources directory (data source)

Create /app/assets/javascripts/sources directory, and create a VideoGameSource.es6 file in it, with the following content:

/app/assets/javascripts/sources/VideoGameSource.es6

const VideoGameSource = {
  
  fetchVideoGames: {
    // should fetch has precedence over the value returned by local in determining whether remote should be called
    // in this particular example if the value is present locally it would return but still fire off the remote request (optional)
    //shouldFetch(state) {
    //  return true
    //}
    
    // this function checks in our local cache first
    // if the value is present it'll use that instead (optional).
    local(state) {
      return state.videogames.size > 0 ? state.videogames : null;
    },
    
    // remotely fetch something (required)
    remote (state) {
      return $.get('/video_games', function (results) { return results } );
    },

    // here we setup some actions to handle our response
    //loading: videoGameActions.loadingResults, // (optional)
    success: videoGameActions.updateVideoGames, // (required)
    error: videoGameActions.videoGameFailed, // (required)
  },
  
  fetchVideoGameDetail: {
    //shouldFetch(state) {
    //  return true
    //}

    local(state, id) {
      return state.videogame.get("id") == id ? state.videogame : null;
    },
    
    remote (state, id) {
      return $.get('/video_games/' + id, function (result) { return result } );
    },

    loading: videoGameActions.videoGameLoading, // (optional)
    success: videoGameActions.updateVideoGameDetail, // (required)
    error: videoGameActions.videoGameFailed, // (required)
  },
  
  updateVideoGameNameServer: {
    //shouldFetch(state) {
    //  return true
    //}

    //local(state) {
    //  return state.results[state.value] ? state.results : null;
    //},
    
    remote (state, id, newname) {
      return $.ajax( { url: '/video_games/' + id, type: 'put', data: {id: id, name: newname} } );
    },

    //loading: videoGameActions.loadingResults, // (optional)
    success: videoGameActions.updateVideoGameNameServer, // (required)
    error: videoGameActions.videoGameFailed, // (required)
  }
  
};

In this file, we do all the operations that are linked to the server, such as fetching data, or updating database data. See the alt Data Sources documentation for more information.

A brief description of each function:

  • fetchVideoGames: this function gets the list of video games from the server. It first checks the local cache to see if video games have already been retrieved, and if not, it sends an ajax request to the server to get the list. When the results arrive, it updates the video games list by calling « updateVideoGames » action.
  • fetchVideoGameDetail: this function gets the detail of video games from the server. It first checks the local cache to see if the details of this video game have already been retrieved, and if not, it sends an ajax request to the server to get them. When the result arrives , it updates the video games detail page by calling « updateVideoGameDetail » action. When the data is being fetch, the videoGameLoading action is called, which displays a « loading div ».
  • updateVideoGameNameServer: this function updates the name of the video game in the database. It sends an ajax request to the server.

stores directory

Create /app/assets/javascripts/stores directory, and create a VideoGameStore.es6 file in it, with the following content:

/app/assets/javascripts/sources/VideoGameStore.es6

class VideoGameStore {

  constructor () {
    this.videogames = new Immutable.List();
    this.videogame = new Immutable.Map();
    this.videogameSelected = new Immutable.Map();
    this.errorMessage = null;
    
    this.bindActions(videoGameActions);
    this.registerAsync(VideoGameSource);
  }
  
  onUpdateVideoGames (videogames) {
    this.videogames = this.videogames.mergeDeep(videogames);
  }
  
  onUpdateVideoGameDetail (videogame) {
    this.videogame = this.videogame.merge(videogame);
  }
  
  onVideoGameLoading () {
    this.videogame = new Immutable.Map();
  }
  
  onUpdateVideoGameName (name) {    
    this.videogame = this.videogame.set("name", name);
  }
  
  onSelectVideoGame (videogame) {
    this.videogameSelected = this.videogameSelected.merge(videogame);
  } 
  
  onVideoGameFailed (errorMessage) {
    this.errorMessage = errorMessage;
  }
  
  onUpdateVideoGameNameServer(videogame) {    
    //we want to re-initialize state so that we do not display old values when reloading components
    this.videogames = new Immutable.List();
    this.videogameSelected = new Immutable.Map();
    this.videogame = new Immutable.Map( { id: videogame.id, name: videogame.name } );
  }

}

var videoGameStore = alt.createStore(VideoGameStore, 'VideoGameStore');

Here, you can see that we initialize the store state with Immutable objects. Using immutable objects can greatly improve performance, facilitating the comparison between old and new states/props (in our case thanks to the PureRenderMixin addon).

The bindActions function in the constructor enables to link the actions to the store, and more precisely to the functions inside the store. The link work only if your store functions start with « on » + « the name of the action » (see createStore Alt documentation for more information).

The registerAsync function in the constructor enables to link the store to the data source. Thanks to this, you will be able to call data source functions directly from the store (ex: videoGameStore.fetchVideoGames(), videoGameStore.fetchVideoGameDetail(), …).

And now a brief description of each function:

  • onUpdateVideoGames: updates the video games list (if it has changed) and therefore refreshes « video games list » or « best video games list ». We use the « mergeDeep » immutable function because the video games list contains video game objects.
  • onUpdateVideoGameDetail: updates the video game detail (if it has changed) and therefore refreshes the video game detail page.
  • onVideoGameLoading: re-initializes the videogame object, which enables to display a « loading div » on the video game detail page.
  • onUpdateVideoGameName: updates the name of the video game and therefore displays the changes instantly on the video game detail page.
  • onSelectVideoGame: replaces videogameSelected with the new selected video game (if it has changed), and therefore refreshes the selected video game.
  • onVideoGameFailed: initializes errorMessage if there is an fetching data error, and displays the error message on the page.
  • onUpdateVideoGameNameServer: updates the videogame object with the updated video game, and re-initializes video games list and video game selected to display the new values.

containers directory

Create /app/assets/javascripts/containers directory. This directory is going to contain all the containers that are used in this application.

From AltContainer documentation:

The basic idea is that you have a container that wraps your component, the duty of this container component is to handle all the data fetching and communication with the stores, it then renders the corresponding children. The sub-components just render markup and are data agnostic thus making them highly reusable.

Create a videogame-best-container.jsx.erb file inside, which has for purpose to display the « best video games list » page, with the following content:

/app/assets/javascripts/containers/videogame-best-container.jsx.erb

class VideoGameBestContainer extends React.Component {
  componentDidMount () {
    videoGameStore.fetchVideoGames();
  }

  render () { 
    return (
      <AltContainer store={videoGameStore}>
        <VideoGameBestComponent />
      </AltContainer>
    )
  }
}


/*
//Without AltContainer
class VideoGameBestContainer extends React.Component {
  constructor () {
    super();
    this.state = videoGameStore.getState();
    this.onChange = this.onChange.bind(this) //allows the use of 'this' in onChange
  }
  
  onChange (state) {
    this.setState(state);
  }
  
  componentDidMount () {
    videoGameStore.listen(this.onChange);
    videoGameStore.fetchVideoGames();
  }
  
  componentWillUnmount () {
    videoGameStore.unlisten(this.onChange);
  }
  
  render () {
    return (
      <VideoGameBestComponent videogames={this.state.videogames} errorMessage={this.state.errorMessage} />
    )
  }
}
*/

Create a videogame-list-container.jsx.erb file inside, which has for purpose to display the « video game list » page, with the following content:

/app/assets/javascripts/containers/videogame-list-container.jsx.erb

class VideoGameListContainer extends React.Component {
  componentDidMount () {
    videoGameStore.fetchVideoGames();
    videoGameActions.selectVideoGame(null);
  }

  render () {
    return (
      <AltContainer store={videoGameStore}>
        <VideoGameListComponent />
        <VideoGameSelectedComponent />
      </AltContainer>
    )
  }
}


/*
//Without AltContainer
class VideoGameListContainer extends React.Component {
  constructor () {
    super();
    this.state = videoGameStore.getState();
    this.onChange = this.onChange.bind(this)
  }
  
  onChange (state) {
    this.setState(state);
  }
  
  componentDidMount () {
    videoGameStore.listen(this.onChange);
    videoGameStore.fetchVideoGames();
    videoGameActions.selectVideoGame(null);
  }
  
  componentWillUnmount () {
    videoGameStore.unlisten(this.onChange);
  }
  
  render () {
    return (
      <div>
        <VideoGameListComponent videogames={this.state.videogames} videogameSelected={this.state.videogameSelected} />
        <VideoGameSelectedComponent videogameSelected={this.state.videogameSelected} />
      </div>
    )
  }
}
*/

 

Create a videogame-detail-container.jsx.erb file inside, which has for purpose to display the « video game details » page with the following content:

/app/assets/javascripts/containers/videogame-detail-container.jsx.erb

class VideoGameDetailContainer extends React.Component {
  componentDidMount () {
    videoGameStore.fetchVideoGameDetail(this.props.params.id);
  }

  render () {
    return (
      <AltContainer store={videoGameStore}>
        <VideoGameDetailComponent />
      </AltContainer>
    )
  }
}


/*
//Without AltContainer
class VideoGameDetailContainer extends React.Component {
  constructor () {
    super();
    this.state = videoGameStore.getState();
    this.onChange = this.onChange.bind(this);
  }
  
  onChange (state) {
    this.setState(state);
  }
  
  componentDidMount () {
    videoGameStore.listen(this.onChange);
    videoGameStore.fetchVideoGameDetail(this.props.params.id);
  }
  
  componentWillUnmount () {
    videoGameStore.unlisten(this.onChange);
  }
  
  render () {
    return (
      <VideoGameDetailComponent videogame={this.state.videogame} />
    )
  }
}
*/

I like using the AltContainer library (AltContainer.js file) because it brings a lot of automatic mechanisms that keep the code clear, but if you do not want to use it, just take the commented part at the end of each file. In this case, do not forget to:

  • implement the « onChange » method,
  • bind it to « this » in the constructor,
  • « listen » to the store in the « componentDidMount » method
  • and « unlisten » the store in the « componentWillUnmount » method.

components directory

Create /app/assets/javascripts/components directory. This directory is going to contain all the components of the application.

Create an app-component.jsx.erb file inside, which has for purpose to display the navigation links and the component rendered by routes.jsx, with the following content:

/app/assets/javascripts/components/app-component.jsx.erb

//Stateless component
const AppComponent = (props) =>
  <div>
    <h1>Video Games</h1>
    <nav>
      <Link to='/private/video-game-best'>Best Video Games</Link>
      <Link to='/private/video-game-list'>Video Games List</Link>       
    </nav>
    {props.children}
  </div>;

Have a look at « props.children » which returns what the routes (routes.jsx) tells him to.

Create a videogame-best-component.jsx.erb file in the directory, which has for purpose to display the best video games, with the following content:

/app/assets/javascripts/components/videogame-best-component.jsx.erb

class VideoGameBestComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  
  handleDetailClick (id) {
    browserHistory.push('/private/video-game-detail/' + id);
  }

  render () {
    if (this.props.errorMessage) {
      return (
        <div>Something is wrong</div>
      );
    }
    else if (!this.props.videogames.size) {
      return (
        <div>Loading Best Video Games...</div>
      );
    }
    else {
      return (
        <div>
          <h3>Best Video Games</h3>
          <div className='grid grid-pad'>
            { this.props.videogames.slice(0,4).map(
              videogame => {
                return (
                  <div key={videogame.get("id")} className='col-1-4' >
                    <div className='module videogame' onClick={this.handleDetailClick.bind(this,videogame.get("id"))}><h4>{videogame.get("name")}</h4></div>
                  </div>    
                )
              }
            )}
          </div>
          <div className='image-div'>
            <img src="<%= asset_path('my-image.png') %>" alt="logo" />
          </div>
        </div>      
      )
    }
  }
}

Create a videogame-list-component.jsx.erb file in the directory, which has for purpose to display the full list of video games, with the following content:

/app/assets/javascripts/components/videogame-list-component.jsx.erb

class VideoGameListComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  
  handleClick (videogame) {
    videoGameActions.selectVideoGame(videogame);
  }
  
  render () {
    return (
      <div>
        <h2>My Video Games</h2>
        <ul className='videogames'>      
          { this.props.videogames.map(
            videogame => {
              var liClass = '';
              if(this.props.videogameSelected.size == 0 && (this.props.videogameSelected == videogame)) { liClass = 'selected' };
              return (
                <li key={videogame.get("id")} className={liClass} onClick={this.handleClick.bind(this,videogame)}>
                  <span className='badge'>{videogame.get("id")}</span> {videogame.get("name")}
                </li>
              )
            }
          )}
        </ul>
      </div>
    )
  }
}

Create a videogame-selected-component.jsx.erb file in the directory, which has for purpose to display the video game selected in the video game list component, with the following content:

/app/assets/javascripts/components/videogame-selected-component.jsx.erb

class VideoGameSelectedComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  handleDetailClick (id) {
    browserHistory.push('/private/video-game-detail/' + id);
  }
  
  render () {
    return (
      <div>
        { ( () => {
            if(this.props.videogameSelected.size > 0) {
              return (
                <div>
                  <h2>{this.props.videogameSelected.get("name").toUpperCase()} selected</h2>
                  <button onClick={this.handleDetailClick.bind(this,this.props.videogameSelected.get("id"))}>View Details</button>
                </div>
              )
            }
          } )()
        }
      </div>
    )
  }
}

Finally, create a videogame-detail-component.jsx.erb file in the directory, which has for purpose to display the details of the video game (selected by a click in the « best video games » list or the « full video games » list), with the following content:

/app/assets/javascripts/components/videogame-detail-component.jsx.erb

class VideoGameDetailComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  
  handleChange (e) {
    name = e.target.value;
    videoGameActions.updateVideoGameName(name);
  }
  
  goBackClick (id) {
    window.history.back();
  }
  
  updateVideoGameClick () {
    videoGameStore.updateVideoGameNameServer(this.props.videogame.get("id"),this.props.videogame.get("name"));
    alert("updated!");
  }
  
  render () {
    if (this.props.errorMessage) {
      return (
        <div>Something is wrong</div>
      );
    }
    else if (this.props.videogame.size == 0) {
      return (
        <div>Loading Video Game...</div>
      );
    }
    else {
      return (
        <div>
          <h2>{this.props.videogame.get("name")} details</h2>
          <div>
            <label>id: </label>{this.props.videogame.get("id")}
          </div>
          <div>
            <label>name: </label>
            <input type="text" value={this.props.videogame.get("name")} placeholder={this.props.videogame.get("name")} onChange={this.handleChange} />
          </div>
          <button onClick={this.goBackClick}>Back</button>
          <button onClick={this.updateVideoGameClick.bind(this)}>Update</button>
        </div>
      )
    }
  }
}

I will not detail each file but notice these few things:

  • this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); is written in each constructor. It enables to do a « shallow compare » between old props and new props, and it renders the component only if the props have changed. It is one of the major clue for performances. To use it, your component has to be « pure », that is to say it renders the same result given the same props and state (see React PureRenderMixin Documentation).
  • actions are made either by calling « videoGameActions » or « videoGameStore » in the case of data sources actions (such as updateVideoGameNameServer). The call of « videoGameStore » is possible thanks to registerAsync in VideoGameStore.
  • in loops, you have to use the « key » element, it is pure React (do not worry, if you forget you will have a nice error in your console)

2.2 – Stylesheets configuration

The explanation of CSS is out of this scope, so please, just copy the following files in the app/assets/stylesheets directory.

Create style.css

/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: #000; 
  font-family: Cambria, Georgia; 
}

button {
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
}

button:hover {
  background-color: #cfd8dc;
  cursor: pointer;
}

button:disabled {
  background-color: #eee;
  color: #aaa; 
  cursor: pointer;
}

/* everywhere else */
* { 
  font-family: Arial, Helvetica, sans-serif; 
}

Create app.component.css

/app/assets/stylesheets/app.component.css

h1 {
  font-size: 2em;
  color: #000;
  margin-bottom: 0;
}

h2 {
  font-size: 1.5em;
  margin-top: 0;
  padding-top: 0;
}

nav a {
  padding: 5px 10px;
  text-decoration: none;
  margin: 15px 10px 30px 0;
  display: inline-block;
  background-color: #227dd3;
  border-radius: 4px;
}

nav a:visited, a:link {
  color: #FFF;
}

nav a:hover {
  color: #E1E0E0;
  background-color: #208ef6;
}

nav a.router-link-active {
  color: #039be5;
}

Create video-game-best.component.css

/app/assets/stylesheets/video-game-best.component.css

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

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

.videogames li {
  cursor: pointer;
  position: relative;
  left: 0;
  background-color: #ea6422;
  margin: .5em;
  padding: .25em 0;
  height: 2em;
  border-radius: 15px;
}

.videogames li.selected:hover {
  background-color: #ffc33e !important;
  color: white;
}

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

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

.videogames .badge {
  display: inline-block;
  font-size: small;
  color: white;
  padding: 0.8em 0.7em 0 0.7em;
  background-color: #ffc33e;
  line-height: 1em;
  position: relative;
  left: -1px;
  top: -4px;
  height: 2.5em;
  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: 2em;
}

[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: #ffc33e;
    max-height: 120px;
    min-width: 120px;
    background-color: #ea6422;
    border-radius: 50px;
}

h4 {
  position: relative;
}

.module:hover {
  background-color: #ffc33e;
  cursor: pointer;
  color: #ea6422;
}

.image-div {
  text-align: center;
}

.image-div img {
  width: 6em;
}

.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;
    }
}

Create video-game-detail.component.css

/app/assets/stylesheets/video-game-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 {
  background-color: #ff992b;
  color: #FFF; 
  margin: 20px 5px 0 0;
  font-family: Arial;
  border: none;
  padding: 5px 10px;
}

button:hover {
  background-color: #ed8a1f;
}

button:disabled {
  color: #ccc; 
}

3 – Start the server and see the results

rails s

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

Do not forget to create a user on: http://your-server/users/sign_up

We are done! If you have any suggestion of how to improve this post, please, let me know.

Publicités

React and Immutable.js

A quick view on performances

Our application is already quite fast as React handles the virtual DOMs comparison and is very efficient for that (and also because the application is really small 😉 ).

But if you take a deeper look you will notice something strange: everytime you go on a page, the associated component is rendered 3 times. To check that, add a console.log below every render function in your app-component, dashboard-component, heroes-component and hero-detail-component:

class DashboardComponent extends React.Component {
  ...
  render () {
    console.log("render DashboardComponent");
    ...
  }
}
class HeroesComponent extends React.Component {
  ...
  render () {
    console.log("render HeroesComponent");
    ...
  }
}

And so on…

Now, open a Development console in your browser and wander around your different pages: you will see in your console that a same component is often rendered 3 times right away.

Why that? The first render is due to the fact that React calls the previous known version of the component and renders it. Then your action (like fetching heroes or fetching hero details) is called in the componentDidMount function, and if you remember we  do a dispatch() to the store  before fetching the data (in order to display a « loading » div): this renders the component again. Finally, we get the result of our fetching and we dispatch once more to the store, which renders the component a last time.

shouldComponentUpdate and PureRenderMixin

React explains how to avoid re-rendering all the time here: Advanced Performance. To summarize, it uses the shouldComponentUpdate function, that enables to « short circuit » the process. It means that you can use this function in your component to decide whether or not you want to render it.

Most of the time, you will want to render the component only if its state or its props have changed. Therefore, React offers a function that will do the comparison between the previous state and the new state, and between the previous props and the new props. This function is called: PureRenderMixin (documentation is here: PureRenderMixin), and it uses the shouldComponentUpdate function presented above. To use it, your component has to be « pure », which means from the React documentation that: « it renders the same result given the same props and state ». This notion is still vague for me 😉

Immutable.JS

Even though several people had talked to me about Immutable, it took me a little while to understand what was its purpose in a React application. It was only when dealing with shouldComponentUpdate and PureRenderMixin functions that I got the main idea: Immutable enables to compare deeply and easily two Javascript Objects. Indeed, when using the « = » comparison it will work with simple values such as integers or strings, but not with array, hash or complex data structures, because in these last cases « = » will compare the references of the objects and not the values inside them. When dealing with state and props, we want to compare the values and not the references, therefore Immutable takes all its meaning here!

Do not hesitate to have a look at the official documentation: Immutable.js.

Rails and React and Alt project updated with PureRenderMixin and Immutable

Let’s see how we can improve the performances of our Rails and React and Alt “Tour of Heroes” tutorial with what we have seen above.

1 – Rails configuration

1.1 – Get Immutable and put it in your vendor directory

First, we are going to download the Immutable javascript file and put it in our vendor directory (where we already have alt.js and ReactRouter.min.js). The immutable documentation « Getting started » page gives you two links, I took this one: https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.6/immutable.min.js.

get https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.6/immutable.min.js file and put it there: vendor/assets/javascripts/immutable.min.js

1.2 – Activate PureRenderMixin add-on

By default, the react-rails gem does not include React add-ons, but it allows you to do it by simply adding this line in your application.rb file: config.react.addons = true. Do not forget to restart your rails server after this operation.

In config/application.rb:

require File.expand_path('../boot', __FILE__)

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module ReactJSWithReactRails
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
    # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
    # config.time_zone = 'Central Time (US & Canada)'

    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
    # config.i18n.default_locale = :de

    # Do not swallow errors in after_commit/after_rollback callbacks.
    config.active_record.raise_in_transactional_callbacks = true
    
    #reactjs
    config.react.addons = true
  end
end

1.3 – Update heroes_controller.rb

We have a small modification to do here. So far, the update method of heroes_controller was not returning anything. We did not mind as we were rendering all components all the time. But now, as we want to render only when required, we will need to get a result from the update method, so that we will know that we have to refresh (ie render) the components.

Let’s add this code to the end of our heroes_controller.rb:

    hero = { id: params[:id], name: params[:name] }
    respond_to do |format|
      format.json { render json: hero }
    end

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)
    
    hero = { id: params[:id], name: params[:name] }
    respond_to do |format|
      format.json { render json: hero }
    end
  end

end

2 – Javascript configuration

2.1 – Update application.js

We have to include the immutable.min.js library so we add it in our applications.js file.

In app/assets/javascripts/application.js:

//= require jquery
//= require jquery_ujs

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

2.2 – Create addons_init.js

The only React add-on we will use as for now is PureRenderMixin. You can initialize it by creating an addons_init.js file in your javascripts assets directory with the following line:

In app/assets/javascripts/addons_init.js:

var PureRenderMixin = React.addons.PureRenderMixin;

2.3 – Update components.js

We must now include this new addons_init.js into our components.js file.

In app/assets/javascripts/components.js:

//= require routes
//= require alt_init
//= require addons_init
//= require_tree ./actions
//= require_tree ./stores
//= require_tree ./components

2.4 – Update HeroActions.es6

We are now going to update our HeroActions file. First, I suggest we remove the two dispatch() functions in fetchHeroes () and fetchHeroDetail (id) because we do not want to display the loading div anymore, we rather want to reduce the number of renders (we only render if state or props have changed). Second, we modify a little bit updateHeroNameServer (id,newname) to dispatch the result of our modified heroes_controller update method.

In app/assets/javascripts/actions/HeroActions.es6:

class HeroActions {
  
  constructor() {
    //the following line enables to create actions that only do a 'return' of the parameter
    this.generateActions('updateHeroes', 'updateHeroDetail', 'updateHeroName', 'selectHero');
  }
  
  fetchHeroes () {
     return (dispatch) => {
      $.get('/heroes', function (results) { return results } )
        .done((heroes) => { this.updateHeroes(heroes) } )
        .fail((errorMessage) => { this.heroesFailed(errorMessage) } );
    }
  }
  
  fetchHeroDetail (id) {
    return (dispatch) => {
      $.get('/heroes/' + id, function (result) { return result } )
        .done((hero) => { this.updateHeroDetail(hero) } )
        .fail((errorMessage) => { this.heroesFailed(errorMessage) } );
    }
  }
  
  updateHeroNameServer (id,newname) {
    return (dispatch) => {
      $.ajax( { url: '/heroes/' + id, type: 'put', data: {id: id, name: newname} } )
        .done((hero) => { dispatch(hero); } )
        .fail((errorMessage) => { this.heroesFailed(errorMessage) } );
    }
  }
  
  heroesFailed (errorMessage) {
    return errorMessage;
  }
  
}

var heroActions = alt.createActions(HeroActions);

2.5 – Update HeroStore.es6 (immutable)

In our HeroStore, we remove the onFetchHeroes () and the onFetchHeroDetail () functions as we do not need them anymore. They were needed to render the loading div when calling dispatch() but we removed them both in our HeroAction.

In the constructor we introduce Immutable by initializing our variables with Immutable objects instead of classical javascript objects.

Then we create an onUpdateHeroNameServer (hero) function that will be called when doing the dispatch(hero) in HeroAction. In this function we reinitialize the Immutable objects so that our components will know that they have to render.

In app/assets/javascripts/stores/HeroStore.es6:

class HeroStore {

  constructor () {
    this.heroes = new Immutable.List();
    this.hero = new Immutable.Map();
    this.heroSelected = new Immutable.Map();
    this.errorMessage = null;
    
    this.bindActions(heroActions);
  }
  
  onUpdateHeroes (heroes) {
    this.heroes = this.heroes.mergeDeep(heroes);
  }
  
  onUpdateHeroDetail (hero) {
    this.hero = this.hero.merge(hero);
  }
  
  onUpdateHeroName (name) {    
    this.hero = this.hero.set("name", name);
  }
  
  onSelectHero (hero) {
    this.heroSelected = this.heroSelected.merge(hero);
  } 
  
  onHeroesFailed (errorMessage) {
    this.errorMessage = errorMessage;
  }
  
  onUpdateHeroNameServer(hero) {    
    //we want to re-initialize state so that we do not display old values when reloading components
    this.heroes = new Immutable.List();
    this.heroSelected = new Immutable.Map();
    this.hero = new Immutable.Map( { id: hero.id, name: hero.name } );
  }

}

var heroStore = alt.createStore(HeroStore, 'HeroStore');

2.6 – Update your components (PureRenderMixin and immutable)

All we are going to do in our components is to add the PureRenderMixin function (which will compare previous state with new state and previous props with new props) and transform all calls to state objects by adding a get as we are now dealing with Immutable objects (example: replace hero.id with hero.get(« id »)).

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

class DashboardComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
    this.state = heroStore.getState();
    this.onChange = this.onChange.bind(this) //allows the use of 'this' in onChange
  }
  onChange(state) {
    this.setState(state);
  }
  componentDidMount () {
    heroStore.listen(this.onChange);
    heroActions.fetchHeroes();
  }
  componentWillUnmount() {
    heroStore.unlisten(this.onChange);
  }
  
  handleDetailClick (id) {
    browserHistory.push('/hero-detail/' + id);
  }

  render () {
    if (this.state.errorMessage) {
      return (
        <div>Something is wrong</div>
      );
    }
    else if (!this.state.heroes.size) {
      return (
        <div>Loading dashboard...</div>
      );
    }
    else {
      return (
        <div>
          <h3>Top Heroes</h3>
          <div className='grid grid-pad'>
            { this.state.heroes.slice(0,4).map(
              hero => {
                return (
                  <div key={hero.get("id")} className='col-1-4' >
                    <div className='module hero' onClick={this.handleDetailClick.bind(this,hero.get("id"))}><h4>{hero.get("name")}</h4></div>
                  </div>    
                )
              }
            )}
          </div>
        </div>      
      )
    }
  }
}

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

class HeroesComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
    this.state = heroStore.getState();
    this.onChange = this.onChange.bind(this) //allows the use of 'this' in onChange
  }
  onChange(state) {
    this.setState(state);
  }
  componentDidMount () {
    heroStore.listen(this.onChange);
    heroActions.fetchHeroes();
    heroActions.selectHero(null);
  }
  componentWillUnmount() {
    heroStore.unlisten(this.onChange);
  }

  handleClick (hero) {
    heroActions.selectHero(hero);
  }
  handleDetailClick (id) {
    browserHistory.push('/hero-detail/' + id);
  }
  
  render () {
    return (
    <div>
      <h2>My Heroes</h2>
      <ul className='heroes'>      
        { this.state.heroes.map(
          hero => {
            var liClass = '';
            if(this.state.heroSelected.size == 0 && (this.state.heroSelected == hero)) { liClass = 'selected' };
            return (
              <li key={hero.get("id")} className={liClass} onClick={this.handleClick.bind(this,hero)}>
                <span className='badge'>{hero.get("id")}</span> {hero.get("name")}
              </li>
            )
          }
        )}
      </ul>
      { ( () => {
          if(this.state.heroSelected.size > 0) {
            return (
              <div>
                <h2>{this.state.heroSelected.get("name").toUpperCase()} is my hero</h2>
                <button onClick={this.handleDetailClick.bind(this,this.state.heroSelected.get("id"))}>View Details</button>
              </div>
            )
          }
        } )()
      }
    </div>
    )
  }
}

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

class HeroDetailComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
    this.state = heroStore.getState();
    this.onChange = this.onChange.bind(this);
  }
  onChange(state) {
    this.setState(state);
  }
  componentDidMount () {
    heroStore.listen(this.onChange);
    heroActions.fetchHeroDetail(this.props.params.id);
  }
  componentWillUnmount() {
    heroStore.unlisten(this.onChange);
  }
  
  handleChange (e) {
    name = e.target.value;
    heroActions.updateHeroName(name);
  }
  goBackClick (id) {
    window.history.back();
  }
  updateHeroClick () {
    heroActions.updateHeroNameServer(this.state.hero.get("id"),this.state.hero.get("name"));
    alert("updated!");
  }
  
  render () {
    if (this.state.errorMessage) {
      return (
        <div>Something is wrong</div>
      );
    }
    else if (this.state.hero == null) {
      return (
        <div>Loading hero...</div>
      );
    }
    else {
      return (
        <div>
          <h2>{this.state.hero.get("name")} details</h2>
          <div>
            <label>id: </label>{this.state.hero.get("id")}
          </div>
          <div>
            <label>name: </label>
            <input type="text" value={this.state.hero.get("name")} placeholder={this.state.hero.get("name")} onChange={this.handleChange} />
          </div>
          <button onClick={this.goBackClick}>Back</button>
          <button onClick={this.updateHeroClick.bind(this)}>Update</button>
        </div>
      )
    }
  }
}

(optional) Transform app-component to a Stateless function

Our app-component is stateless so we can transform it as explained at the end of this page Reusable Components in React documentation.

In app/assets/javascripts/components/app-component.es6.jsx:

//Stateless component
const AppComponent = (props) =>
  <div>
    <h1>Tour of Heroes</h1>
    <nav>
       <Link to='/heroes-list'>Heroes</Link>
       <Link to='/dashboard'>Dashboard</Link>
    </nav>
    {props.children}
  </div>;

Conclusion

Now, if you add the console.log again right below the render function of your components, you will see that render is not called as often as before. It is only called when the state changes. It is obviously fast when switching from heroes list to dashboard, as the heroes list does not change.

Rails and React and Alt « Tour of Heroes » tutorial

Rails and React with Alt – ES6 – without npm

React could stand alone if you have a very simple application as shown above. However, with a big one, you will soon lose track of which components exchanges with which one, and what impacts may have such change in a component. You need an architecture that will structure a little bit your application. That is where FLUX arrives! You can get some information here: https://facebook.github.io/flux/.

Flux has multiple implementations but James K Nelson gives a nice comparison here:  Which Flux Implementation Should I Use With React? We are going to test Alt implementation, as it look easier than the others.

From now on, we are also going to use ES6. I am not a big fan of ES6, as I find it almost as verbose as plain Javascript (ie ES3 or ES5) but it brings a few interesting new features (classes, arrow functions, promises, …), and it is the langage that is more and more used in documentations you can find on the Web. If you want to keep using plain Javascript, just reuse the code above and follow the Plain Javascript chapter of Alt documentation.

Let’s start with a new rails project. I will go over all the steps again so that you do not have to read from the beginning…

1 – Rails configuration

1.1 – Create a new rails project

rails new reactjs_in_ES6_with_alt_project

1.2 – Update your Gemfile

In your Gemfile, uncomment therubyracer gem, and add both the react-rails and the sprockets-es6 gems to your Gemfile. The first gem enables you to use React in your project and the second one to transform ES6 to Plain Javascript properly. If you do not install the sprockets-es6 gem, you may still interpret some ES6, but you will encounter javascript error message such as: Class constructor […] cannot be invoked without ‘new’

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'

#Enables ES6 (otherwise you may encounter javascript errors with the current es6 transpiler)
gem 'sprockets-es6'

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 – Get Alt Javascript library and put it in vendor directory

The Alt guide says that you can get a browser build of alt here. So download this file and copy it in your vendor/assets/javascripts directory.

get https://cdn.rawgit.com/goatslacker/alt/master/dist/alt.js file and put it there: vendor/assets/javascripts/alt.js

1.5 – Update your routes

It is exactly the same thing as before.

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.6 – 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.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 in real life)

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 Alt and React one now…

2 – Alt and React configuration

Files tree:
 
(app/assets)
|
|_ (javascripts)
| |_ application.js
| |_ components.js
| |_ alt_init.js
| |_ routes.es6.jsx
| |
| |_ (actions)
| | |_ HeroActions.es6
| |
| |_ (stores)
| | |_ HeroStore.es6
| |
| |_ (components)
| | |_ app-component.es6.jsx
| | |_ dashboard-component.es6.jsx
| | |_ heroes-component.es6.jsx
| | |_ hero-detail-component.es6.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

We must require the React, ReactRouter.min, and Alt libraries. The React library allows us to use React components, the ReactRouter to manage the routing on the client side, and the Alt library to add some Flux architecture to our project.

In app/assets/javascripts/application.js:

//= require jquery
//= require jquery_ujs

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

2.1.2 – Update components.js

components.js contains all the files used by React. As we are using Flux architecture, we decompose our project in « actions », « stores » and components.

In app/assets/javascripts/components.js:

//= require routes
//= require alt_init
//= require_tree ./actions
//= require_tree ./stores
//= require_tree ./components

2.1.3 – Alt Configuration

  • Create « alt_init.js » file. This file initializes the Alt object that will be used to bring Flux architecture to the project.

In app/assets/javascripts/alt_init.js:

var alt = new Alt();
  • Create the « actions » directory and the « HeroActions.es6 » file inside (note that the extension is .es6 so that the sprockets-es6 gem can recognize it and transforms it).

In app/assets/javascripts/actions/HeroActions.es6:

class HeroActions {
  
  constructor() {
    //the following line enables to create actions that only do a 'return' of the parameter
    this.generateActions('updateHeroes', 'updateHeroDetail', 'updateHeroName', 'selectHero');
  }
  
  fetchHeroes () {
     return (dispatch) => {
      // we dispatch an event here so we render a specific div while loading (manage by onFetchHeroes in HeroStore).
      dispatch();
      $.get('/heroes', function (results) { return results } )
        .done((heroes) => { this.updateHeroes(heroes) } )
        .fail((errorMessage) => { this.heroesFailed(errorMessage) } );
    }
  }
  
  fetchHeroDetail (id) {
    return (dispatch) => {
      // we dispatch an event here so we render a specific div while loading (manage by onFetchHeroDetail in HeroStore).
      dispatch();
      $.get('/heroes/' + id, function (result) { return result } )
        .done((hero) => { this.updateHeroDetail(hero) } )
        .fail((errorMessage) => { this.heroesFailed(errorMessage) } );
    }
  }
  
  updateHeroNameServer (id,newname) {
    return (dispatch) => {
       $.ajax( { url: '/heroes/' + id, type: 'put', data: {id: id, name: newname} } )
        .done(() => { } )
        .fail((errorMessage) => { this.heroesFailed(errorMessage) } );
    }
  }
  
  heroesFailed (errorMessage) {
    return errorMessage;
  }
  
}

var heroActions = alt.createActions(HeroActions);

  • Create stores directory and create the HeroStore.es6 file.

In app/assets/javascripts/stores/HeroStore.es6:

class HeroStore {

  constructor () {
    this.heroes = [];
    this.hero = null;
    this.heroSelected = null;
    this.errorMessage = null;
    
    this.bindActions(heroActions);
  }
  
  onUpdateHeroes (heroes) {
    this.heroes = heroes;
  }
  
  onUpdateHeroDetail (hero) {
    this.hero = hero;
  }
  
  onUpdateHeroName (name) {
    this.hero.name = name;
  }
  
  onSelectHero (hero) {
    this.heroSelected = hero;
  }
  
  onFetchHeroes () {
    // reset the array while we're fetching new heroes so React can
    // be smart and render a specific div for us since the data is empty.
    this.heroes = [];
  }
  
  onFetchHeroDetail () {
    // reset the array while we're fetching new heroes so React can
    // be smart and render a specific div for us since the data is empty.
    this.hero = null;
  }
  
  onHeroesFailed (errorMessage) {
    this.errorMessage = errorMessage;
  }

}

var heroStore = alt.createStore(HeroStore, 'HeroStore');

2.1.4 – ReactRouter configuration

Create the « routes.es6.jsx » file. It is this file that will manage routing on client side.

In app/assets/javascripts/routes.es6.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

class StaticRoute extends React.Component {
  render () {
    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.5 – React configuration

Create the « components » directory and the following files: « app-component.es6.jsx », « dashboard-component.es6.jsx », « heroes-component.es6.jsx », « hero-detail-component.es6.jsx ».

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

class AppComponent extends React.Component {
  render () {
    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.es6.jsx:

class DashboardComponent extends React.Component {
  constructor () {
    super();
    this.state = heroStore.getState();
    this.onChange = this.onChange.bind(this); //allows the use of 'this' in onChange
  }
  onChange(state) {
    this.setState(state);
  }
  componentDidMount () {
    heroStore.listen(this.onChange);
    heroActions.fetchHeroes();
  }
  componentWillUnmount() {
    heroStore.unlisten(this.onChange);
  }
  
  handleDetailClick (id) {
    browserHistory.push('/hero-detail/' + id);
  }

  render () {
    if (this.state.errorMessage) {
      return (
        <div>Something is wrong</div>
      );
    }
    else if (!this.state.heroes.length) {
      return (
        <div>Loading dashboard...</div>
      );
    }
    else {
      return (
        <div>
          <h3>Top Heroes</h3>
          <div className='grid grid-pad'>          
            { this.state.heroes.slice(0,4).map(
              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>    
                )
              }
            )}
          </div>
        </div>      
      )
    }
  }
}

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

class HeroesComponent extends React.Component {
  constructor () {
    super();
    this.state = heroStore.getState();
    this.onChange = this.onChange.bind(this); //allows the use of 'this' in onChange
  }
  onChange(state) {
    this.setState(state);
  }
  componentDidMount () {
    heroStore.listen(this.onChange);
    heroActions.fetchHeroes();
    heroActions.selectHero(null);
  }
  componentWillUnmount() {
    heroStore.unlisten(this.onChange);
  }

  handleClick (hero) {
    heroActions.selectHero(hero);
  }
  handleDetailClick (id) {
    browserHistory.push('/hero-detail/' + id);
  }
  
  render () {
    return (
    <div>
      <h2>My Heroes</h2>
      <ul className='heroes'>      
        { this.state.heroes.map(
          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>
            )
          }
        )}
      </ul>
      { ( () => {
          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>
            )
          }
        } )()
      }
    </div>
    )
  }
}

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

class HeroDetailComponent extends React.Component {
  constructor () {
    super();
    this.state = heroStore.getState();
    this.onChange = this.onChange.bind(this);
    this.updateHeroClick = this.updateHeroClick.bind(this);
  }
  onChange(state) {
    this.setState(state);
  }
  componentDidMount () {
    heroStore.listen(this.onChange);
    heroActions.fetchHeroDetail(this.props.params.id);
  }
  componentWillUnmount() {
    heroStore.unlisten(this.onChange);
  }
  
  handleChange (e) {
    name = e.target.value;
    heroActions.updateHeroName(name);
  }
  goBackClick (id) {
    window.history.back();
  }
  updateHeroClick () {
    heroActions.updateHeroNameServer(this.state.hero.id,this.state.hero.name);
    alert("updated!");
  }
  
  render () {
    if (this.state.errorMessage) {
      return (
        <div>Something is wrong</div>
      );
    }
    else if (this.state.hero == null) {
      return (
        <div>Loading hero...</div>
      );
    }
    else {
      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.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 and Alt in ES6…

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…


Rails and Angular2 « Tour of Heroes » tutorial

Rails and Angular2 – Plain Javascript – without npm

Why using Plain Javascript whereas Google is suggesting TypeScript? Shame on me, but I did not succeed to make TypeScript work on my rails environment (I did not spend much time to be honest), and in fact I did not care as the only purpose was to understand main Angular2 concepts.

1 – Rails configuration

1.1 – Create a new rails project

rails new angular2_project

1.2 – Install nodejs or uncomment « therubyracer » in Gemfile

What?! « You said you did not use NodeJS! ». No, I did not use it as a web server, however we need a « Javascript runtime » to manage javascript. You can also use « therubyracer », you just have to uncomment it in your Gemfile.
I have not been able to really know which one was more efficient. On the Web you can find some people saying that nodejs is less memory consuming than therubyracer, but we can also find some people who benchmarked both and explain that therubyracer is a lot quicker…

In root:

apt-get install nodejs

1.3 – Update your routes

The three following lines is all you need, as the routing will be done directly in javascript by the client (powerful, isn’t it? Your web server is not required to go from one page to the other, the client can do it by itself). 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.4 – Update your application controller

You only have to render views/layouts/application.html.erb file in your index action.

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 "layouts/application"
  end
end

1.5 – Update the application view

Two things: add the three angular2 javascript libraries Rx.umd.js, angular2-polyfills.js, angular2-all.umd.dev.js, and replace the « yield » line with the <my-app> tag. Here I use beta.8 of Angular2, but you may have to update the version as the angular2 development progresses.

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

<!DOCTYPE html>
<html>
  <head>
    <base href="/">
  
    <title>Angular2</title>
    
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    
    <%# Angular2 %>
    <%= javascript_include_tag 'https://code.angularjs.org/2.0.0-beta.8/Rx.umd.js' %>
    <%= javascript_include_tag 'https://code.angularjs.org/2.0.0-beta.8/angular2-polyfills.js' %>
    <%= javascript_include_tag 'https://code.angularjs.org/2.0.0-beta.8/angular2-all.umd.dev.js' %>
        
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    
    <%= csrf_meta_tags %>
  </head>

  <body>
    <my-app></my-app>
  </body>
</html>

1.6 – Create a hero controller

The following file is very ugly, I know… It is obviously not what you are usually doing but I did not want to create a database and fill it with the data, so I created a file (/tmp/heroes_list) in which I stored the information. Of course in real life you use a database but it is out of this scope. The only purpose here was to show you how to get data from the client and how to give him data back (in fact the only interesting thing here is the « respond_to » block).

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.html { 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.html { 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.html { render nothing: true }
    end
  end

end

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

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… we are done for the rails part.

2 – Angular2 configuration

Files tree:
 
(app/assets)
|
|_ (javascripts)
| |_ application.js
| |_ main.js
| |
| |_ (application)
| | |_ app-component.js
| |
| |_ (directives)
| | |_ dashboard-component.js
| | |_ heroes-component.js
| | |_ hero-detail-component.js
| |
| |_ (services)
| | |_ heroes-service.js
| | |_ mock-heroes.js
|
|_ (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

In app/assets/javascripts/application.js:

//= require jquery
//= require jquery_ujs
//= require turbolinks

//for angular2
//= require main

2.1.2 – Create main.js

In app/assets/javascripts/main.js:

//= require application/app-component

document.addEventListener('DOMContentLoaded', function() {
    ng.platform.browser.bootstrap(MyApp);
});

2.1.3 – Create application directory and following file

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

//= require directives/dashboard-component
//= require directives/heroes-component
//= require directives/hero-detail-component
//= require services/heroes-service

var MyApp = ng.core.Component({
    selector: 'my-app',
    directives: [ng.router.ROUTER_DIRECTIVES],
    providers: [ng.router.ROUTER_PROVIDERS, ng.http.HTTP_PROVIDERS, HeroService], // ng.http.HTTP_PROVIDERS enables to use http and get the list from the server
    template: "<h1>{{title}}</h1> \
                <nav> \
                  <a [routerLink]=\"['Heroes']\">Heroes</a> \
                  <a [routerLink]=\"['Dashboard']\">Dashboard</a> \
                </nav> \
                <router-outlet></router-outlet>"
}).Class({
    constructor: [ ng.router.Router, function(router) {
            router.config([
                { path: '/dashboard', name: 'Dashboard', component: DashboardComponent, useAsDefault: true },
                { path: '/heroes-list', name: 'Heroes', component: HeroesComponent },                
                { path: '/detail/:id', name: 'HeroDetail', component: HeroDetailComponent }
            ]);
            
            this.title = 'Tour of Heroes';
    }]
});

2.1.4 – Create directives directory and following files

In app/assets/javascripts/directives/dashboard-component.js:

//= require services/heroes-service

var DashboardComponent = ng.core.Component({
    template: "<h3>Top Heroes</h3> \
                <div class='grid grid-pad'> \
                  <div *ngFor='#hero of heroes' (click)='gotoDetail(hero)' class='col-1-4' > \
                    <div class='module hero'> \
                      <h4>{{hero.name}}</h4> \
                    </div> \
                  </div> \
                </div>"
}).Class({
    constructor: [
      HeroService, ng.router.Router, ng.http.Http, function(heroService, router, http) {
        this._heroService = heroService;
        this._router = router;
        this._http = http;
      }
    ],
    ngOnInit: function() {
      //we get the list from mock-heroes.js
      //this._heroService.getHeroes.then(heroes => this.heroes = heroes.slice(1,5)) 
      
      //we get the list from the server
      this._heroService.getHeroes(this._http).subscribe(heroes => this.heroes = heroes.slice(1,5));
    },
    gotoDetail: function(hero) { this._router.navigate(['HeroDetail', { id: hero.id }]); }
});

In app/assets/javascripts/directives/heroes-component.js:

//= require services/heroes-service
//= require directives/hero-detail-component

var HeroesComponent = ng.core.Component({
    properties: ['model'],
    directives: [HeroDetailComponent],    
    template: "<h2>My Heroes</h2> \
                <ul class='heroes'> \
                  <li *ngFor='#hero of heroes_list' [class.selected]='hero === selectedHero' (click)='onSelect(hero)'> \
                     <span class='badge'>{{hero.id}}</span> {{hero.name}} \
                  </li> \
                </ul> \
                <div *ngIf='selectedHero'> \
                  <h2> \
                    {{selectedHero.name | uppercase}} is my hero \
                  </h2> \
                  <button (click)='gotoDetail()'>View Details</button> \
                </div>"
}).Class({
    constructor: [
      HeroService, ng.router.Router, ng.http.Http, function(heroService, router, http) {
        this._heroService = heroService;
        this._router = router;
        this._http = http;
      }
    ],
    ngOnInit: function() { this.getHeroes() },
    onSelect: function(hero) { this.selectedHero = hero },
    gotoDetail: function() { this._router.navigate(['HeroDetail', { id: this.selectedHero.id }]); },
    getHeroes: function() {
      //we get the list from mock-heroes.js
      //this._heroService.getHeroes.then(heroes => this.heroes_list = heroes);
      
      //we get the list from the server
      this._heroService.getHeroes(this._http).subscribe(heroes => this.heroes_list = heroes);
    }
});

In app/assets/javascripts/directives/hero-detail-component.js:

//= require services/heroes-service

var HeroDetailComponent = ng.core.Component({
    template: "<div *ngIf='hero'> \
                <h2>{{hero.name}} details</h2> \
                <div> \
                  <label>id: </label>{{hero.id}} \
                </div> \
                <div> \
                  <label>name: </label> \
                  <input [(ngModel)]='hero.name' placeholder='name'/> \
                </div> \
                <button (click)='goBack()'>Back</button> \
                <button (click)='updateHero(hero)'>Update</button> \
              </div>"
}).Class({
    constructor: [
      HeroService, ng.router.RouteParams, ng.http.Http, function(heroService, routeParams, http) {       
        this._heroService = heroService;
        this._routeParams = routeParams;
        this._http = http;
      }
    ],
    ngOnInit: function() { 
        id = +this._routeParams.get('id');
        
        //we get the list from mock-heroes.js
        //this._heroService.getHero(id).then(hero => this.hero = hero);
        
        //we get the list from the server
        this._heroService.getHero(this._http, id).subscribe(hero => this.hero = hero);
      },
    goBack: function() { window.history.back(); },
    updateHero: function(hero) { this._heroService.updateHero(this._http, hero.id, hero.name).subscribe(alert("updated!")); }
});

2.1.5 – Create services directory and following files

Important note: mock-heroes.js is not required, it is only if you want to stick to angular2 « Tour of Heroes » tutorial. If so uncomment first part  ‘we get the list from mock-heroes.js’ and comment second part ‘we get the list from server’.

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

//=require services/mock-heroes

var HeroService = function() {
  //we get the list from mock-heroes.js
  //this.getHeroes = Promise.resolve(Heroes);
  //this.getHero = function(id) { return Promise.resolve(Heroes).then(heroes => heroes.filter(hero => hero.id === id)[0]) };
  
  //we get the list from the server
  this.getHeroes = function(http) { return http.get('/heroes').map(res => res.json()) };
  this.getHero = function(http,id) { return http.get('/heroes/' + id).map(res => res.json()) };

  headers = new ng.http.Headers({ 'Content-Type': 'application/json' });
  options = new ng.http.RequestOptions({ headers: headers, method: "put" });
  this.updateHero = function(http,id,name) { return http.post('/heroes/' + id, JSON.stringify({ id, name }), options) };
};

In app/assets/javascripts/services/mock-heroes.js:

var Heroes = [
    { "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" }
  ];

2.2 – stylesheets configuration

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 angular2…