Rails is awesome, but modern web needs Loco-motive.
Rails is cool. But modern web needs Loco-motive.
Loco-Rails is a Rails engine from the technical point of view. Conceptually, it is a framework that works on top of Rails and consists of 2 parts: front-end and back-end. They are called Loco-JS and Loco-Rails, respectively.
This is how it can be visualized:
Loco Framework
|
|--- Loco-Rails (back-end part)
| |
| |--- Loco-Rails-Core (logical structure for JS / can be used separately with Loco-JS-Core)
|
|--- Loco-JS (front-end part)
|
|--- Loco-JS-Core (logical structure for JS / can be used separately)
|
|--- Loco-JS-Model (model part / can be used separately)
|
|--- other built-in parts of Loco-JS
Loco-JS-UI - connects models with UI elements (a separate library)
The following sections contain a more detailed description of its internals and API.
Let's assume, 2 users are navigating to a chat room page containing a list of chat members. This is a regular request-response application without technics like AJAX polling and WebSockets.
User A | User B |
---|---|
is joining a chat | --- |
--- | is joining a chat and is seeing User A who joined before |
is not seeing User B on the list of chat members | is seeing User A and User B as chat members |
is refreshing a page | is seeing User A and User B as chat members |
is seeing User A and User B as chat members | is seeing User A and User B as chat members |
So, you have to constantly refresh a page to get the current list of chat members. Or you need to provide a "live" functionality through AJAX or WebSockets. This requires a lot of unnecessary work/code for every element of your app like this. It should be much easier. And by easier, I mean ~1 significant line of code on the back-end and front-end side. Look for the Loco.emit
method on the back-end and subscribe
function on the front-end.
# app/controllers/user/rooms_controller.rb
class User
class RoomsController < UserController
def join
@hub.add_member current_user
Loco.emit @room, :member_joined, payload: {
room_id: @room.id,
member: {
id: current_user.id,
username: current_user.username
}
}
redirect_to user_room_url(@room)
end
end
end
Below is how the front-end version of Room
model can look like. If they share the same name, you can consider them as "connected". Otherwise, you need to specify the mapping. For all the options, look at the Loco-JS-Model documentation.
// frontend/js/models/Room.js
import { Models } from "loco-js";
class Room extends Models.Base {
static identity = "Room";
constructor(data) {
super(data);
}
}
export default Room;
Below is an example of a view that always renders an up-to-date list of chat members.
// frontend/js/views/user/rooms/Show.js
import { subscribe } from "loco-js";
import Room from "models/Room";
const memberJoined = member => {
const li = `<li id='user_${member.id}'>${member.username}</li>`;
document.getElementById("members").insertAdjacentHTML("beforeend", li);
};
const createReceivedMessage = roomId => {
return function(type, data) {
switch (type) {
case "Room member_joined":
if (data.room_id !== roomId) return;
memberJoined(data.member);
break;
}
};
};
export default {
render: roomId => {
subscribe({ to: Room, with: createReceivedMessage(roomId) });
},
renderMembers: members => {
for (const member of members) {
memberJoined(member);
}
}
};
This is just the tip of the iceberg. Look at Loco-JS and Loco-JS-Model documentation for more.
Loco-JS
Loco-Rails
To have Loco fully functional, you have to install both: back-end and front-end parts.
1️⃣ Loco-Rails works with Rails 5 onwards. You can add it to your Gemfile with:
gem 'loco-rails'
At the command prompt run:
$ bundle install
$ bin/rails generate loco:install
$ bin/rails db:migrate
2️⃣ Now it's time for the front-end part. Install it using npm (or yarn):
$ npm install loco-js --save
Familiarize yourself with the proper sections from the Loco-JS documentation on how to set up everything on the front-end side.
Look inside test/dummy/
to check a recommended setup with the webpack.
Loco-Rails and Loco-JS both use Semantic Versioning (MAJOR.MINOR.PATCH). It is required to keep the MAJOR version number the same between Loco-Rails and Loco-JS to maintain compatibility.
Some features may require an upgrade of MINOR version both for front-end and back-end parts. Check Changelogs and follow our Twitter to be notified.
1️⃣ loco:install
generator creates config/initializers/loco.rb
file (among other things) that holds configuration:
# frozen_string_literal: true
Loco.configure do |c|
c.silence_logger = false # false by default
c.notifications_size = 10 # 100 by default
c.app_name = "loco_#{Rails.env}" # your app's name (required for namespacing)
end
Where:
2️⃣ Browse all generated files and customize them according to the comments.
Use Loco.emit
or Loco.emit_to
module functions to send different types of messages.
Loco.emit
This module function emits a notification that informs recipients about an event that occurred on the given resource - e.g., the post was updated, the ticket was validated. If a WebSocket connection is established - a message is sent this way. If not - it's delivered via AJAX polling. Switching between an available method is done automatically.
Notifications are stored in the loco_notifications table in the database. One of the advantages of saving messages in a DB is that when the client loses connection with the server and restores it after a certain time - he will get all not received notifications 👏 unless you delete them before, of course.
Example:
receivers = [article.user, Admin, 'a54e1ef01cb9']
data = { foo: 'bar' }
Loco.emit(article, :confirmed, to: receivers, payload: data)
Arguments:
created_at == updated_at
updated_at > created_at
getWire().token = "<token>";
⚠️ If you wonder how to receive those notifications on the front-end side, look at the proper section of Loco-JS README.
When you emit a lot of notifications, you create a lot of records in the database. This way, your loco_notifications table may soon become very big. You must periodically delete old records. Below is a somewhat naive approach, but it works.
# frozen_string_literal: true
class GarbageCollectorJob < ApplicationJob
queue_as :default
after_perform do |job|
GarbageCollectorJob.set(wait_until: 1.hour.from_now).perform_later
end
def perform
Loco::Notification.where('created_at < ?', 1.hour.ago)
.find_each(&:destroy)
end
end
Loco.emit_to
This module function emits a direct message to recipients. Direct messages are sent only via a WebSocket connection and are not persisted in a DB.
⚠️ It utilizes ActionCable under the hood. You can use ActionCable in a standard way and Loco-way side by side. If you choose to stick to Loco only - you will never have to create ApplicationCable::Channel
s. Remember that Loco places ActiveJob
s into the :loco
queue.
If you want to send a message to a group of recipients, persist this group, and have an ability to add/remove members - an entity called Communication Hub may be handy.
You can treat it like a virtual room where you can add/remove members.
It works over WebSockets only with the emit_to
module function.
Loco
also provides hub management module functions such as add_hub
, get_hub
, del_hub
.
Details:
add_hub(name, members = [])
- creates and returns an instance of Loco::Hub
with a given name and members passed as a 2nd argument. In a typical use case - members should be an array of ActiveRecord instances.
get_hub(name)
- returns an instance of Loco::Hub
with a given name or nil
if a hub does not exist.
del_hub(name)
- destroys an instance of Loco::Hub
with a given name if it exists.
Important instance methods of Loco::Hub
:
name
members
- returns the hub's members. Members are stored in an informative, shortened form inside Redis. Be aware that this method performs calls to DB to fetch all members.raw_members
- returns hub's members in the shortened form as they are stored: "{class}:{id}"
add_member(member)
del_member(member)
include?(member)
destroy
Example:
hub1 = Hub.get('room_1')
admin = Admin.find(1)
data = { type: 'NEW_MESSAGE', message: 'Hi all!', author: 'system' }
Loco.emit_to([hub1, admin], data)
Arguments:
⚠️ Check out the proper section of Loco-JS README about receiving these messages on the front-end.
You can send messages over a WebSocket connection from the browser to the server using the emit
function. These messages can be received on the back-end by the Loco::NotificationCenter
class located in app/services/loco/notification_center.rb
loco:install
generator generates this class.
The received_message
instance method is called automatically for each message sent by front-end clients. 2 arguments are passed:
a hash with resources that can sign in to your app. You define them as loco_permissions
inside ApplicationCable::Connection
class. The keys of this hash are lowercase class names of signed-in resources, and the values are the instances themselves.
a hash with sent data
You can look at the working example here.
$ bundle install
$ docker-compose up
$ bin/rails db:create
$ bin/rails test
Capybara powers integration tests. Capybara is cool, but sometimes random tests fail unexpectedly. So before you assume that something is wrong, just run failed tests separately. It helps to keep the focus on the browser's window that runs integration tests on macOS.
Loco::Emitter
methods are available as Loco
's module_function
sLoco::Emitter
will be removed in Loco-Rails 7connection.rb
template has been modified
Breaking changes:
FLUSHDB
is recommendedreceived_signal
instance method of NotificationCenter
has been renamed to received_message
Loco.configure
initialization method requires a blocknpm
CoffeeScript
code, have been removedemit_to
- send messages to chosen recipients over WebSocket connection (an abstraction on the top of ActionCable
)
Communication Hubs - create virtual rooms, add members and emit_to
these hubs messages using WebSockets. All in 2 lines of code!
now emit
uses WebSocket connection by default (if available). But it can automatically switch to AJAX polling in case of unavailability. And all the notifications will be delivered, even those that were sent during this lack of a connection. 👏 If you use ActionCable
solely and you lost connection to the server, then all the messages that were sent in the meantime are gone 😭.
🔥 Only version 4 is under support and development.
Informations about all releases are published on Twitter
Loco-Rails is released under the MIT License.
Zbigniew Humeniuk from Art of Code