🌡
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#refresh
action, manage thestate_id
select 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_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
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
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
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 thehas_many
association fields_for
expands to all children if achild_attributes=
setter is present (which is the case ifaccepts_nested_attributes_for
is 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
uuid
s - 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
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 thesession
- 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