Description
Loco-Rails is a Rails engine from the technical point of view. Conceptually, it is a framework that works on a top of Rails and consists of 2 parts: front-end and back-end. They are called Loco-JS and Loco-Rails, respectively. Both parts cooperate.
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)
Loco-Rails repository contains a decent-size demo app under test/dummy which shows different implementation strategies using Loco for many common problems.
And that is just the top of an iceberg. Both projects (Loco-Rails and Loco-JS) have README pages with more detailed description of all features.
Greetings from Cracow, Poland
Zbigniew Humeniuk
Loco-Rails alternatives and similar gems
Based on the "Frameworks" category.
Alternatively, view Loco-Rails alternatives based on common mentions on social networks and blogs.
-
minitest
minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking. -
Spork
A DRb server for testing frameworks (RSpec / Cucumber currently) that forks before each run to ensure a clean testing state. -
Konacha
Test your Rails application's JavaScript with the mocha test framework and chai assertion library -
RR
RR is a test double framework that features a rich selection of double techniques and a terse syntax. โบ -
RSpecTracer
RSpec Tracer is a specs dependency analyzer, flaky tests detector, tests accelerator, and coverage reporter tool for RSpec. It maintains a list of files for each test, enabling itself to skip tests in the subsequent runs if none of the dependent files are changed. It uses Ruby's built-in coverage library to keep track of the coverage for each test.
Scout Monitoring - Performance metrics and, now, Logs Management Monitoring with Scout Monitoring
* Code Quality Rankings and insights are calculated and provided by Lumnify.
They vary from L1 to L5 with "L5" being the highest.
Do you think we are missing an alternative of Loco-Rails or a related project?
README
Rails is cool. But modern web needs Loco-motive.
๐ง What is Loco-Rails?
Loco-Rails is a Rails engine from the technical point of view. Conceptually, it is a framework that works on a top of Rails and consists of 2 parts: front-end and back-end. They are called Loco-JS and Loco-Rails, respectively. Both parts cooperate.
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.
โ But how is Loco supposed to help?
- by providing a logical structure for a JavaScript code along with a base class for controllers. You exactly know where to start looking for a JavaScript code that runs a current page (Loco-JS-Core)
- you have models that protect API endpoints from sending invalid data. They also facilitate fetching objects of a given type from the server (Loco-JS-Model)
- you can easily assign a model to a form enriching this form with fields' validation (Loco-JS-UI)
- you can subscribe to a model or a collection of models on the front-end by passing a function. Front-end and back-end models can be connected. This function is called when a notification for a given model is sent on the server-side. (Loco)
- it allows sending messages over WebSockets in both directions with just a single line of code on each side (Loco)
- it respects permissions. You can filter out sent messages if a sender is not signed in as a given resource, for example, a given admin or user) (Loco)
๐จ Other, more specific problems that Loco solves
Current state everywhere
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 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
emit @room, :member_joined, data: {
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.
๐ค Dependencies
Loco-JS
- ๐ no strict external dependencies. ๐ But check out its "soft dependencies"โ๏ธ
Loco-Rails
- Loco-Rails-Core - Rails plugin that has been extracted from Loco-Rails so it could be used as a stand-alone lib. It provides a logical structure for JavaScript code that corresponds with Rails` controllers and their actions that handle a given request. Loco-Rails-Core requires Loco-JS-Core to work.
- modern Ruby (tested on >= 2.3.0)
- Rails 5
- Redis and redis gem - Loco-Rails stores information about WebSocket connections in Redis. It is not required if you don't want to use ActionCable, or you use Rails in the development environment. In the last case - Loco-Rails uses an in-process data store or Redis (if available).
๐ฅ Installation
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.
โ๏ธ Configuration
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:
- notifications_size - max number of notifications returned from the server at once
- app_name - used as key's prefix to store info about current WebSocket connections in Redis or memory
In a production environment - it's better not to store all the data Loco-Rails needs to work in memory. A better option is Redis, which is shared between app servers.
If Loco-Rails discovers Redis instance under Redis.current
, it will use it. Except that, you can specify Redis instance directly using redis_instance: Redis.new(your_config)
.
2๏ธโฃ Browse all generated files and customize them according to the comments.
๐ฎ Usage
Emitting messages ๐ก
- include
Loco::Emitter
module inside any class - use
emit
oremit_to
methods provided by this module to send a different type of messages
If you want to use a low-level
interface without including a module, look inside the source code of Loco::Emitter
.
emit
This method 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:
include Loco::Emitter
receivers = [article.user, Admin, 'a54e1ef01cb9']
data = { foo: 'bar' }
emit(article, :confirmed, to: receivers, data: data)
Arguments:
- a resource this event relates to
- a name of an event that occurred (Symbol/String). Default values are:
- :created - when
created_at == updated_at
- :updated - when
updated_at > created_at
- :created - when
- a hash with relevant keys:
- :to - message's recipients. It can be a single object or an array of objects. Instances of models, their classes, and strings are accepted. If a recipient is a class, then given notification is addressed to all instances of this class currently signed in. If a receiver is a string (token), clients will receive notifications who have subscribed to this token on the front-end side. They can do this by invoking this code:
getWire().token = "<token>";
- :data - additional data, serialized to JSON, transmitted along with the notification
- :to - message's recipients. It can be a single object or an array of objects. Instances of models, their classes, and strings are accepted. If a recipient is a class, then given notification is addressed to all instances of this class currently signed in. If a receiver is a string (token), clients will receive notifications who have subscribed to this token on the front-end side. They can do this by invoking this code:
โ ๏ธ If you wonder how to receive those notifications on the front-end side, look at the proper section of Loco-JS README.
Garbage collection
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
emit_to
This method emits a direct message to recipients. Direct messages are sent only via 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.
Communication Hub
You can treat it like a virtual room where you can add/remove members.
It works over WebSockets only with the emit_to
method.
Loco::Emitter
module also includes methods for managing hubs such as add_hub
, get_hub
, del_hub
.
Details:
add_hub(name, members = [])
- creates and returns an instance ofLoco::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 ofLoco::Hub
with a given name ornil
if a hub does not exist.del_hub(name)
- destroys an instance ofLoco::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 / in-process storage. 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:
include Loco::Emitter
hub1 = Hub.get('room_1')
admin = Admin.find(1)
data = { type: 'NEW_MESSAGE', message: 'Hi all!', author: 'system' }
emit_to([hub1, admin], data)
Arguments:
- recipients - a single object or an array of objects. ActiveRecord instances and Communication Hubs are allowed.
- data - a hash serialized to JSON during sending.
โ ๏ธ Check out the proper section of Loco-JS README about receiving these messages on the front-end.
๐ Receiving notifications sent over WebSockets
Notification Center ๐ฐ
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
insideApplicationCable::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.
๐ฉ๐ฝโ๐ฌ Tests
$ 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.
๐ Changelog
Major releases ๐
4.1 (2020-07-27)
- Loco-JS-Core has been updated to v0.2
4.0 (2020-07-26)
- Breaking changes:
received_signal
instance method ofNotificationCenter
has been renamed toreceived_message
Loco.configure
initialization method requires a block
3.0
- Loco-JS and Loco-JS-Model are no longer distributed with Loco-Rails and have to be installed using
npm
- all generators, generating legacy
CoffeeScript
code, have been removed
2.2
- Loco-JS and Loco-JS-Model have been updated
2.0
- changes in the front-end architecture - Loco-JS-Model has been extracted from Loco-JS
1.5
- Loco-JS dropped the dependency on jQuery. So it officially has no dependencies ๐
1.4
- Ability to specify Redis instance through configuration
1.3
emit_to
- send messages to chosen recipients over WebSocket connection (an abstraction on the top ofActionCable
)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 useActionCable
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
๐ License
Loco-Rails is released under the MIT License.
๐จโ๐ญ Author
Zbigniew Humeniuk from Art of Code
*Note that all licence references and agreements mentioned in the Loco-Rails README section above
are relevant to that project's source code only.