Morphs โ
By default, StimulusReflex updates your entire page. After re-processing your controller action, rendering your view template, and sending the raw HTML to your browser, StimulusReflex uses the amazing morphdom
library to do the smallest number of DOM modifications necessary to refresh your UI in just a few milliseconds. For many developers, this will be a perfect solution forever. They can stop reading here.
Most real-world applications are more sophisticated, though. You think of your site in terms of sections, components and content areas. We reason about our functionality with abstractions like "sidebar" but when it's time to work, we shift back to contemplating a giant tree of nested containers. Sometimes we need to surgically swap out one of those containers with something new. Sending the entire page to the client seems like massive overkill. We need to update just part of the DOM without disturbing the rest of the tree... and we need it to happen in ~10ms.
Other times... we just need to hit a button which feeds a cat which may or may not still be alive in a steel box. ๐
It's almost as if complex, real-world scenarios don't always fit the one-size-fits-all default full page Reflex.
Introducing Morphs โ
Behind the scenes, there are actually three different modes in which StimulusReflex can process your requests. We refer to them by what they will replace on your page: Page, Selector and Nothing. All three benefit from the same logging, callbacks, events and promises.
Changing the Morph mode happens in your server-side Reflex class, either in the action method or the callbacks. Both markup e.g. data-reflex
and programmatic e.g. stimulate()
mechanisms for initiating a Reflex on the client work without modification.
morph
is only available in Reflex classes, not controller actions. Once you change modes, you cannot change between them.
What are you replacing? | Process Controller Action? | Typical Round-Trip Speed |
---|---|---|
The full page (default) | Yes | ~50ms |
All children of a CSS DOM selector | No | ~15ms |
Nothing at all | No | ~6ms |
Page Morphs โ
Page morphs are the default behavior of StimulusReflex and they are what will occur if you don't call morph
in your Reflex.
All Reflexes are, in fact, Page morphs - until they are not. ๐ดโ๏ธ
What makes Page Morphs interesting and distinct from other Morph types is that they are the only one that re-runs the page's controller action before rendering the new HTML markup.
Any instance variables that you set in your Reflex action method are available to your controller action. In addition, there is a special @stimulus_reflex
variable that is set to true
when a controller action is being run by a Reflex.
INFO
StimulusReflex does not support using redirect_to
in a Page Morph. If you try to return an HTTP 302 in your controller during a Reflex action, your page content will become "You are being redirected."
Scoping Page Morphs โ
Instead of updating your entire page, you can specify exactly which parts of the DOM will be updated using the data-reflex-root
attribute.
data-reflex-root=".class, #id, [attribute]"
Simply pass a comma-delimited list of CSS selectors. Each selector will retrieve one DOM element; if there are no elements that match, the selector will be ignored.
StimulusReflex will decide which element's children to replace by evaluating three criteria in order:
- Is there a
data-reflex-root
on the element with thedata-reflex
? - Is there a
data-reflex-root
on an ancestor element above the element in the DOM? It could be the element's immediate parent, but it doesn't have to be. - Just use the
body
element.
Here is a simple example: the user is presented with a text box. Anything they type into the text box will be echoed back in two div elements, forwards and backwards.
<div data-reflex-root="[forward],[backward]">
<input type="text" value="<%= @words %>" data-reflex="keyup->Example#words">
<div forward><%= @words %></div>
<div backward><%= @words&.reverse %></div>
</div>
class ExampleReflex < ApplicationReflex
def words
@words = element[:value]
end
end
INFO
One interesting detail of this example is that by assigning the root to [forward],[backward]
we are implicitly telling StimulusReflex to not update the text input itself. This prevents resetting the input value while the user is typing.
INFO
In StimulusReflex, morphdom
is called with the childrenOnly flag set to true.
This means that <body>
or the custom parent selector(s) you specify are not updated. For this reason, it's necessary to wrap anything you need to be updated in a div, span or other bounding tag so that it can be swapped out without confusion.
If you're stuck with an element that just won't update, make sure that you're not attempting to update the attributes on an <a>
.
INFO
It's completely valid for an element with a data-reflex-root
attribute to reference itself via a CSS class or other mechanism. Just always remember that the parent itself will not be replaced! Only the children of the parent are modified.
Permanent Elements โ
Perhaps you just don't want a section of your DOM to be updated by StimulusReflex. Perhaps you need to integrate 3rd-party elements such as ad tracking scripts, Google Analytics, and any other widget that renders itself such as a React component or legacy jQuery plugin.
Just add data-reflex-permanent
to any element in your DOM, and it will be left unchanged by full-page Reflex updates and morph
calls that re-render partials. Note that morph
calls which insert simple strings or empty values do not respect the data-reflex-permanent
attribute.
<div data-reflex-permanent>
<iframe src="https://ghbtns.com/github-btn.html?user=stimulusreflex&repo=stimulus_reflex&type=star&count=true" frameborder="0" scrolling="0" class="ghbtn"></iframe>
<iframe src="https://ghbtns.com/github-btn.html?user=stimulusreflex&repo=stimulus_reflex&type=fork&count=true" frameborder="0" scrolling="0" class="ghbtn"></iframe>
</div>
INFO
We have encountered scenarios where the data-reflex-permanent
attribute is ignored unless there is a unique id
attribute on the element as well. If you are working with the Trix editor (ActionText) you absolutely must use data-reflex-permanent
and specify an id
attribute.
Please let us know if you can identify this happening in the wild, as technically it shouldn't be necessary... and yet, it works.
ยฏ*(ใ)*/ยฏ
INFO
Beware of Ruby gems that implicitly inject HTML into the body as it might be removed from the DOM when a Reflex is invoked. For example, consider the intercom-rails gem which automatically injects the Intercom chat into the body. Gems like this often provide instructions for explicitly including their markup. We recommend using the explicit option whenever possible, so that you can wrap the content with data-reflex-permanent
.
Selector Morphs โ
This is the perfect option if you want to re-render a partial, update a counter or just set a container to empty. Since it accepts a string, you can pass a value to it directly, use render
to regenerate a partial or even connect it to a ViewComponent.
Updating a target element with a Selector morph does not invoke ActionDispatch. There is no routing, your controller is not run, and the view template is not re-rendered. This means that if your content is properly fragment cached, you should see round-trip updates in 10-15ms... which is a nice change from the before times. ๐
Tutorial โ
Let's first establish a baseline HTML sample to modify. Our attention will focus primarily on the div
known colloquially as #foo.
<header data-reflex="click->Example#change">
<%= render partial: "path/to/foo", locals: {
message: "Am I the medium or the massage?"
} %>
</header>
Behold! For this is the foo
partial. It is an example of perfection:
<div id="foo">
<span class="spa"><%= message %></span>
</div>
You create a Selector morph by calling the morph
method. Typically, it takes two parameters: selector and html. We pass any valid CSS DOM selector that returns a reference to the first matching element, as well as the value we're updating it with.
class ExampleReflex < ApplicationReflex
def change
morph "#foo", "Your muscles... they are so tight."
end
end
If you consult your Elements Inspector, you'll now see that #foo now contains a text node and your header
has gained some attributes. This is just how StimulusReflex makes the magic happen.
<header data-reflex="click->Example#change">
<div id="foo">Your muscles... they are so tight.</div>
</header>
Morphs only replace the children of the element that you are targeting. If you need to update the target element (as you would with outerHTML
) consider targeting the parent of the element you need to change. You could, for example, call morph "header", "No more #foo."
and start fresh.
INFO
Cool, but where did the span go? We're glad you asked!
The truth is that a lot of complexity and nasty edge cases are being hidden away, while presenting you intelligent defaults and generally trying to follow the principle of least surprise.
There's no sugar coating the fact that there's a happy path for all of the typical use cases, and lots of gotchas to be mindful of otherwise. We're going to tackle this by showing you best practices first. Start by #winning now and later there will be a section with all of the logic behind the decisions so you can troubleshoot if things go awry / Charlie Sheen.
Intelligent defaults โ
Morphs work differently depending on whether you are replacing existing content with a new version or something entirely new. This allows us to intelligently re-render partials and ViewComponents based on data that has been changed in the Reflex action.
yelling = element.value.upcase
morph "#foo", render(partial: "path/to/foo", locals: {
message: yelling
})
INFO
Since StimulusReflex v3.4, render
has been delegated to the controller class responsible for rendering the current page. Of course, you're still free to use ApplicationController
or any other ActionDispatch controller to render your content.
You'll have access to all the same helpers that you would in a normal Rails HTTP request and the subsequent SSR handling of it.
If ViewComponents are your thing, we have you covered:
morph "#foo", render(FooComponent.new(
message: "React is making your muscles sore."
))
The foo
partial (listed in the Tutorialsection above) is an example of a best practice for several subtle but important reasons which you should use to model your own updates:
- it has a single top-level container element with the same CSS selector as the target
- inside that container element is another element node, not a text node
If you can follow those two guidelines, you will see several important benefits regardless of how the HTML stream is generated:
- DOM changes will be performed by the
morphdom
library, which is highly efficient - morph will respect elements with the
data-reflex-permanent
attribute - any event handlers set on contents should remain intact (unless they no longer exist)
As you have already seen, it's okay to morph a container with a string, or a container element that has a different CSS selector. However, morph
will treat these updates slightly differently:
- DOM elements are replaced by updating innerHTML
- elements with the
data-reflex-permanent
attribute will be over-written - any event handlers on replaced elements are immediately de-referenced
- you could end up with a nested container that might be jarring if you're not expecting it
Let's say that you update #foo with the following morph:
morph "#foo", %(<div id="foo">Let's do something about those muscles.</div>)
This update will use morphdom
to update the existing #foo div. However, because #foo contains a text node, data-reflex-permanent
is ignored. (Sorry! We just work here.)
morph "#foo", %(<div id="baz"><span>Just breathe in... and out.</span></div>)
Now your content is contained in a span
element node. All set... except that you changed #foo to #baz.
<header data-reflex="click->Example#change">
<div id="foo">
<div id="baz">
<span>Just breathe in... and out.</span>
</div>
</div>
</header>
That's great - if that's what you want. ๐คจ
Ultimately, we've optimized for two primary use cases for morph functionality:
- Updating a partial or ViewComponent to reflect a state change.
- Updating a container element with a new value or HTML fragment.
Real-world example: Pagy refactoring โ
If you're doing pagination in Rails, pagy is the tool for the job. pagy works great with StimulusReflex, Bootstrap and FontAwesome:
<div id="paginator">
<%= render partial: "paginator", locals: { pagy: @pagy } %>
</div>
<div id="posts">
<%= render @posts %>
</div>
def index
@pagy, @posts = pagy(Post.all, page: 1)
end
class PagyReflex < ApplicationReflex
include Pagy::Backend
def paginate
pagy, posts = pagy(Post.all, page: element.dataset.page.to_i)
morph "#paginator", render(partial: "paginator", locals: { pagy: pagy })
morph "#posts", render(posts)
end
end
<nav class="d-flex justify-content-center">
<ul class="pagination">
<li class="page-item"><a href="#" id="page_prev_li" class="page-link" data-reflex="click->Pagy#paginate" data-page="<%= pagy.prev || 1 %>"><span class="far fa-angle-double-left"></span></a></li>
<% pagy.series.each do |item| %>
<% if item == :gap %>
<li class="page-item disabled"><a class="page-link" id="page_gap_li">...</a></li>
<% else %>
<li class="page-item <%= "active" if item.is_a?(String) %>">
<a href="#" id="page_<%= item %>_li" class="page-link" data-reflex="click->Pagy#paginate" data-page="<%= item %>"><%= item %></a>
</li>
<% end %>
<% end %>
<li class="page-item"><a href="#" id="page_next_li" class="page-link" data-reflex="click->Pagy#paginate" data-page="<%= pagy.next || pagy.last %>"><span class="far fa-angle-double-right"></span></a></li>
</ul>
</nav>
Hang on, though... if you watch the client-side logging when you click the button to advance to the 2nd page, you'll see that both morph
calls used CableReady inner_html
operations to update the divs. While this might be fine for some applications, inner_html
completely wipes out any Stimulus controllers present in the replaced DOM hierarchy and doesn't respect the data-reflex-permanent
attribute. How can we adapt this so that both morph
operations are performed by the morphdom
library?
The paginator
partial is only rendered one time, so this one is easy: we have to move the top-level div into the partial. When it gets re-rendered, it will automatically match what morph
needs to update the contents because it is the contents:
<%= render partial: "paginator", locals: { pagy: @pagy } %>
<div id="posts"><%= render @posts %></div>
<div id="paginator">
<nav class="d-flex justify-content-center">
<ul class="pagination">
<li class="page-item"><a href="#" id="page_prev_li" class="page-link" data-reflex="click->Pagy#paginate" data-page="<%= pagy.prev || 1 %>"><span class="far fa-angle-double-left"></span></a></li>
<% pagy.series.each do |item| %>
<% if item == :gap %>
<li class="page-item disabled"><a class="page-link" id="page_gap_li">...</a></li>
<% else %>
<li class="page-item <%= "active" if item.is_a?(String) %>">
<a href="#" id="page_<%= item %>_li" class="page-link" data-reflex="click->Pagy#paginate" data-page="<%= item %>"><%= item %></a>
</li>
<% end %>
<% end %>
<li class="page-item"><a href="#" id="page_next_li" class="page-link" data-reflex="click->Pagy#paginate" data-page="<%= pagy.next || pagy.last %>"><span class="far fa-angle-double-right"></span></a></li>
</ul>
</nav>
</div>
The posts
partial (not listed) is rendered as a collection, and so it must be handled differently. You cannot put the top-level div into each element of the collection!
Instead, simply wrap the render
call itself with markup for the top-level div:
morph "#posts", %(<div id="posts">#{render(posts)}</div>)
Now, both paginator
and posts
are being updated using morphdom
.
Morphing Multiplicity โ
What fun is morphing if you can't stretch out a little?
morph "#username": "hopsoft", "#notification_count": 5
morph "#regrets"
You can call morph
multiple times in your Reflex action method.
You can use Ruby's implicit Hash syntax to update multiple selectors with one morph. These updates will all be sent as part of the same broadcast, and executed in the order they are defined. Any non-String values will be coerced into Strings. Passing no html argument is equivalent to ""
.
dom_id
โ
One of the best perks of Rails naming conventions is that you can usually calculate what the name of an element or resource will be programmatically, so long as you know the class name and id.
Inside a Reflex class, you might find yourself typing code like:
morph "#user_#{user.id}", user.name
The dom_id
helper is available inside Reflex classes and supports the optional prefix argument:
morph dom_id(user), user.name
View Helpers that emit URLs โ
If you are planning to render a partial that uses Rails routing view helpers to create URLs, you will need to set up your environment configuration files to make sure that your site's URL is available inside your Reflexes.
You'll know that you forgot this step if your URLs are coming out as example.com
.
Things go wrong... โ
We've worked really hard to make morphs easy to work with, but there are some rules and edge cases that you have to follow if you want your Selector Morphs to use a CableReady morph
operation instead of an inner_html
operation.
If you're not getting the results you expect, please consult the Morphing Sanity Checklist to make sure you're not accidentally using the wrong operation. Use of radiolabel can help provide an early warning.
Nothing Morphs โ
Your user clicks a button. Something happens on the server. The browser is notified that this task was completed via the usual callbacks and events.
Nothing morphs are Remote Procedure Calls, implemented on top of ActionCable.
Sometimes you want to take advantage of the chasis and infrastructure of StimulusReflex, without any assumptions or expectations about changing your DOM afterwards. The bare metal nature of Nothing morphs means that the time between initiating a Reflex and receiving a confirmation can be low single-digit milliseconds, if you don't do anything to slow it down.
Nothing morphs usually initiate a long-running process, such as making calls to APIs or supervising I/O operations like file uploads or video transcoding. However, they are equally useful for emitting signals; you could send messages into a queue, tell your media player to play, or tell your Arduino to launch the rocket.
The key strategy when working with Nothing morphs is to avoid blocking calls at all costs. While we might still be years away from a viable asynchronous Ruby ecosystem, using Reflexes to initiate Rails ActiveJob instances - ideally processed by Sidekiq and powered by Redis - provides a reliable platform that financial institutions still pay millions of dollars to achieve.
In a sense, Nothing morphs are yin to CableReady's yang. A Reflex conveys user intent to the server, and a broadcast is the vehicle for server intent to be realized on the client, completing the circle. โฏ๏ธ
I can't take the suspense. How can I capture this raw power for myself? โ
It's wickedly hard... but with practice, you'll be able to do it, too:
morph :nothing
That's it. That's the entire API surface. ๐
Multi-Stage Morphs โ
You can morph the same target element multiple times in one Reflex by calling CableReady directly. One clever use of this technique is to morph a container to display a spinner, call an API or access computationally intense results - which you cache for next time - and then replace the spinner with the new update... all in the same Reflex action.
morph :nothing
cable_ready[stream_name].morph({ spinner... }).broadcast
# long running work
cable_ready[stream_name].morph({ progress update... }).broadcast
# long running work
cable_ready[stream_name].morph({ final update... }).broadcast
ActiveJob Example โ
Let's step through creating an ActiveJob that will be triggered by a Nothing morph. Upon completion, the job will increment a counter and direct CableReady to update the browser. Note that you'll have to ensure that your ActiveJob infrastructure is up and running, ideally backed by Sidekiq and Redis.
First, some quick housekeeping: you need to create an ActionCable channel. Running rails generate channel counter
should do the trick. We want to stream updates to anyone listening in on the counter
stream.
class CounterChannel < ApplicationCable::Channel
def subscribed
stream_from "counter"
end
end
When the channel client receives data, send it to CableReady for processing.
import consumer from "./consumer"
consumer.subscriptions.create("CounterChannel", {
received(data) {
if (data.cableReady) CableReady.perform(data.operations)
}
})
Create a view template that contains a button
to launch the Reflex as well as a span
to hold the current value. We'll pull in the current value of the counter key in the Rails cache. If it doesn't yet exist, set the value to 0.
<button data-reflex="click->Counter#increment">
Increment Counter
</button>
The counter currently reads:
<span id="counter">
<%= Rails.cache.fetch("counter", raw: true) { 0 } %>
</span>
This is the complete implementation of a minimum viable Nothing morph Reflex action. Note that in a real application, you would almost certainly pass parameter arguments into your ActiveJob constructor. An ActiveJob can accept a wide variety of data types. This includes ActiveRecord models, which makes use of the Global ID system behind the scenes.
You need to give the job enough information to successfully broadcast any important results to the correct places. For example, if you're planning to broadcast notifications to a specific user, make sure to pass the user resource (ActiveRecord model instance) to the ActiveJob.
class CounterReflex < ApplicationReflex
def increment
IncrementJob.set(wait: 3.seconds).perform_later
morph :nothing
end
end
Finally, the job includes CableReady::Broadcaster so that it can send commands back to the client. We then use CableReady to queue up a text_content
operation with the newly incremented value before ultimately sending the broadcast.
class IncrementJob < ApplicationJob
include CableReady::Broadcaster
queue_as :default
def perform
cable_ready["counter"].text_content(
selector: "#counter",
text: Rails.cache.increment("counter")
).broadcast
end
end
This setup might seem like overkill to increment a number on your page, but you only need to setup the channel once, and then you really just need an ActiveJob class to make the magic happen. You can use these examples as starting points for applications of arbitrary sophistication and complexity.
INFO
There's an amazing resource on best practices with ActiveJob and Sidekiq available here.