Check out my consultancy offering at RailsReviews.com!

🌡
I've got a fever, and the only cure is
more Reflex!

An assorted collection of patterns, directly extracted from the course.

DynamicFormReflex

More

Create and manage complex forms with page morphs

How?

  • A model (Address) has associations to two interdependent other models (Country => State)
  • Via a DynamicFormReflex#refresh action, manage the state_id select box, which depends on the country

Caveat To use this with unpersisted records, you will need to adapt the def resource method slightly:

def resource
  @resource ||= if element.dataset.sgid.present?
    element.signed[:sgid] 
  else
    element.dataset.resource_name.classify.new
  end
end

Variations

  • You can also use this with has_many associations:
class Address < ApplicationRecord
  belongs_to :tenant
  has_many :inhabitants
  
  accepts_nested_attributes_for :inhabitants
class Controller
  def edit
    @address = Address.find(params[:id])
    
    @address.inhabitants.build
  end
end
<%= form_with model: @address do |form| %>
  <%= form.label :tenant_id %>
  <%= form.collection_select :tenant_id, Tenant.all, :id, :name, {include_blank: true}, 
    data: {reflex: "change->DynamicForm#refresh", sgid: @address.to_sgid.to_s, 
    resource_name: "address", association: "tenant"} %>
  
  <%= form.fields_for :inhabitants do |inhabitant_fields| %>
    <%= inhabitant_fields.label :inhabitant_id %>
    <%= inhabitant_fields.collection_select :inhabitant_id, @address.tenant&.members || [], :id, :name %>
  <% end %>
<% end %>

Filterable

More

Decouple your filtering and faceted search using concerns

How

  • Use a Filterable concern to mediate between your controllers/concerns and the filtering logic
  • In separate Filter classes, define how and which rules apply, and how to update filter params

Caveats

The Book and Restaurant classes in this example mimick an ActiveRecord model. The array refinements simply serve as stand-ins for model scopes or AR queries.

InfiniteScrollReflex

More

Successively add HTML fragments to the DOM on demand

How?

  • CableReady is used to insert a new set of items using insert_adjacent_html before a #sentinel element after a “Load more” button is clicked.
  • A scoped page morph is used to determine the next page and hides the “Load more” button when the last page is reached.

Caveat

Note that in a real-world app, you’d probably want to use model partials and collection rendering instead of inline rendering the items.

Variations

  • Use a Stimulus controller and an IntersectionObserver to automatically trigger loading:
import ApplicationController from "./application_controller";
import { useIntersection } from "stimulus-use";

export default class extends ApplicationController {
  static targets = ["button"];

  connect() {
    super.connect();
    useIntersection(this, { element: this.buttonTarget });
  }

  appear() {
    this.buttonTarget.disabled = true;
    this.buttonTarget.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';

    this.stimulate("ArticlesInfiniteScroll#load_more", this.buttonTarget);
  }
}
  • This example uses Pagy for pagination, but of course you could also just use .limit and .offset or any other pagination method.

NestedFormReflex

More

A reflex to construct a form that wraps a has_many relationship with nested attributes on the fly.

How?

  • New children are instantiated by calling .build on the has_many association
  • fields_for expands to all children if a child_attributes= setter is present (which is the case if accepts_nested_attributes_for is set) - see API docs

Caveat

Clean up your session (or other persistent store) after form submission.

TemplateReflex

More

Compose a UI using page morphs

How?

  • UI components (“templates”) are inserted/removed using two reflex actions, and are identified by uuids
  • The session is used to persist/manage them

Caveat

Note that in a real-world app, you’d probably want to use model partials and empty model instances to construct your UI (the Template class acts as a stand-in for both model and partial)

Variations

Use kredis as ephemeral persistence store

WizardReflex

More

Write powerful wizards using page morphs

How?

  • An empty state model is memoized in the controller (@book ||= Book.new(title: ...))
  • A general purpose WizardReflex is used to step through the wizard and perist the model’s state in the session
  • A @current_step variable is in/decreased to display the current wizard pane.
  • Validations are performed contextually, i.e. on: :step_1, etc.
  • An allowlist approach is used to centrally sanitize resource classes and strong params in that reflex.

Caveat

In these examples, the amount of steps per wizard are hardcoded.

Variations

Enrich individual WizardReflexes with custom input processing logic:

class WizardReflex < ApplicationReflex
  def refresh
    additional_attributes, processed_resource_params = yield(resource_params) if block_given?

    session[:"new_#{resource_name.underscore}"] = resource_class.new(processed_resource_params || resource_params)
    session[:"new_#{resource_name.underscore}"].assign_attributes(**additional_attributes || {})

    # ...
  end
  
  # ...
end

class BookReflex < WizardReflex
  def refresh
    super do |params|
      [{isbn: '1234'}, params.except(...)]
    end
  end
end