Rails Integration
- Last updated 29 Oct 22
Trailblazer runs with any Ruby web framework, whether that is Rails, Hanami or Grape. Why? Because it’s a set of additional abstraction layers like forms, service objects (“operations”) or view components. Those are designed to be completely isolated and decoupled from the hosting infrastructure framework.
Nevertheless, you’re lucky if your project is using Rails since we provide convenient glue code in the trailblazer-rails
gem.
gem "trailblazer-rails"
This will automatically pull the trailblazer
gem along with its commonly used dependencies. The Trailblazer::Rails::Railtie
railtie will activate all necessary convenience methods for you. You don’t have to do anything manually.
There’s a beautiful QUICK START tutorial introducing you to Trailblazer in Rails.
Controller
Controller extensions help with
- invoking operations and dealing with the outcome
- rendering cells that access variables from the operation’s result object
Please note, though, that we’re slowly moving towards using endpoints in controllers. Once this new layer becomes stable we will support both approaches.
Manual invoke
In order to run an operation in a controller you may simply invoke it manually.
class SongsController < ApplicationController
def create
result = Song::Operation::Create.(params: params) # manual invocation.
# ...
end
end
Note how you have to pass all data into the #call
, even variables such as params
, that are “magically everywhere” in Rails. This is per design - we want encapsulation!
After running the operation, you need to handle different outcomes.
class SongsController < ApplicationController
def create
result = Song::Operation::Create.(params: params) # manual invocation.
if result.success?
redirect_to song_path(result[:model].id)
else # failure
@form = result[:"contract.default"]
render
end
end
end
In a bit more complex situations, with more data being passed into the operation, this might become repetitive and quite complex.
Run
The trailblazer-rails
gem provides #run
to simplify the task of invoking operations.
def new
ctx = run Song::Operation::New
@form = ctx[:"contract.default"]
render
end
The #run
method will always return the result object (or ctx) from the operation.
While it invokes the specified operation it does three things for you.
- Without any additional configuration,
#run
passes the controller’sparams
hash into the operation call as in the example above. - It automatically assigns
@model
and, if available,@form
for you, after invoking the operation. - The
ctx
object is assigned to@_result
.
To handle success and failure cases, run accepts an optional block.
def create
_ctx = run Song::Operation::Create do |ctx|
# success!
return redirect_to song_path(ctx[:model].id) # don't forget the return.
end
# failure
@form = _ctx[:"contract.default"]
render
end
The block is only run for a successful outcome of the operation - or in other words, when ctx.success?
is true. Into the block is passed the result object (or ctx
) as a block argument.
Do not forget to use return
in the block, otherwise Ruby will execute the remaining code under the block, too.
After the block, you usually add code for handling a failing outcome of the operation run. Here, you can use the ctx
object returned from #run
.
The success block of #run
can also hand you variables from ctx
as keyword arguments, just as you know it from [operation steps].
def create
_ctx = run Song::Operation::Create do |ctx, model:,**|
return redirect_to song_path(model.id) # see how model is available?
end
# failure
@form = _ctx[:"contract.default"]
render
end
This is convenient shortcut to access ctx variables, and gives you a runtime error should that variable be absent.
Keyword arguments for #run
are available from trailblazer-rails
>= 2.4.2.
If your operation needs more variables in the ctx you can hand in those via #run
.
class SongsController < ApplicationController
# ...
def create
# vvvvvvvvvvvvvvvvvvvvvvvvvv
_ctx = run Song::Operation::Create, current_user: current_user do |ctx, model:, **|
return redirect_to song_path(model.id)
end
@form = _ctx[:"contract.default"]
render
end
end
This will add :current_user
to the ctx passed into the Song::Operation::Create
operation, allowing you to access it in the steps, as done in the example step below.
class Song::Operation::Create < Trailblazer::Operation
# ...
step :user_allowed?
def user_allowed?(ctx, current_user:, **)
# do something with {current_user}
end
end
You can also configure generic variables on the controller level, they will be passed to all operations of that controller.
Often, several operations across an application slice have generic variables, such as the current_user
. Instead of redundantly passing them to #run
in every controller action, you can configuring variables on the controller-level.
Override #_run_options
to do that automatically for all run
calls in a controller.
class SongsController < ApplicationController
private def _run_options(options)
options.merge(
current_user: current_user
)
end
# ...
end
The overridden #_run_options
method will result in :current_user
being passed into the operation when using #run
without having to specify that particular parameter.
def create
_ctx = run Song::Operation::Create do |ctx, model:, **|
return redirect_to song_path(model.id)
end
@form = _ctx[:"contract.default"]
render
end
When mixing both styles, options passed directly to #run
will win over possible same-named options in #_run_options
.
Endpoint
During the past few years of Trailblazer a new layer emerged, we call it endpoint. An endpoint is literally an “operation” getting invoked straight from the controller and running your business operation (like Song::Operation::Create
) at some point. Before and after that, you can place code steps you’d normally use in before_filter
and friends.
We will document this new (completely optional!) layer once it is fully presentable. Until then, check out the API docs.
Cells
Cells is a separate gem, it provides view components to maintain strong encapsulation in your partial world.
The trailblazer-rails
gem makes it very simple to use the Cells gem along with TRB. It overrides ActionController#render
and allows to render a cell.
File structure
In the early, adventurous days of Cells - the project was kicked off in 2007 - both naming of cell classes and their file structure was following the Rails Way™ style. In a Rails app, you could find a structure as below.
gem "cells"
app
├── cells
│ ├── song_cell.rb
│ ├── song
│ │ ├── show.erb
│ │ ├── list.erb
While this is still possible by simply using the cells
gem stand-alone, most projects using Trailblazer prefer the new file structure implemented in the trailblazer-cells
gem.
gem "trailblazer-cells"
app
├── concepts
│ ├── song
│ │ ├── cell
│ │ │ ├── show.rb # Song::Cell::Show
│ │ │ ├── list.rb
│ │ ├── view
│ │ │ ├── show.erb
│ │ │ ├── list.erb
This results in your cells being named Song::Cell::Show
, adhering to the Trailblazer convention. Refer to the API docs to learn more about Trailblazer cells.
Render
The trailblazer-rails
gem extends the #render
method and allows you to render cells in controllers and views.
class SongsController < ApplicationController
def create
run Song::New # optional
render cell(Song::Cell::New, @model)
end
end
You simply invoke #cell
the way you did it before, and pass it to render
. If the first argument to #render
is not a cell, the original Rails render
version will be used, allowing you to use serializers, partials or whatever else you need.
Per default, #render
will add layout: true
to render the controller’s ActionView layout.
The controller’s layout can be disabled using layout: false
. These are pure Rails mechanics.
render cell(Song::Cell::New, @model), layout: false
In fact, any option passed to render
will be passed through to the controller’s #render
.
As you’re using #cell
from the Cells gem you may pass any option the cell understands.
render cell(Song::Cell::New, model, layout: Song::Cell::DarkLayout)
For instance, you can instruct the cell to use a separate layout cell. Note that this option is passed to #cell
, not to #render
.
Configuration
You can set controller-wide options passed to every #cell
call by overriding the #options_for_cell
method.
class SongsController < ApplicationController
private def options_for_cell(model, options)
{
layout: Song::Cell::DarkLayout # used for all #cell calls.
}
end
end
This is extremely helpful for providing controller-wide options like :layout
, allowing you to skip this specific option in all actions.
class SongsController < ApplicationController
def create
# ..
render cell(Song::Cell::New, model) # no need to pass {layout: Song::Cell::DarkLayout}
end
This feature was added in trailblazer-rails
version 2.2.0.
Reform
Reform is a form object library that helps to move validations out of your models.
It has excellent Rails support which will soon be summarized here. Until then, you can find API docs here.
Loader
The trailblazer-loader
gem implements a very simple way to load all files in your concepts
directory in a heuristically meaningful order. It can be used in any environment.
In trailblazer-rails
2.3.0 this component has been removed in favor of Rails autoloading. You can safely skip this section!
Loader with Rails
The trailblazer-loader
gem comes pre-bundled with trailblazer-rails
for historical reasons: in the early days of Trailblazer, the conventional file name concepts/product/operation/create.rb
didn’t match the short operation name, such as Product::Create
.
The trailblazer-loader
gem’s duty was to load all concept files without using Rails’ autoloader, overcoming the latter’s conventions.
Over the years, and with the emerge of controller helpers or our workflow engine calling operations for you, the class name of an operation more and becomes a thing not to worry about.
Many projects use Trailblazer along with the Rails naming convention now. This means you can disable the loader gem, and benefit from Rails auto-magic behavior such as faster loading in the “correct” order, reloading and all the flaws that come with this non-deterministic behavior.
As a first step, add Operation
to your operation’s class name, matching the Rails naming convention.
# app/concepts/product/operation/create.rb
module Product::Operation
class Create < Trailblazer::Operation
# ...
end
end
It’s a Trailblazer convention to put [ConceptName]::Operation
in one line: it will force Rails to load the concept name constant, so you don’t have to reopen the class yourself.
This will result in a class name Product::Operation::Create
.
Next, disable the loader gem, in config/initializers/trailblazer.rb
.
# config/initializers/trailblazer.rb
YourApp::Application.config.trailblazer.enable_loader = false
Trailblazer files will now be loaded by Rails - you need to follow the Rails autoloading file naming from here on, and things should run smoothly. A nice side-effect here is that in bigger projects (with hundreds of operations), the start-up time in development accelerates significantly.
The infamous warning: toplevel constant Cell referenced by Notification::Cell
warning is a bug in Ruby. You should upgrade to Ruby >= 2.5.