Skip to content

Instantly share code, notes, and snippets.

@srinidhiprabandham
Last active March 9, 2017 10:18
Show Gist options
  • Save srinidhiprabandham/91c56d8921b2c1293f92c24651a4c9a2 to your computer and use it in GitHub Desktop.
Save srinidhiprabandham/91c56d8921b2c1293f92c24651a4c9a2 to your computer and use it in GitHub Desktop.
Action Cable concepts and demo

Real Time Rails with Action Cable

Recent years have seen the rise of "the real-time web." Web apps we use every day rely on real-time features—things like new posts magically appearing at the top of your feeds or real time collaboration tools like Google Docs without the user having to refresh the page.*

Common Technologies used for real-time web.

  • Polling.

Polling or Ajax Polling is the process in which the client periodically requests data from the server.

  • Long Polling.

In Long Polling the client makes a request to the server to load a resource, If it has the necessary data then the server responds with the data otherwise the request is held on to until the necessary resource is available. Once the client gets the resource it restarts the process with the request.

  • Server-sent events(SSE).

SSE is a standard where the client requests server for a resource, this request is held on to and subsequent iterations with this client will happen over the created connection via events.

  • Web sockets.

Are protocol buit over the TCP/IP protocol. This allows the connection between the client and the server to be "bi-directional" (full-duplex).

  • Commet.

Is an umbrella term encompassing multiple techniques for achieving client-server interaction.

Why real-time Web ?

HTML operates using a request-response protocol. That is, a request is made to the server and the server responds with either the necessary data or an error.

Think of this as talking on a walkie-talkie only one of the participants get to talk at a time. And usually we need some means of communicating that one of the parties is "done", ("over").

Not only is this old fashioned but leads to a very bad user experience (UX).

Real-time web enables both (or all) of the participants to interact with one another just as they would in real life. Think of a Skype call. Where all the users can talk at once no restrictions !!

Introduction to Action Cable.

Action Cable implements WebSockets. And as the docs quotes

It's a full-stack offering that provides both a client-side JavaScript framework and a server-side Ruby framework. You have access to your full domain model written with Active Record or your ORM of choice.

Action Cable concepts.

Before actually implementing the code. Let us understand how Action Cable implements WebSockets (Opening a connection and maintaining it).

Action Cable can be configured to run as a stand-alone server or as a process in the main Application. (We will be using the second approach as it will be a lot easier to implement).

Action Cable utilises the Pub/Sub approach to communicate data between the server and client(s).

Building the actual application.

  • What we will be building

    To show case the power of web-sockets and Action Cable we will be building a collaborative board. Where all connected users can well draw. (I feel showing another chat application would not make sense). When done it will look like shown below.

  • Application Architecture

    ruby -v => 2.3.0 rails -v => 5.0.2

    With the above two dependencies installed. We are now ready to generate the application.

  • Create the application.

    rails new action-cable-demo

  • Setup the controller action that will show our board.

    cd action-cable-demo && rails g controller boards index

    This will create a controller called BoardsController with one action index and generate the necessary views.

  • Setting up the canvas and some colours.

    We will be using HTML's <canvas> as the base. Which will enable us to draw stuff on. Open up app/views/boards/index.html and replace the content with

<div id='main-content'>   
  <canvas id='board' width='400' height='400' style="position:absolute;top:1%;left:10%;border:2px solid;"></canvas>
  <div style="position:absolute;top:12%;left:43%;">Choose Color</div>  
  <div style="position:absolute;top:15%;left:45%;width:10px;height:10px;background:green;" id="green"></div>   
  <div style="position:absolute;top:15%;left:46%;width:10px;height:10px;background:blue;"  id="blue"></div>
  <div style="position:absolute;top:15%;left:47%;width:10px;height:10px;background:red;"   id="red"></div>   
  <div style="position:absolute;top:17%;left:45%;width:10px;height:10px;background:yellow;"id="yellow"></div>
  <div style="position:absolute;top:17%;left:46%;width:10px;height:10px;background:orange;"id="orange"></div>
  <div style="position:absolute;top:17%;left:47%;width:10px;height:10px;background:black;" id="black"></div>   
  <div style="position:absolute;top:20%;left:43%;">Eraser</div>
  <div style="position:absolute;top:24%;left:45%;width:15px;height:15px;background:white;border:2px solid;" id="white"></div>
  <img id="canvasimg" style="position:absolute;top:10%;left:52%;" style="display:none;">
</div>
<input type="button" value="clear" id="clr" size="23" style="position:absolute;top:65%;left:10%;">
  • Start up the webserver from the root of your project folder with rails s and visit localhost:3000 in your borwser and you should see a board with some colours.

    Board

  • Now Lets get the canvas working.

    open app/assets/javascripts/boards.coffee and paste in this code.

     $(document).ready ->
       canvas = undefined
       window.ctx = undefined
       w = undefined
       h = undefined
       flag = false
       prevX = 0
       currX = 0
       prevY = 0
       currY = 0
       dot_flag = false
       x = 'black' #default color
       y = 3 #Size of the line.
    
       $("#green").on 'click', -> color(this)
       $("#blue").on 'click', -> color(this)
       $("#red").on 'click', -> color(this)
       $("#yellow").on 'click', -> color(this)
       $("#orange").on 'click', -> color(this)
       $("#black").on 'click', -> color(this)
       $("#white").on 'click', -> color(this)
    
       $("#clr").on 'click', -> 
         erase()
    
       init = ->
         canvas = $("#board")[0]
         window.ctx = canvas.getContext('2d')
         w = canvas.width
         h = canvas.height
         canvas.addEventListener 'mousemove', ((e) ->
           findxy 'move', e
           return
         ), false
         canvas.addEventListener 'mousedown', ((e) ->
           findxy 'down', e
           return
         ), false
         canvas.addEventListener 'mouseup', ((e) ->
           findxy 'up', e
           return
         ), false
         canvas.addEventListener 'mouseout', ((e) ->
           findxy 'out', e
           return
         ), false
         return
       # Set the sellected color 
       color = (obj) ->
         x = obj.id
         if x == 'white'
           y = 14
         else
           y = 3
       
       draw = ->
         ctx.beginPath();
         ctx.moveTo(prevX, prevY);
         ctx.lineTo(currX, currY);
         ctx.strokeStyle = x;
         ctx.lineWidth = y;
         ctx.stroke();
         ctx.closePath();
    
       erase = ->
         m = confirm("Want to clear")
         if m
           ctx.clearRect(0, 0, w, h)
           $("#canvasimg")[0].style.display = "none"
    
       findxy = (res, e) ->
         if res == 'down'
           prevX = currX
           prevY = currY
           currX = e.clientX - (canvas.offsetLeft)
           currY = e.clientY - (canvas.offsetTop)
           flag = true
           dot_flag = true
           if dot_flag
             window.ctx.beginPath()
             window.ctx.fillStyle = x
             window.ctx.fillRect currX, currY, 2, 2
             window.ctx.closePath()
             dot_flag = false
         if res == 'up' or res == 'out'
           flag = false
         if res == 'move'
           if flag
             prevX = currX
             prevY = currY
             currX = e.clientX - (canvas.offsetLeft)
             currY = e.clientY - (canvas.offsetTop)
             draw()
    
       #Initialize the board
       init()
    

    With this if we refresh the page we should be able to draw on the canvas.

  • Time to setup action cable.

    With this all subscribers get to see what we are drawing and collobrate with us.

    Open config/routes.rb and add

    mount ActionCable.server => '/cable'

    Open app/views/layouts/application.html.erb and add (to the head section)

    <%= action_cable_meta_tag %>

  • Creating Channels.

    rails gives us a generator to generate channels just like with controllers, models and migrations

    rails g channel board draw clear [Think of this as generating a controller with actions]

    Running this will generate two files; a javascript file located in app/assets/javascripts/channels/board.coffee and a ruby file located in app/channels/board_channel.rb.

    Let's analyze the ruby file first:

    class BoardChannel < ApplicationCable::Channel
      def subscribed
        # stream_from "some_channel"
      end
    
      def unsubscribed
        # Any cleanup needed when channel is unsubscribed
      end
    
      def draw
      end
    
      def clear
      end
    end
    

    The subscribed method is a default that’s called when a client connects to the channel, and it’s usually used to subscribe the client to listen to changes. The draw and clear actions are custom actions that we created when we ran the generator. It will be used to receive data from its client-side representation.

    App.board = App.cable.subscriptions.create "BoardChannel",
      connected: ->
        # Called when the subscription is ready for use on the server
    
      disconnected: ->
        # Called when the subscription has been terminated by the server
    
      received: (data) ->
        # Called when there's incoming data on the websocket for this channel
    
      draw: ->
        @perform 'draw'
    
      clear: ->
        @perform 'clear'
    

    The first line of the above code App.board = App.cable.subscriptions.create "BoardChannel" is where the magic happens. This initializes the client and subscribes to the server via a connection called "board_channel"

  • With the framework setup let us now change the draw and clear method such that it actually transmits the events over the socket and updates all subscribers.

    Modify app/assets/javascripts/channels/board.coffee 's receive, draw and clear methods to:

App.board = App.cable.subscriptions.create "BoardChannel",
    connected: ->
      # Called when the subscription is ready for use on the server

    disconnected: ->
      # Called when the subscription has been terminated by the server

    received: (data) ->
      data = data.data
      if data.message == "true"
        m = confirm('Are you sure you want to clear ?')
        if m
          window.ctx.clearRect 0, 0, data.w, data.h
          $("#canvasimg")[0].style.display = 'none'
      else
        window.ctx.beginPath()
        window.ctx.moveTo data.prevX, data.prevY
        window.ctx.lineTo data.currX, data.currY
        window.ctx.strokeStyle = data.strokeStyle
        window.ctx.lineWidth = data.lineWidth
        window.ctx.stroke()
        window.ctx.closePath()

    draw: (data) ->
      @perform 'draw', data: data

    clear: (data) ->
      @perform 'clear', data: data

And the erase and draw functions in our app/javascripts/boards.coffee to be

  draw = ->
  App.board.draw({
      currX: currX,
      currY: currY,
      prevX: prevX,
      prevY: prevY,
      strokeStyle: x,
      lineWidth: y,
    })

  erase = ->
    App.board.clear({message: "true", w: w, h: h})

This makes the publising part of our pub/sub complete. Let us handel the data that we are getting and boardcast it to all subscribers.

Edit app/channels/board_channel.rb to receive the data sent from the server and publish it to all Subscribes.

class BoardChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def draw(data)
    ActionCable.server.broadcast "room_channel", data: data['data']
  end

  def clear(data)
    ActionCable.server.broadcast "room_channel", data: data['data']
  end
end

NOTE The use of ActionCable.server.broadcast in the channels ! This was done to show that the boardcast method is accessible across the rails application. So if need be we could be broadcasting events from say a long running background job, to notify that it is complete.

  • Testing it out:

    Open up a new window and navigate to http://localhost:3000 Drawing content on one should reflect on the other window as well.

  • Deploying the application to Heroku.

    1. Login to Heroku.
    2. Create a new application
    3. add heroku origin.
    4. Replace sqlite3 in Gemfile to pg.
    5. update config/database.yml to include pg connection parameters.
    6. Since we are not using redis to handle our socket connections we have to make changes to config/cable.yml and change adapter: async

That about sums up the basics of Real-Time Web and how rails and action cable makes it trivial to create real-time applications. Hope you have enjoyed this tutorial, you can visit a working version of this application on Heroku

Some of the other awesome blogs that go into details about Action Cable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment