Ruby on Rails View Components with Trailblazer Cells
JavaScript is now more composable than ever, and it feels it won’t change for a long time as it’s one of the most efficient ways of creating websites and web apps now. But how do Ruby on Rails and View Components cooperate with each other? Is it possible to include component-based structure within RoR applications? With Trailblazer Cells, it is.
In recent times view components became a really popular web design pattern. View component is standalone part of view, which can be used at many views.
Most common JavaScript frontend frameworks like React, Ember or Angular use view components pattern. Even last hot topic in web world "Web Components" is just implementation of view components.
Ok, but it’s all about JavaScript world. What if we want to stay in Rails world? Can we use view components in RoR app? Yes, We can! We can do it with Trailblazer Cells.
On the first page of Cells documentation we read:
A cell is an object that represent a fragment of your UI. The scope of that fragment is up to you: it can embrace an entire page, a single comment container in a thread or just an avatar image link.
I could just list adventages of cells, but you can find them in the documentation. I would like to show simple example of cells’ usage. I hope it will show you benefits of view components in rails app.
The example is a simple app with CRUD actions for below database.
Let’s start by creating the app with scaffolds.
rails new cells-app
cd cells-app
rails g scaffold company name:string city:string
rails g scaffold department name:string company:references
rails g scaffold employee first_name:string last_name:string department:references
rake db:create db:migrate
add
root to: ‘companies#index’
toconfig/routes.rb
We’ve created base app with scaffolds for company, department and employee. Now we can configure cells’:
Add to gemfile:
gem "trailblazer-cells"
gem "cells-erb"
gem "cells-rails"
Run
bundle install
Run
mkdir app/cells
So, finally we can go to most interesting part – cells implementation. If we list the app/views
we will see that we already have a lot of views:
We will use code from these views in our cells.
We will create cells for each type of view:
IndexCell
ShowCell
NewCell
EditCell
And some view components cells:
TableCell
TitleCell
FormCell
NoticeCell
We will replace each template with cell. Completed project you can find on GitHub. Here I will just show two examples of cells: TableCell
and IndexCell
.
TableCell
Firstly, we need to create files structure. TableCell
is built from one ruby file and four erb(html) files. Our cells’ directory should look like below.
table_cell.rb
is the core of our cell, it contains all helpful methods.
table_cell.rb
class TableCell < Cell::ViewModel
def items
model[:items]
end
def attributes
model[:attributes]
end
def model_name
model[:model_name]
end
def header
render
end
def body
render
end
def row(item)
@item = item
render
end
def row_attributes(item)
attributes.map do |attribute|
item.send(attribute)
end
end
def show_path(item)
polymorphic_path(item)
end
def edit_path(item)
edit_polymorphic_path(item)
end
end
The main view part of TableCell
is show.erb
, it will be rendered while we will use the cell. It contains table tag and renders header.erb
and body.erb
views.
table/show.erb
<table>
<%= header %>
<%= body %>
</table>
In header.erb
we put thead
tag and th
tag for each attribute provided to cell.
table/header.erb
<thead>
<tr>
<% attributes.each do |header| %>
<%= content_tag :th, header.capitalize %>
<% end %>
<th colspan="3"></th>
</tr>
</thead>
In body.erb
we render table row for each item provided to cell.
table/body.erb
<% items.each do |item| %>
<%= content_tag :tr, row(item) %>
<% end %>
row.erb
creates td
tag for each attribute of the item.
table/row.erb
<% row_attributes(@item).each do |attribute| %>
<%= content_tag :td, attribute %>
<% end %>
<td><%= link_to 'Show', show_path(@item) %></td>
<td><%= link_to 'Edit', edit_path(@item) %></td>
<td>
<%= link_to 'Destroy', show_path(@item), method: :delete, data: { confirm: 'Are you sure?' } %>
</td>
IndexCell
At first we need to add abstract PageCell
with html layout, the main job of layout is including javascript and stylesheets assets. Each view needs to load the layout, so views cells will inherit from the PageCell
. We have to create app/cells/page_cell.rb
and app/cells/page/layout.erb
files.
page_cell.erb
class PageCell < Cell::ViewModel
layout :layout
end
page/layout.erb
<!DOCTYPE html>
<html>
<head>
<title>CellsApp</title>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
</head>
<body>
<%= yield %>
</body>
</html>
Let’s speed up. I’ve already created TitleCell
and NoticeCell
. We need them for IndexCell
. IndexCell
displays all of needed components on index page.
index_cell.erb
class IndexCell < PageCell
def items
model[:items]
end
def attributes
model[:attributes]
end
def model_name
model[:model_name]
end
def notice
model[:notice]
end
def new_path
new_polymorphic_path(model_name)
end
end
index/show.erb
<%=cell NoticeCell, notice: notice %>
<%=cell TitleCell, title: "Listing #{model_name.pluralize.capitalize}" %>
<%=cell TableCell, items: items, attributes: attributes %>
<br>
<%= link_to "New #{model_name.capitalize}", new_path %>
Implementation of others cells you can see at the repository. So, we have all cells ready. Let’s use them.
companies_controller.rb
class CompaniesController < ApplicationController
before_action :set_company, only: [:show, :edit, :update, :destroy]
def index
render html: cell(IndexCell, items: Company.all, model_name: 'company',
attributes: [:name, :city], notice: notice)
end
def show
render html: cell(ShowCell, item: @company, model_name: 'company',
attributes: [:name, :city], notice: notice)
end
def new
render html: cell(NewCell, item: Company.new, model_name: 'company',
attributes: [:name, :city])
end
def edit
render html: cell(EditCell, item: @company, model_name: 'company',
attributes: [:name, :city], notice: notice)
end
...
departments_controller.rb
class DepartmentsController < ApplicationController
before_action :set_department, only: [:show, :edit, :update, :destroy]
def index
render html: cell(IndexCell, items: Department.all, model_name: 'department',
attributes: [:name, :company], notice: notice)
end
def show
render html: cell(ShowCell, item: @department, model_name: 'department',
attributes: [:name, :company], notice: notice)
end
def new
render html: cell(NewCell, item: Department.new, model_name: 'department',
attributes: [:name, :company_id])
end
def edit
render html: cell(EditCell, item: @department, model_name: 'department',
attributes: [:name, :company_id], notice: notice)
end
employees_controller.rb
class EmployeesController < ApplicationController
before_action :set_employee, only: [:show, :edit, :update, :destroy]
def index
render html: cell(IndexCell, items: Employee.all, model_name: 'employee',
attributes: [:first_name, :last_name, :department], notice: notice)
end
def show
render html: cell(ShowCell, item: @employee, model_name: 'employee',
attributes: [:first_name, :last_name, :department], notice: notice)
end
def new
render html: cell(NewCell, item: Employee.new, model_name: 'employee',
attributes: [:first_name, :last_name, :department_id])
end
def edit
render html: cell(EditCell, item: @employee, model_name: 'employee',
attributes: [:first_name, :last_name, :department_id], notice: notice)
end
As you can see we have used the same cells for all controllers. That is the power of cells. Now If you want to change title header size for all pages, you have to just change one file, the TitleCell
:
diff --git a/app/cells/title/show.erb b/app/cells/title/show.erb
index 580040d..f8704a2 100644
--- a/app/cells/title/show.erb
+++ b/app/cells/title/show.erb
@@ -1,3 +1,3 @@
-<h1>
+<h3>
<%= title %>
-</h1>
\ No newline at end of file
+</h3>
\ No newline at end of file
That’s all. The change at one file is propagated to all views. How cool is that? And that is the sense of cells.
Of course, cells have more advantages, but I had only one goal in this article. I wanted to show you that with cells you can create flexible and reusable views at rails. I hope that now you can see it.
Let's talk about Jamstack and headless e-commerce!
Contact us and we'll warmly introduce you to the vast world of Jamstack & headless development!