Two Patterns for StimulusReflex Form Submissions
Tags: rails, stimulusreflex, cableready
Published: 2020-10-19 18:01:01

---
title: Two Patterns for StimulusReflex Form Submissions
published: true
date: 2020-10-16 00:00:00 UTC
tags: rails,stimulusreflex,cableready
canonical_url: https://blog.minthesize.com/two-patterns-for-stimulus-reflex-form-submissions
---

Forms are _the_ standard CRUD pattern and can be handled by standard Rails remote forms perfectly. Still there might be times when you want to use your newly acquired StimulusReflex superpowers for a slicker user experience. My suggestion would be to stick to your last, but below are two patterns you can utilize to improve not only your user interactions, but also your application structure.

## 1. Vanilla CableReady

On the [StimulusReflex discord](https://discord.gg/XveN625) we frequently get asked how to best handle form submissions in reflexes. My answer tends to be: “Don’t”. Instead I’m first going to show you a more robust technique that relies on standard Rails remote forms and the power of [CableReady](https://cableready.stimulusreflex.com/).

I’m borrowing a bit of the excellent [Calendar Demo](http://expo.stimulusreflex.com/demos/calendar/2020-10-01) on StimulusReflex’s expo site for the purposes of this walkthrough. (If you want to follow along, this RailsByte will install StimulusReflex and TailwindCSS. You can also use the [demo repo](https://github.com/julianrubisch/stimulus_reflex_form_patterns), complete with version tags)

For starters, our tiny calendar app needs just one model, `CalendarEvent`, which we generate with a scaffold command:

```sh
rails new calendula --template "https://www.railsbytes.com/script/z0gsd8" --skip-spring --skip-action-text --skip-active-storage --skip-action-mailer --skip-action-mailbox --skip-sprockets --skip-webpack-install
bin/rails g scaffold CalendarEvent occurs_at:datetime description:text color:string
```

In the `index` template, we only render two partials: One containing the form to add a new event, and the calendar grid itself. We inject `@dates`, which is just a collection of `Date` objects we want to display, and `@calendar_events`, all `CalendarEvent` s in this date range.

```erb
<!-- app/views/calendar_events/index.html.erb -->
<div id="calendar-form">
  <%= render "form", calendar_event: @calendar_event %>
</div>

<div class="w-screen p-8">
  <div class="grid grid-cols-7 w-full" id="calendar-grid">
    <%= render partial: "calendar_events/calendar_grid",
               locals: {dates: @dates, 
                        calendar_events: @calendar_events} %> 
  </div>
</div>
```

The `_calendar_grid.html.erb` partial renders a collection of `_date.html.erb` partials:

```erb
<!-- app/views/calendar_events/_calendar_grid.html.erb -->
<% Date::DAYNAMES.each do |name| %> 
  <div class="bg-gray-500 text-white p-3"><%= name %></div>
<% end %>

<%= render partial: "date", collection: dates,
           locals: { calendar_events: calendar_events } %>
```

```erb
<!-- app/views/calendar_events/_date.html.erb -->
<div class="flex flex-col justify-between border h-32 p-3"> 
  <span class="text-small font-bold"><%= date %></span>

  <% (calendar_events[date] || []).each do |calendar_event| %> 
    <span class="bg-<%= calendar_event.color %>-700 rounded text-white px-4 py-2">
      <%= calendar_event.description %>
    </span>
  <% end %>
</div>
```

The `_form.html.erb` partial is just the standard one generated by the scaffold, nothing fancy here. In the `calendar_events_controller.rb`, however, we need to change a couple of things. (I’ll omit the part where all the instance variables are set up. You can find it in the [demo repo](https://github.com/julianrubisch/stimulus_reflex_form_patterns/blob/main/app/controllers/calendar_events_controller.rb) if you’re interested)

First, we remove the standard redirect upon successful insert. Second, we need to include the `CableReady::Broadcaster` module to inject this functionality. Then, we re-fetch the `@calendar_events` containing the newly created event and `morph` over the `#calendar-grid` using a `CableReady` broadcast:

```ruby
# app/controllers/calendar_events_controller.rb
class CalendarEventsController < ApplicationController
  include CableReady::Broadcaster

  # ...

  def create
    @calendar_event = CalendarEvent.new(calendar_event_params)

    if @calendar_event.save
      @calendar_events = CalendarEvent.where(occurs_at: @date_range)
                                      .order(:occurs_at)
                                      .group_by(&:occurs_at_date)

      cable_ready["CalendarEvents"].morph({selector: "#calendar-grid",
                                           html: CalendarEventsController.render(partial: 'calendar_events/calendar_grid',
                                                                                 locals: {dates: @dates,
                                                                                          calendar_events: @calendar_events}),
                                           children_only: true})
      cable_ready.broadcast
    else
      render :index
    end
  end
  # ...end
```

Of course we need to do the necessary `CableReady` setup, i.e. create a channel and configure the corresponding JavaScript channel.

```sh
bin/rails g channel CalendarEvents 
```

```ruby
# app/channels/calendar_events_channel.rb
class CalendarEventsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "CalendarEvents"
  end
end
```

```js
// app/javascript/channels/calendar_events_channel.js
import CableReady from "cable_ready";
import consumer from "./consumer";

consumer.subscriptions.create("CalendarEventsChannel", {
  received(data) {
    if (data.cableReady) CableReady.perform(data.operations);
  }
});
```

Looking at the result we can observe that the insert works as expected, only the form does not reset. That’s expected though, since we’re not dealing with a normal request/response cycle, so we have to do this ourselves:

![Inserting an event](https://dev-to-uploads.s3.amazonaws.com/i/o73xnq4xqrycx5pi683r.gif)

Let’s add a second `CableReady` operation for replacing the `#calendar-form` partial:

```ruby
class CalendarEventsController < ApplicationControllerinclude CableReady::Broadcaster

  # ...

  def create
    @calendar_event = CalendarEvent.new(calendar_event_params)

    if @calendar_event.save
      @calendar_events = CalendarEvent.where(occurs_at: @date_range)
                           .order(:occurs_at)
                           .group_by(&:occurs_at_date)

      cable_ready["CalendarEvents"].morph({selector: "#calendar-grid",
                                           html: CalendarEventsController.render(partial: 'calendar_events/calendar_grid',
                                                                                 locals: {dates: @dates,
                                                                                          calendar_events: @calendar_events}),
                                           children_only: true}
                                         )

      # -----> we also have to reset the form <-----
      cable_ready["CalendarEvents"].inner_html({selector: "#calendar-form",
                                                html: CalendarEventsController.render(partial: "form",
                                                                                      locals: {calendar_event: CalendarEvent.new})
                                               })
      cable_ready.broadcast
    else
      render :index
    end
  end

  # ...end
```

Note that I’m deliberately using the `inner_html` operation here, since `morph` would not actually clear the inputs, since their current state is not reflected in the DOM.

![Form Reset](https://dev-to-uploads.s3.amazonaws.com/i/nn3f41nzlvkg8gi1z3kf.gif)

The added benefit of this approach over a pure StimulusReflex based one is that we can update our model(s) on other parts of the site, too. If we isolate that logic in an `ActiveJob` (or a service object, for those so inclined), we can make it more testable and reuse it from other parts of the application.

```sh
bin/rails g job StreamCalendarEvents
```

```ruby
# app/controllers/calendar_events_controller.rb
def create
  @calendar_event = CalendarEvent.new(calendar_event_params)

  if @calendar_event.save
    @calendar_events = CalendarEvent.where(occurs_at: @date_range)
                         .order(:occurs_at)
                         .group_by(&:occurs_at_date)

    StreamCalendarEventsJob.perform_now(dates: @dates,
                                        calendar_events: @calendar_events,
                                        date_range: @date_range)
  else
    render :index
  end
end
```

```ruby
# app/jobs/stream_workloads_job.rb
class StreamCalendarEventsJob < ApplicationJob
  include CableReady::Broadcaster

  queue_as :default

  def perform(dates:, calendar_events:, date_range:)
    cable_ready["CalendarEvents"].morph({selector: "#calendar-grid",
                                         html: CalendarEventsController.render(partial: 'calendar_events/calendar_grid',
                                                                               locals: {dates: dates,
                                                                                        calendar_events: calendar_events,
                                                                                        date_range: date_range}),
                                         children_only: true})

    # we also have to reset the form
    cable_ready["CalendarEvents"].inner_html({selector: "#calendar-form",
                                              html: CalendarEventsController.render(partial: "form",
                                                                                    locals: {calendar_event: CalendarEvent.new})
                                             })
    cable_ready.broadcast
  end
end
```

We’ll revisit this `ActiveJob` in part 2.

## 2. Use a Concern to DRY your StimulusReflex Form Submissions

Pure reflex-based form submissions can be a source of **Shotgun Surgery** smells if you pay no attention, since you’ll end up with a lot of duplicated logic. True, standard Rails controllers are resource-based too, but creating a `resource.rb`, `resources_controller.rb`, `resources_reflex.rb` structure will lead you down the path of unmaintainability quickly. Let’s revisit the example from above and put the form handling logic in a reflex.

### Re-Using the Pattern

So what’s the point in this exercise if I didn’t demonstrate how this pattern can be reused? For this, let’s assume we want to include national holidays in our calendar (I’m using the [holidays](https://github.com/holidays/holidays) and [country_select](https://github.com/stefanpenner/country_select) gems here). First, we need a model to store this data. We’ll use a standard ActiveRecord and use it in a “singleton” fashion (meaning, we pretend there’s only ever one instance of it).

```sh
bin/rails g model Settings holiday_region:string
```

We’d like to add a simple dropdown to choose a country from, so we just add

```erb
<div class="grid grid-cols-2 w-full">
  <div id="calendar-form"
       data-controller="reflex-form"
       data-reflex-form-reflex-name="CalendarEventsReflex">
    <%= render "form", calendar_event: @calendar_event %>
  </div>

  <div id="settings-form"
       data-controller="reflex-form"
       data-reflex-form-reflex-name="SettingsReflex">
    <%= form_with(model: Setting.first, method: :put, url: "#", class: "m-8",
                  data: {signed_id: Setting.first.to_sgid.to_s, target: "reflex-form.form"}) do |form| %>
    <div class="max-w-lg rounded-md shadow-sm sm:max-w-xs">
      <%= form.label :holiday_region %>

      <%= form.country_select :holiday_region,
                              {only: Holidays.available_regions.map(&:to_s).map(&:upcase)},
                              class: "form-select block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5",
                              data: {action: "change->reflex-form#submit", reflex_dataset: "combined"} %>
    </div>
    <% end %>
  </div>
</div>
```

to our `calendar_events/index.html.erb`. Note that we use the same stimulus controller we created above! We are passing in a signed global ID (SGID) of our `Settings` instance, too. We need to extend our `Submittable` concern to check whether a signed ID is present in the element’s dataset. If so, we retrieve the record and assign the changed attributes. If not, our creation logic stays in place:

```ruby
# app/reflexes/concerns/submittable.rb

module Submittable

  # ...

  included do
    before_reflex do
      if element.dataset.signed_id.present?
        @resource = GlobalID::Locator.locate_signed(element.dataset.signed_id)
        @resource.assign_attributes(submit_params)
      else
        resource_class = Rails.application.message_verifier("calendar_events")
                           .verify(element.dataset.resource)
                           .safe_constantize
        @resource = resource_class.new(submit_params)
      end
    end
  end

  # ...
end
```

Last but not least we have to create the `SettingsReflex` that includes the `Submittable` concern.

```sh
bin/rails g stimulus_reflex SettingsReflex
```

```ruby
# app/reflexes/settings_reflex.rb
class SettingsReflex < ApplicationReflex
  include Submittable

  before_reflex :fetch_dates

  after_reflex do
    @calendar_events = CalendarEvent.where(occurs_at: @date_range)
                         .order(:occurs_at)
                         .group_by(&:occurs_at_date)

    StreamCalendarEventsJob.perform_now(dates: @dates,
                                        calendar_events: @calendar_events,
                                        date_range: @date_range)
  end

  private

  def submit_params
    params.require(:setting).permit(:holiday_region)
  end

  def fetch_dates
    # ...
  end
end
```

And here lies the beauty of this pattern: The actual reflex logic is reduced to handling the DOM patching, and even that could be extracted to a concern since it’s the exact same as above. We only need to adapt our `_date.html.erb` partial to display the holiday name:

```erb
<div class="flex flex-col justify-between border h-32 p-3 <%= "bg-gray-200" unless date_range.include? date %>">
  <span class="text-small font-bold"><%= date %></span>

  <% holidays = Holidays.on(date, Setting.first.holiday_region.downcase.to_sym) %>

  <% unless holidays.empty? %>
    <span class="bg-blue-600 rounded text-white px-4 py-2">
      <%= holidays.map { |h| h[:name] }.join(" ") %>
    </span>
  <% end %>

  <% (calendar_events[date] || []).each do |calendar_event| %>
    <span class="bg-<%= calendar_event.color %>-700 rounded text-white px-4 py-2"><%= calendar_event.description %> </span>
  <% end %>
</div>
```

Here’s the end result:

![Selecting a Holiday Region](https://dev-to-uploads.s3.amazonaws.com/i/orgmtwn2doz907doyvti.gif)

## Conclusion

I’ve introduced two patterns for form submissions using [CableReady](https://cableready.stimulusreflex.com) and [StimulusReflex](https://docs.stimulusreflex.com) that you should consider when implementing it in your application. Using websockets as a transport necessitates the re-thinking of some established patterns, but done right can lead to a more cohesive codebase.