Jun 08, 2016
Table of contents:
The very first component of Trailblazer was Cells. I briefly mentioned Cells in my review of Trailblazer, but so far in this exploration of Trailblazer we yet to explore them.
Cells encapsulate parts of your UI into components. A Cell is basically a View Model that allows you to define logic for your view using good Object-oriented programming techniques.
This makes it really easy to bundle your User Interface into widgets that can then be composed and tested in isolation.
This is in stark contrast to the Rails way of having an ocean of global helper functions and messy views.
In today’s tutorial we will take our first look at getting started with Trailblazer Cells.
If you have only ever worked on small to medium sized Rails applications, you might be wondering why Cells is such a good idea. Isn’t it an unnecessary level of indirection?
The answer to that question is yes. Yes it is an unnecessary level of indirection for small, or simple view templates.
However, if you crack open just about any large or “legacy” Rails project, you will often find an ApplicationHelper
with hundreds of helper methods.
The Rails way of working with views is to have helper functions that are sprinkled through your views.
But it is almost inevitable that these helper functions get out of control. One day you will wake up and find that what was once just a handle of helpful methods is hundreds of random methods that are distributed seemingly at random throughout your views.
So if the out-of-the-box Rails development tools are not enough, what makes Trailblazer Cells so good?
A Cell encapsulates a part of your UI into a component. This allows you to move the logic from the View into the Cell, and keep the View itself clean and easy to work with.
Cells also reintroduce the Object-oriented methodologies that have been sucked out of Rails View layer. Inheritance suddenly becomes really easy, and we can often remove messy if statements all together.
Finally, Cells are really easy to test. Instead of booting up the entire stack to test the contents of a particular UI component, you can instantiate the Cell in isolation. This gives you a lot of confidence in your View layer, as it saves you from writing brittle tests that are hard to maintain.
There are actually more benefits to using Cells, but for simplicity we will concentrate on these characteristics to begin with!
Last week I created the Operation classes for creating a new user. This is your typical registration process that requires the user to enter an email, username, and password.
As a follow on from last week, in today’s tutorial we will look at a very simple Cell example that encapsulates the registration form.
We won’t be covering every bit of functionality of Trailblazer’s Cells in today’s tutorial as there is a lot to cover. But hopefully it will give you a good sense of the philosophy behind the architecture style.
By default, Cells are completely decoupled from Rails. You could use Cells in pretty much any type of Ruby application, I’m just using Rails in this example because it’s what I’m most familiar with.
However, Rails has certain expectations and so we annoyingly have to implement extra stuff in our code to make it play nicely.
The first thing I’m going to do is to create a Form
Cell class that can be extended by any of my Form Cells in this application:
module Culttt
module Cells
class Form < Trailblazer::Cell
include ActionView::Helpers::FormHelper
def dom_class(record, prefix = nil)
ActionView::RecordIdentifier.dom_class(record, prefix)
end
def dom_id(record, prefix = nil)
ActionView::RecordIdentifier.dom_id(record, prefix)
end
def form
@model.contract
end
end
end
end
This implements the dom_class
and dom_id
methods that Rails will be expecting, as well as includes ActionView::Helpers::FormHelper
.
I’ve also provided a helper method for the @model.contract
.
If you remember back to What are Trailblazer Contracts?, the Contract is actually just an instance of Reform. We’ll be using this later in the view template, and so I think making it available as form
makes more sense.
Now that we have the base Form
class setup, we can create the Cell class for the registration form.
Create a new file under your user
concept directory called cell.rb
:
module User::Create
class Cell < Culttt::Cells::Form
def show
render
end
end
end
As you can see, I’m extending from Culttt::Cells::Form
, which in turn is extending from Trailblazer::Cell
.
This is a really simple example as so we only need to implement the show
method.
As we will see in the coming weeks, the Cell
class provides a really nice way to implementing View based logic.
Now that we have the Cell class, we also need to create the actual template that will hold the HTML. In my case I’m using Slim, but you can use whatever templating engine you prefer.
Create a new directory under your user concept directory called cell/view
and then create a new file called cell.slim
:
- if form.errors.any? ul - form.errors.full_messages.each do |msg| li = msg =
form_for form, html: {class: "registration-form"}, url: join_url do |f| div =
f.label :email, class: "qa-email-label" = f.text_field :email, class:
"qa-email-input" div = f.label :username, class: "qa-username-label" =
f.text_field :username, class: "qa-username-input" div = f.label :password,
class: "qa-password-label" = f.password_field :password, class:
"qa-password-input" div = f.submit "Register", class: "qa-submit"
Hopefully you will recognise this as a fairly typical example of a form in Rails. There are only a couple of things to note.
Firstly, the form_for form
is using the form
helper method that we defined in the base Form
class from earlier.
Secondly, the join_url
is simply the Rails helper method for the /join
URL.
And finally, I’ve included qa-
classes on the inputs to help with testing. This idea came from this excellent article More Transparent UI Code with Namespaces.
There are a couple of different types of tests you can write for Cells. In this pretty simple example, I’m just going to assert that the correct form elements have been included.
Obviously these tests will pass right now, but as you can probably imagine, views tend to change a lot, and so ensuring that form elements haven’t accidently been “deleted” is actually a valuable regression test to have.
First I’m going to add Capybara to my project. This will allow me to write nice HTML and CSS assertions:
group :test do
gem 'faker'
gem 'factory_girl_rails'
gem 'minitest-rails-capybara'
end
Next, I will include Capybara in the Cell::TestCase
test class:
Cell::TestCase.class_eval do
include ::Capybara::DSL
include ::Capybara::Assertions
end
And finally I can write my tests:
require 'test_helper'
module User::Create::CellTest
class CellTest < Cell::TestCase
controller JoinController
test 'has correct markup' do
html =
concept(
'user/create/cell',
User::Create::Operation::Default.present({})
).()
html.must_have_selector('form.registration-form')
html.must_have_selector('label.qa-email-label')
html.must_have_selector('input.qa-email-input')
html.must_have_selector('label.qa-username-label')
html.must_have_selector('input.qa-username-input')
html.must_have_selector('label.qa-password-label')
html.must_have_selector('input.qa-password-input')
html.must_have_selector('input.qa-submit')
end
end
end
There is a couple of interesting things to point out in this test.
First we need to set the controller to JoinController
because Rails will require it.
Secondly we (ironically) use the concept
helper method to generate the HTML from the cell.
Thirdly, when passing the Operation, I use the present
method. I don’t want to run the Operation before it has been passed to the Cell.
Finally, as you can see, in this test I’m simply asserting that the correct qa-
classes are in the generated HTML.
One of my favourite aspects of Trailblazer is definitely Cells. I really hate having messy logic in my views and so I’m definitely a big fan of View Models. I find it inevitable that you will need this type of abstraction in larger projects.
I also really love how easy it is to test Cells. I can simply generate the HTML and make the assertions I require. Previously I probably would of just not bothered to test the contents of a view because it would be too much hassle.
In an upcoming tutorial I’m going to show you how I used Cells to clean up what would of been a really messy view situation.