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…


Publicités

Une réflexion sur “Rails and Angular2 « Tour of Heroes » tutorial

  1. Je trouve que Angular2 est une technologie très interessante
    – Utilisation de concept des composants
    – Il supporte plusieurs languages comme TypeScript
    – Utilisation de l’orienté objet dans un script
    – La performance
    – La grande communauté
    – Il est facilement testable et maintenable
    – Aussi l’utilisation de l’injection des dépendences
    Vous pouvez visiter mon site pour angular2
    http://www.angular-tuto.com

    J'aime

Laisser un commentaire

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

Logo WordPress.com

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

Image Twitter

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

Photo Facebook

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

Photo Google+

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

Connexion à %s