How easily integrate Rails' ActionCable into your Angular2/ionic2 application
Author:
Published: December 1, 2016
Time to read: 9 min
ActionСable is easily integrated WebSockets with the Rails application parts. It allows you to write real-time functionality with Ruby style according to the rest Rails application functionality, at the same time being productive and scalable. It is a full stack, including the client framework in JavaScript and server-side framework in Ruby. You get access to the models, written with Active Record or other ORM. So what’s the point of this post? I am gonna create a chat application on base Rails+Ionic2, note for angular2 it’s same. And tell how easily integrate Rails ActionCable into your Angular2/ionic2 application. The Ionic Framework enables the creation of cross-platform mobile applications with HTML, CSS, and JavaScript(Angular). Ionic 1 was built withAngular 1.*, and with Angular 2 recently released, the second major version of Ionic is also imminent. Ionic 2 has just hit Release Candidate stage, and if you are looking to build cross-platform apps quickly, and you already know Angular 2 or JavaScript, this guide will get you up to speed.
So, first of all, let’s initialize a new rails application:
rails new chat –api -d postgresql rails db:create
Well done, now we should add gemfiles. A Gemfile is a file we create which is used for describing gem dependencies for Ruby programs. Your Gemfile should always be in the root of your project directory, this is where Bundler expects it to be and it is the standard place for any package manager style files to live. It is useful to note that your Gemfile is evaluated as Ruby code. When it is evaluated by Bundler the context it is in allows us access to certain methods that we will use to explain our gem requirements.
- Gemfile
source 'https://rubygems.org'
ruby '2.3.1'
gem 'rails', '~> 5.0.0', '>= 5.0.0.1'
gem 'pg', '~> 0.18'
gem 'puma', '~> 3.0'
gem 'active_model_serializers'
gem 'decent_exposure', '~> 3.0.0'
gem 'kaminari'
gem 'redis'
gem 'rack-cors', require: 'rack/cors'
group :development, :test do
gem 'pry'
gem 'rspec-rails', '~> 3.5'
gem 'rspec-collection_matchers'
gem 'factory_girl_rails'
gem 'rubocop'
gem 'simplecov', require: false
gem 'database_cleaner'
gem 'shoulda-matchers'
gem 'faker'
end
group :development do
gem 'listen', '~> 3.0.5'
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
gem 'rubocop'
end
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
The next step of our development is including AbstractController.
- AbstractController::Helpers to application_controller.rb it needs for gem ‘decent_exposure’
controllers/application_controller.rb:
#frozen_string_literal: true
class ApplicationController < ActionController::API
include AbstractController::Helpers
end
After that to create V1::Base controller
- V1::Base controller
controllers/v1/base_controller.rb:
#frozen_string_literal: true
class V1::BaseController < ApplicationController
private
def render_api(object, status = :ok)
if object.respond_to?(:errors) && object.errors.present?
render json: { message: object.errors.full_messages.to_sentence }, status: :unprocessable_entity
else
render json: object, status: status
end
end
end
- Now create a new chat channel
channels/chat_channel.rb:
#frozen_string_literal: true
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from 'chat'
end
end
- Create a message model: rails g model message body:text sender:string
models/message.rb:
#frozen_string_literal: true
class Message < ApplicationRecord
validates :body, :sender, presence: true
after_create :broadcast
private
def broadcast
ActionCable.server.broadcast('chat', as_json.merge(action: 'CreateMessage'))
end
end
- Run rails db:migrate
- Create a messages controller:
controllers/v1/messages_controller.rb:
#frozen_string_literal: true
class V1::MessagesController < V1::BaseController
expose :message
expose :messages, -> { Message.order(created_at: :desc).page(params[:page]).per(15) }
def index
render_api(messages)
end
def create
message.save
render_api(message, :created)
end
private
def message_params
params.require(:message).permit(:body, :sender)
end
end
- Create a message serializer rails g serializer message
serializers/message_serializer.rb
#frozen_string_literal: true
class MessageSerializer < ActiveModel::Serializer
attributes :id, :body, :sender, :created_at, :updated_at
end
- Add routes
config/routes.rb:
#frozen_string_literal: true
Rails.application.routes.draw do
mount ActionCable.server => '/cable'
scope 'api' do
namespace :v1 do
resources :messages, only: [:index, :create]
end
end
end
- Initialize gem ‘rack-cors’
config/application.rb:
#frozen_string_literal: true
require_relative 'boot'
require 'rails'
#Pick the frameworks you want:
require 'active_model/railtie'
require 'active_job/railtie'
require 'active_record/railtie'
require 'action_controller/railtie'
require 'action_mailer/railtie'
require 'action_view/railtie'
require 'action_cable/engine'
# require "sprockets/railtie"
require 'rails/test_unit/railtie'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Ng2CableExample
class Application < Rails::Application
config.api_only = true
# Enable CORS
config.middleware.use Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :post, :options, :put, :patch, :delete]
end
end
end
end
Disable protection and allow requests from any origin:
config/initializers/action_cable.rb
#frozen_string_literal: true
Rails.application.config.action_cable.disable_request_forgery_protection = true
- Initialize rspec in rails app: rails generate rspec:install
spec/rails_helper.rb:
#frozen_string_literal: true
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
#Prevent database truncation if the environment is production
abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'spec_helper'
require 'rspec/rails'
require 'database_cleaner'
require 'simplecov'
require 'rspec/collection_matchers'
ActiveRecord::Migration.maintain_test_schema!
SimpleCov.start
RSpec.configure do |config|
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
config.fixture_path = "#{::Rails.root}/spec/fixtures"
config.include FactoryGirl::Syntax::Methods
config.include Requests::JsonHelpers, type: :controller
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
end
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
- Add json helper
spec/support/request_helpers.rb:
#frozen_string_literal: true
module Requests
module JsonHelpers
def json
@json ||= JSON.parse(response.body)
end
end
end
- Create a message factory
spec/factories/messages.rb:
#frozen_string_literal: true
FactoryGirl.define do
factory :message do
body { Faker::Lorem.sentence }
sender { Faker::Name.name }
end
end
- Create
spec/controller/v1/messages_controller_spec.rb:
#frozen_string_literal: true
require 'rails_helper'
RSpec.describe V1::MessagesController, type: :controller do
let!(:message) { create(:message) }
let(:message_params) { attributes_for(:message) }
describe '#index' do
it do
get :index
expect(json).not_to be_empty
end
end
describe '#create' do
it 'successfully create' do
post :create, params: { message: message_params }
expect(response).to be_success
end
it 'validation failed' do
post :create, params: { message: { body: '' } }
expect(response).to have_http_status(422)
end
end
end
- To make sure that it runs: rspec spec
So for back-end it’s all. Let’s start to build frontend
Ionic CLI and Cordova To create Ionic 2 projects, you’ll need to install the latest version of the CLI and Cordova. Before you do that, you’ll need a recent version of Node.js. Download the installer for Node.js 6 or greater and then proceed to install the Ionic CLI and Cordova for native app development: npm install -g ionic cordova
Once that’s done, create your first Ionic app: ionic start chat –v2 cd chat
Create chat page ionic g page chat
Add ng2-cable(https://github.com/victir/ng2-cable) to our project: npm install ng2-cable –save ionic serve
Add providers(Ng2Cable, Broadcaster)
src/app/app.module.ts:
import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { FormsModule } from '@angular/forms';
import { MyApp } from './app.component';
import { AboutPage } from '../pages/about/about';
import { ContactPage } from '../pages/contact/contact';
import { HomePage } from '../pages/home/home';
import { TabsPage } from '../pages/tabs/tabs';
import { ChatPage } from '../pages/chat/chat';
import { Ng2Cable, Broadcaster } from 'ng2-cable/js/index';
@NgModule({
declarations: [
MyApp,
AboutPage,
ContactPage,
HomePage,
TabsPage,
ChatPage
],
imports: [
IonicModule.forRoot(MyApp), FormsModule
],
bootstrap: [IonicApp],
entryComponents: [
MyApp,
AboutPage,
ContactPage,
HomePage,
TabsPage,
ChatPage
],
providers: [
{provide: ErrorHandler, useClass: IonicErrorHandler},
Ng2Cable,
Broadcaster
]
})
export class AppModule {}
Subscribe to channel
src/app/app.component.ts:
import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';
import { Ng2Cable } from 'ng2-cable/js/index';
import { TabsPage } from '../pages/tabs/tabs';
@Component({
template: '<ion-nav [root]="rootPage"></ion-nav>'
})
export class MyApp {
rootPage = TabsPage;
constructor(platform: Platform, private ng2cable: Ng2Cable) {
platform.ready().then(() => {
StatusBar.styleDefault();
Splashscreen.hide();
});
this.ng2cable.subscribe('https://ng2-cable-example.herokuapp.com/cable', 'ChatChannel');
//By default event name is 'channel name'. But you can pass from backend field { action: 'MyEventName'}
}
}
Set chat page to main tab
src/pages/tabs.ts:
import { Component } from '@angular/core';
import { ChatPage } from '../chat/chat';
import { AboutPage } from '../about/about';
import { ContactPage } from '../contact/contact';
@Component({
templateUrl: 'tabs.html'
})
export class TabsPage {
// this tells the tabs component which Pages
// should be each tab's root Page
tab1Root: any = ChatPage;
tab2Root: any = AboutPage;
tab3Root: any = ContactPage;
constructor() { }
}
Create messageService
src/services/message.ts:
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
@Injectable()
export class MessageService {
public apiUrl = 'https://ng2-cable-example.herokuapp.com';
constructor(private http: Http) {
}
query(page:any) {
return this.http.get(`${this.apiUrl}/api/v1/messages?page=${page}`).map(res => {
return res.json();
});
}
create(message:any) {
return this.http.post(`${this.apiUrl}/api/v1/messages`, message).map(res => {
return res.json();
});
}
}
Build Chat component
src/pages/chat.ts:
import { Component, ViewChild } from '@angular/core';
import { NavController, Content } from 'ionic-angular';
import { MessageService } from '../../services';
import { Broadcaster } from 'ng2-cable/js/index';
@Component({
selector: 'page-messages',
templateUrl: 'chat.html',
providers: [MessageService]
})
export class ChatPage {
@ViewChild(Content) content: Content;
public messages: any[] = [];
public page: number = 1;
public currentSender: any;
public message: any = {};
constructor(private messageService: MessageService,
private broadcaster: Broadcaster,
public navCtrl: NavController) {}
ionViewDidLoad() {
this.checkUser();
this.loadMessages();
this.content.scrollToBottom();
// init listener
this.broadcaster.on<string>('CreateMessage').subscribe(
message => {
this.messages.push(message);
this.content.scrollToBottom();
console.log(message);
}
);
}
loadMessages() {
this.messageService.query(this.page).subscribe(
(messages) => {
this.messages = messages.reverse().concat(this.messages);
}
);
}
createMessage() {
this.message['sender'] = this.currentSender;
this.messageService.create({message: this.message}).subscribe(
()=> {
this.message = {};
}
);
}
checkUser() {
if (this.getCurrentSender()) {
this.currentSender = this.getCurrentSender();
} else {
this.currentSender = prompt('Please enter your nickname', 'Active user');
if (this.currentSender) {
localStorage.setItem('currentSender', this.currentSender);
}
}
}
getCurrentSender() {
return localStorage.getItem('currentSender');
}
}
Build html template
src/pages/chat.html:
<ion-header>
<ion-navbar>
<ion-title>Chat</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<div class="nano has-scrollbar">
<div #scrollContainer class="nano-content pad-all" >
<ul class="list-unstyled media-block">
<li class="mar-btm" *ngFor="let message of messages">
<div class="media-left" [ngClass]="{'media-left': message.sender != currentSender, 'media-right': message.sender == currentSender}">
<img [src]="message.sender == currentSender ? 'assets/img/avatar2.png': 'assets/img/avatar1.png'" class="img-circle img-sm" alt="Profile Picture">
</div>
<div class="media-body pad-hor" [ngClass]="{'speech-right': message.sender == currentSender}">
<div class="speech">
<a href="#" class="media-heading"></a>
<p></p>
<p class="speech-time">
<i class="fa fa-clock-o fa-fw"></i>
</p>
</div>
</div>
</li>
</ul>
</div>
</div>
</ion-content>
<ion-footer>
<ion-toolbar>
<form name="sendMessageForm" (submit)="createMessage()">
<ion-grid>
<ion-row>
<ion-col width-90>
<ion-item>
<ion-label color="primary" floating>Enter your text</ion-label>
<ion-input [(ngModel)]="message.body" name="body"></ion-input>
</ion-item>
</ion-col>
<ion-col width-10>
<button [disabled]="!message.body" ion-button icon-right>Send</button>
</ion-col>
</ion-row>
</ion-grid>
</form>
</ion-toolbar>
</ion-footer>
If you have any questions, feedback or corrections for this article, please do get in touch with myself or the Active Bridge team. Angular2+rails Demo: https://ng2-cable-example.herokuapp.com
If you have a GitHub profile please leave a star on these repositories: ng2-cable: https://github.com/viktor-shmigol/ng2-cable Ionic2 example github: https://github.com/viktor-shmigol/ng2-cable-ionic2-example Rails + Angular2 example github: https://github.com/viktor-shmigol/ng2-cable-example
Author:
Published: December 1, 2016
Time to read: 9 min
Share: