Episodes

Resources

Download Source Code

Summary

# Terminal
rails g channel list

# layouts/application.html.erb
<%= action_cable_meta_tag %>

# channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
      logger.add_tags 'ActionCable', current_user.id
    end

    private

    def find_verified_user
      if verified_user = env['warden'].user
        verified_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

# channels/list_channel.rb
class ListChannel < ApplicationCable::Channel
  def subscribed
    list = current_user.lists.find_by(params[:list_id])
    stream_for list
  end

  def unsubscribed
    Rails.logger.info "Disconnected from server"
  end
end

# javascript/controllers/list_channel_controller.js
import { Controller} from "stimulus"
import consumer from '../channels/consumer'

export default class extends Controller {
  connect() {}

  initialize() {
    this.subscription()
  }

  disconnect() {
    this.subscription().unsubscribe()
    this.subscription().disconnected()
  }

  subscription() {
    if (this._subscription == undefined) {
      let _this = this
      this._subscription = consumer.subscriptions.create({ channel: "ListChannel", list_id: this.data.get("id") }, {
        connected() {
          // Called when the subscription is ready for use on the server
          console.log(`connected to ${_this.data.get("id")}`)
        },

        disconnected() {
          // Called when the subscription has been terminated by the server
          console.log(`disconnected from ${_this.data.get("id")}`)
        },

        received(data) {
          // Called when there's incoming data on the websocket for this channel
          console.log(`received from ${_this.data.get("id")}`)
          let type = data.type
          if (type == "create") {
            _this.createItem(data.column_id, data.item)
          } else if (type == "update") {
            _this.updateItem(data.column_id, data.column)
          } else if (type == "destroy") {
            _this.destroyItem(data.item_id)
          }

        }
      });
    }
    return this._subscription
  }

  createItem(column_id, item) {
    let column = document.getElementById(`column_${column_id}`)
    column.insertAdjacentHTML("afterBegin", item)
  }

  updateItem(column_id, items) {
    let column = document.getElementById(`column_${column_id}`)
    column.innerHTML = items
  }

  destroyItem(item_id) {
    let item = document.getElementById(`item_${item_id}`)
    item.remove()
  }

}

# items_controller.rb
  def create
    @item = @column.items.new(item_params)

    if @item.save
      ListChannel.broadcast_to(@list, type: 'create',
                                      item: ItemsController.render(@item, locals: { list: @list, column: @column }),
                                      column_id: @column.id)
      redirect_to @list, notice: 'Item was successfully created.'
    else
      render :new
    end
  end

  def update
    params[:positions].uniq.each_with_index do |id, index|
      @list.items.find(id).update(position: index + 1)
    end
    if @item.update(item_params)
      ListChannel.broadcast_to(@list, type: "update",
                                      column_id: @column.id,
                                      column: ItemsController.render(@column.items.order(position: :asc),
                                                locals: { list: @list, column: @column }))
      new_column_items = @list.columns.find(params[:item][:column_id]).items.order(position: :asc)
      ListChannel.broadcast_to(@list, type: "update",
                                      column_id: params[:item][:column_id],
                                      column: ItemsController.render(new_column_items,
                                                locals: { list: @list, column: @column }))
      respond_to do |format|
        format.html { redirect_to @list, notice: 'Item was successfully updated.' }
        format.json {}
      end
    else
      render :edit
    end
  end

  def destroy
    if @item.destroy
      ListChannel.broadcast_to(@list, type: "destroy", item_id: @item.id)
    end
    redirect_to @list, notice: 'Item was successfully destroyed.'
  end

# views/lists/show.html.erb
<%= content_tag :div, class: :row, data: { "list-channel-id": @list.id, controller: 'list-channel draggable' } do %>
  <% @list.columns.each do |column| %>
    <div class='col'>
      <div class='card'>
        <h5 class='card-title'><%= column.name %></h5>
        <div class='card-body'>
          <%= content_tag :ul, class: 'card-text dropzone', id: dom_id(column), data: { id: column.id, target: 'draggable.column' } do %>
            <%= render column.items, list: @list, column: column %>
          <% end %>
        </div>
        <%= link_to "New Item", [:new, @list, column, :item] %>
      </div>
    </div>
  <% end %>
<% end %>

# views/items/_item.html.erb
<%= content_tag :li, id: dom_id(item), data: { target: 'draggable.item',
                                               id: item.id,
                                               url: list_column_item_path(list, column, item) } do %>
  <div class="card">
    <div class="card-body">
      <p class="card-text">
        <%= item.content %>
      </p>
      <%= link_to 'delete', [list, column, item], method: :delete %>
    </div>
  </div>
<% end %>

# javascript/channels/list_channel.js
// import consumer from "./consumer"

// document.addEventListener("turbolinks:load", function () {
//   let list_room = document.getElementById('list_room')
//   if (list_room) {
//     consumer.subscriptions.create( { channel: "ListChannel", list_id: list_room.dataset.id }, {
//       connected() {
//         // Called when the subscription is ready for use on the server
//         console.log(`connected to ${list_room.dataset.id}`)
//       },

//       disconnected() {
//         // Called when the subscription has been terminated by the server
//         console.log(`disconnected from ${list_room.dataset.id}`)
//       },

//       received(data) {
//         // Called when there's incoming data on the websocket for this channel
//         console.log(`received from ${list_room.dataset.id}`)
//       }
//     });
//   }
// })

// document.addEventListener("turbolinks:before-visit", function () {
//   let list_room = document.getElementById('list_room')
//   if (list_room) {
//     consumer.list_channel.unsubscribe()
//     consumer.list_channel.disconnected()
//   }
// })