🌡
I've got a fever, and the only cure is
more Reflex!
An assorted collection of patterns, directly extracted from the course.
DynamicFormReflex
Create and manage complex forms with page morphs
How?
- A model (
Address) has associations to two interdependent other models (Country=>State) - Via a
DynamicFormReflex#refreshaction, manage thestate_idselect box, which depends on thecountry
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_manyassociations:
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
Decouple your filtering and faceted search using concerns
How
- Use a
Filterableconcern to mediate between your controllers/concerns and the filtering logic - In separate
Filterclasses, 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
Successively add HTML fragments to the DOM on demand
How?
- CableReady is used to insert a new set of items using
insert_adjacent_htmlbefore a#sentinelelement after a “Load more” button is clicked. - A scoped page morph is used to determine the next
pageand 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
IntersectionObserverto 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
.limitand.offsetor any other pagination method.
NestedFormReflex
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
.buildon thehas_manyassociation fields_forexpands to all children if achild_attributes=setter is present (which is the case ifaccepts_nested_attributes_foris set) - see API docs
Caveat
Clean up your session (or other persistent store) after form submission.
TemplateReflex
Compose a UI using page morphs
How?
- UI components (“templates”) are inserted/removed using two reflex actions, and are identified by
uuids - The
sessionis 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
Write powerful wizards using page morphs
How?
- An empty state model is memoized in the controller (
@book ||= Book.new(title: ...)) - A general purpose
WizardReflexis used to step through the wizard and perist the model’s state in thesession - A
@current_stepvariable 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