The Practical Framework Guide

Search

Cable Car and View Variants

Work in progress

Template Renderer

To try to clean up controllers, keep them lean, handle a variety of rendering variants/complexity, and fit into the larger Rails MVC flow; we should have a template handler for CableReady.

The renderer itself

## railtie.rb

initializer("cable_ready.template_renderer") do
  ActiveSupport.on_load :action_view do
    ActionView::Template.register_template_handler :cablecar, CableCarTemplate
  end
end
# cable_car_template.rb
# frozen_string_literal: true

class CableCarTemplate
  attr_accessor :source

  def initialize(&block)
    self.source = block
  end

  def render_in(view_context)
    source.call
    view_context.controller.render(
      cable_ready: view_context.cable_car
    )
  end

  def format
    :json
  end

  def self.call(template, source = nil)
    src = source || template.source
    <<~RUBY
      __cablecar_template__ = CableCarTemplate.new do
        #{src}
      end
      __cablecar_template__.render_in(self).to_s
    RUBY
  end
end

CableCarTemplate uses Rails' render_in support to render the object using the given view_context.

An example template

# example.cable_ready.cablecar
# frozen_string_literal: true

dialog_id = dom_id(@organization_deal, :new_update)

html_string = controller.render_to_string(ExampleComponent.new(
  open: true,
  id: dialog_id
))

cable_car.append(
  html: html_string,
  selector: "#example",
)

Variants

With a template renderer like this, we can use Kasper Timm Hansen's UI Context and Rails Variants approach, using the default Rails rendering pipelines:

def example
  # ...
  respond_to do |format|
    format.html
    format.cable_ready
  end
end
# example.cable_ready.cablecar
# frozen_string_literal: true

dialog_id = dom_id(@organization_deal, :new_update)

html_string = controller.render_to_string(ExampleComponent.new(
  open: true,
  id: dialog_id
))

cable_car.append(
  html: html_string,
  selector: "#example",
)
# example.cable_ready+context_menu.cablecar
# frozen_string_literal: true

html_string = controller.render_to_string(ContextMenu::ExampleComponent.new)

cable_car.replace(
  html: html_string,
  selector: "#context-menu",
)

This means that we can write numerous view approaches without getting bogged down with writing multiple controller actions. Plus, since these are named routes, they're easy to onboard and debug, no hidden incantations!

Extending the approach, forms that need to generate context-specific responses can pass along the variant as a hidden attribute:

<%= hidden_field_tag :variant, request.variant %>