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

trailblazer-rails

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’s params 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

trailblazer-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

trailblazer-rails

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.