<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.2.0">Jekyll</generator><link href="http://blog.edruder.com/feed.xml" rel="self" type="application/atom+xml" /><link href="http://blog.edruder.com/" rel="alternate" type="text/html" /><updated>2026-02-02T15:59:38-08:00</updated><id>http://blog.edruder.com/feed.xml</id><title type="html">edruder.com</title><subtitle>my stuff</subtitle><entry><title type="html">Defense in Depth for Real-Time Rails</title><link href="http://blog.edruder.com/2026/02/01/real-time-rails-defense-in-depth.html" rel="alternate" type="text/html" title="Defense in Depth for Real-Time Rails" /><published>2026-02-01T00:00:00-08:00</published><updated>2026-02-01T00:00:00-08:00</updated><id>http://blog.edruder.com/2026/02/01/real-time-rails-defense-in-depth</id><content type="html" xml:base="http://blog.edruder.com/2026/02/01/real-time-rails-defense-in-depth.html">&lt;p&gt;In the &lt;a href=&quot;/2026/01/31/turbo-actioncable-trap.html&quot;&gt;previous post&lt;/a&gt;, I fixed a race condition between Turbo and
ActionCable that caused connected clients to diverge during rapid interactions.
The fix was straightforward: bypass Turbo’s form submission lifecycle for
actions that broadcast via ActionCable, and return &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;head :no_content&lt;/code&gt; instead of
redirecting. One update path instead of two. No more interference.&lt;/p&gt;

&lt;p&gt;That fixed the loudest problem. But the investigation didn’t stop there.&lt;/p&gt;

&lt;p&gt;As I traced the behavior through every layer–server concurrency, broadcast
delivery, WebSocket lifecycle, client-side state management–I found issues at
each one. A read-then-write race in the mutation method that could silently
merge concurrent adjustments. A polling interval tuned for low database load
rather than real-time responsiveness. A WebSocket disconnection handler that did
literally nothing, silently dropping any broadcasts missed during a network
blip. And fixing the Turbo lifecycle issue inadvertently resolved a separate
class of bugs–phantom sound effects–that I hadn’t fully diagnosed yet.&lt;/p&gt;

&lt;p&gt;No single fix would have been sufficient. The Turbo race from the previous post
was the most visible failure mode, but each of these quieter issues could
independently cause clients to show stale state under the right conditions. The
fixes work as layers, each one addressing a different failure mode.&lt;/p&gt;

&lt;p&gt;The specific bugs were in a poker timer, but the patterns are general to any
Rails app doing real-time sync with ActionCable. This post is less of a
debugging story and more of a hardening guide–four fixes, walking from server
to client, that made the feature reliable under production conditions.&lt;/p&gt;

&lt;h3 id=&quot;pessimistic-locking&quot;&gt;Pessimistic Locking&lt;/h3&gt;

&lt;p&gt;The timer’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adjust_time!&lt;/code&gt; method had a concurrency bug hiding in plain sight.
It read the current state, computed a new value, and wrote it back–without
holding a database lock during the read:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;adjust_time!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;elapsed_seconds&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;current&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;started_at&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;current_computed_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current_time_remaining&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;elapsed_seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;max&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;new_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_computed_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;minutes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;update!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;current_time_remaining: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;started_at: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;broadcast_time_adjusted&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The timer record is loaded by a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before_action&lt;/code&gt; at the start of the request
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@timer = @event.timer&lt;/code&gt;). Each Puma thread gets its own copy of the record with
whatever values were in the database at that moment.&lt;/p&gt;

&lt;p&gt;If two requests arrive close together, both threads read the same starting
state, both compute the same result, and both write it–producing one effective
adjustment from two requests.&lt;/p&gt;

&lt;p&gt;This is a time-of-check-to-time-of-use race, sometimes called TOCTOU: the state
you checked is no longer the state you’re acting on by the time your write
lands. This race exists regardless of your database–PostgreSQL, MySQL, and
SQLite are all vulnerable when the application reads state in one step and
writes in another without holding a lock.&lt;/p&gt;

&lt;p&gt;My app uses SQLite, whose single-writer serialization prevents the writes
themselves from interleaving, but that doesn’t help–the damage is done in the
reads, which happen before either thread tries to write.&lt;/p&gt;

&lt;p&gt;In development, this race never triggered. With a local server responding in
single-digit milliseconds, Turbo’s button-disable behavior serialized the
requests–each response arrived and re-enabled the button before the next click
could fire. Requests never overlapped.&lt;/p&gt;

&lt;p&gt;Here’s the irony: the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo=&quot;false&quot;&lt;/code&gt; fix from the previous post actually
made this race &lt;em&gt;more&lt;/em&gt; likely, not less. With Turbo bypassed, the browser sends
standard form submissions with no Navigator lifecycle managing them. Each click
fires an independent HTTP request immediately, with no button-disable
serialization.&lt;/p&gt;

&lt;p&gt;On a connection with 50-100ms of latency, three rapid clicks easily produce
overlapping requests. The Turbo fix solved the client-side race but opened the
door to a server-side one. Defense in depth means fixing both.&lt;/p&gt;

&lt;p&gt;The fix is pessimistic locking–acquire an exclusive row lock before reading,
so no other thread can read stale state. Active Record’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;with_lock&lt;/code&gt; method
wraps the block in a transaction and acquires an exclusive row lock–equivalent
to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transaction { lock!; ... }&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;adjust_time!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;with_lock&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;reload&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# re-read current state inside the lock&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;elapsed_seconds&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;current&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;started_at&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;current_computed_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current_time_remaining&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;elapsed_seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;max&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;new_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_computed_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;minutes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;update!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;current_time_remaining: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;started_at: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;broadcast_time_adjusted&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Two details matter here.&lt;/p&gt;

&lt;p&gt;First, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reload&lt;/code&gt; inside the lock is critical. Without it, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;with_lock&lt;/code&gt; acquires
the exclusive lock but the method still operates on the stale in-memory
attributes loaded at the start of the request. The lock alone doesn’t refresh
them.&lt;/p&gt;

&lt;p&gt;Second, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_time_adjusted&lt;/code&gt; is outside the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;with_lock&lt;/code&gt; block–broadcasting
doesn’t need the lock, and keeping the critical section small avoids holding it
longer than necessary.&lt;/p&gt;

&lt;p&gt;This pattern applies to any mutation method that reads current state, computes a
new value, and writes it back. It’s distinct from idempotency–each timer
adjustment is intentionally non-idempotent (clicking “-5 minutes” three times
should subtract 15 minutes), so you can’t solve this by making the operation
safe to retry. You need the lock to ensure each read sees the result of the
previous write.&lt;/p&gt;

&lt;h3 id=&quot;tuning-solid-cable-for-real-time-use&quot;&gt;Tuning Solid Cable for Real-Time Use&lt;/h3&gt;

&lt;p&gt;Solid Cable is the database-backed ActionCable adapter that ships as the default
in Rails 8. Instead of Redis pub/sub, broadcasts are inserted as rows into a
cable messages table, and a listener thread polls for new messages at a
configurable interval. That interval determines the maximum delay between a
broadcast being written and a client receiving it.&lt;/p&gt;

&lt;p&gt;Mine was set to 0.5 seconds–a reasonable default that keeps database load low.
For most ActionCable use cases (chat messages, notifications, dashboard
updates), half a second of delivery latency is fine. For a real-time feature
where users expect instant feedback from rapid interactions, it’s not.&lt;/p&gt;

&lt;p&gt;The polling interval also affects batching. When multiple broadcasts are written
within a single poll cycle, they’re all picked up and delivered together. This
isn’t a problem by itself–the client processes them sequentially in order.&lt;/p&gt;

&lt;p&gt;But it widens the timing window for the kind of race conditions I described
above. A broadcast that would have been delivered immediately with push-based
pub/sub instead sits in the database for up to 500 milliseconds, during which
other requests are being processed, other broadcasts are being written, and the
client’s state may be shifting underneath.&lt;/p&gt;

&lt;p&gt;This wasn’t the cause of the Turbo race condition from the previous post–that
bug exists regardless of your ActionCable adapter. But the polling interval
amplified it. Narrower delivery windows mean less opportunity for timing-
dependent failures.&lt;/p&gt;

&lt;p&gt;The fix is a one-line config change:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/cable.yml&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;production&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;adapter&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;solid_cable&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;polling_interval&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;0.1.seconds&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# was 0.5.seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The trade-off is straightforward: 5x more frequent polling means 5x more
database reads. For a small-scale app on SQLite, this is negligible. At scale,
you’d want to benchmark it.&lt;/p&gt;

&lt;p&gt;But the general principle holds: adapter defaults are tuned for broad
compatibility, not for your specific latency requirements. If you’re building a
feature that depends on fast broadcast delivery, check what your adapter is
actually doing.&lt;/p&gt;

&lt;p&gt;(This only applies to polling-based adapters like Solid Cable. If you’re using
Redis-backed ActionCable, pub/sub delivers messages immediately and there’s no
polling interval to tune.)&lt;/p&gt;

&lt;h3 id=&quot;the-phantom-sounds&quot;&gt;The Phantom Sounds&lt;/h3&gt;

&lt;p&gt;After fixing the timer sync issue, a separate problem surfaced: brief, phantom
sound effects during rapid adjustments. The timer has an audio alert that plays
when the blind level advances–a double beep. This sound was firing during
interval adjustments on timers that weren’t even running.&lt;/p&gt;

&lt;p&gt;Users would hear the start of a beep, cut short almost immediately, as if
something triggered the audio and then killed it. This one had me stumped for a
while–I initially suspected some kind of audio race condition or localStorage
corruption.&lt;/p&gt;

&lt;p&gt;Turns out, two bugs were interacting to produce it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first bug was spurious broadcasts.&lt;/strong&gt; The timer is implemented as a Rails
engine mounted in a host application. The host app had a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before_action&lt;/code&gt; that
called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sync_with_client_state!&lt;/code&gt; on every timer request–including adjustment
actions.&lt;/p&gt;

&lt;p&gt;The engine’s adjustment methods explicitly avoided this call. The code comments
said as much: “We intentionally do NOT call sync_with_client_state! here.”
The engine treats the client as the source of truth for running timers–the
client advances blind levels autonomously, and the server catches up at defined
sync points. By adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sync_with_client_state!&lt;/code&gt; to a blanket &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before_action&lt;/code&gt;,
the host app overrode that design.&lt;/p&gt;

&lt;p&gt;The consequence: when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sync_with_client_state!&lt;/code&gt; ran during an adjustment
request, it compared the server’s database state to the computed client state.
For a running timer, the server is typically behind–that’s by design. So the
sync method “caught up” by advancing blind levels and broadcasting
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blinds_increase&lt;/code&gt; to all clients. These broadcasts were spurious. The blinds
hadn’t actually advanced; the server was just reconciling a gap that the engine
was designed to tolerate.&lt;/p&gt;

&lt;p&gt;The fix was a one-line deletion: remove &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sync_with_client_state!&lt;/code&gt; from the
host’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before_action&lt;/code&gt;. The engine already calls it at the right times–on
page load, on channel subscription, and via a periodic sync job.&lt;/p&gt;

&lt;p&gt;The lesson generalizes: when a library comments “we intentionally do NOT do X
here,” that’s a design constraint, not a suggestion. Understand the reasoning
before overriding it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second bug was stale Stimulus controller state during Turbo page
replacement.&lt;/strong&gt; When a spurious &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blinds_increase&lt;/code&gt; broadcast arrived at a client,
it triggered the audio alert path. But Turbo was simultaneously replacing the
page (following the redirect from the adjustment action).&lt;/p&gt;

&lt;p&gt;During page replacement, there’s a window where the old Stimulus controller is
still connected–it still has its ActionCable subscription, it still receives
broadcasts, and it still acts on them. The old controller received the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blinds_increase&lt;/code&gt; broadcast and started playing the alert sound. Then Turbo
finished the page swap, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;disconnect()&lt;/code&gt; fired on the old controller, and the
audio was paused mid-play–producing the abbreviated beep the user heard.&lt;/p&gt;

&lt;p&gt;The audio manager compounded the issue–its teardown method was designed to
preserve in-flight sounds during navigation, which meant it actively started
audio playback during Turbo page replacement.&lt;/p&gt;

&lt;p&gt;This bug was already fixed by the time I diagnosed it! The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo=&quot;false&quot;&lt;/code&gt;
change from the previous post eliminated Turbo page replacement for adjustment
actions entirely. No page replacement means no Stimulus disconnect/reconnect
churn, no window where an old controller receives broadcasts, and no stale audio
state. The phantom sounds disappeared as a side effect of fixing the sync race
condition.&lt;/p&gt;

&lt;p&gt;That’s worth noting as its own lesson: when you fix a lifecycle bug, look for
other symptoms that share the same root cause. I investigated the phantom
sounds as a separate issue, with separate hypotheses about audio race conditions
and localStorage corruption. They turned out to be downstream of the same Turbo
page replacement cycle that caused the timer divergence. The fix was the same
because the cause was the same.&lt;/p&gt;

&lt;h3 id=&quot;websocket-reconnect-recovery&quot;&gt;WebSocket Reconnect Recovery&lt;/h3&gt;

&lt;p&gt;ActionCable subscriptions provide &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;connected&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;disconnected&lt;/code&gt; callbacks on
the client side. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;disconnected&lt;/code&gt; callback fires when the WebSocket connection
drops–network blips, server restarts, mobile devices switching between WiFi
and cellular, laptop sleep/wake cycles.&lt;/p&gt;

&lt;p&gt;My timer controller’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handleDisconnected()&lt;/code&gt; implementation was:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;handleDisconnected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// (empty)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;During a disconnection, the server keeps broadcasting. With Solid Cable, those
broadcasts are written to the database as usual. But without a connected
WebSocket on the other end, they can’t be delivered. The messages sit in the
cable table until they’re trimmed, and the disconnected client never sees them.&lt;/p&gt;

&lt;p&gt;The recovery mechanism already existed on the server side. When the client
reconnects, ActionCable re-subscribes to the channel, which triggers the
server-side &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subscribed&lt;/code&gt; callback. Mine already called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transmit_timer_state&lt;/code&gt;,
which reads the current timer state and sends it as a point-to-point message to
the reconnecting client.&lt;/p&gt;

&lt;p&gt;The server was offering the client a way to catch up. The client just wasn’t
taking it.&lt;/p&gt;

&lt;p&gt;The fix is small. On disconnect, flag that I may have missed broadcasts. On
reconnect, clear the flag and force-accept the next server state–bypassing the
staleness checks that would normally compare it against local state that may no
longer be current:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;handleDisconnected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;missedBroadcasts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;handleConnected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;missedBroadcasts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;missedBroadcasts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;initialized&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// force-accept next server update&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The re-subscription triggers &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transmit_timer_state&lt;/code&gt; on the server, which sends
the current state as a point-to-point message. By resetting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;initialized&lt;/code&gt;, the
client treats that message as an initial sync rather than an incremental update,
accepting it unconditionally instead of comparing it against stale local state.&lt;/p&gt;

&lt;p&gt;Brief WebSocket disconnections are more common in production than you might
expect. WiFi access points hand off connections. Mobile networks switch cells.
Laptops close and reopen. Server deploys cycle processes.&lt;/p&gt;

&lt;p&gt;In development, the WebSocket connects to localhost and effectively never drops.
In production, disconnections are routine–and if your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;disconnected&lt;/code&gt; callback
is empty, every one of them is a silent opportunity for client state to drift.&lt;/p&gt;

&lt;p&gt;The general pattern: if your ActionCable subscription manages client-side state,
implement the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;disconnected&lt;/code&gt; callback. At minimum, flag that the client may have
missed updates. If the server already sends state on subscription (a common
pattern), you may not need additional logic–just ensure the client is ready to
accept that state as a full resync rather than an incremental update.&lt;/p&gt;

&lt;p&gt;This is the catch-all layer. The pessimistic locking ensures the server state is
correct. The Solid Cable tuning ensures broadcasts are delivered promptly. The
Turbo fix ensures the client processes them without interference. But even if
all of those are working, a network blip can still cause a client to miss a
broadcast. This layer ensures it recovers.&lt;/p&gt;

&lt;h3 id=&quot;the-compound-fix&quot;&gt;The Compound Fix&lt;/h3&gt;

&lt;p&gt;Here’s what I ended up with across both posts–five fixes at four layers of
the stack:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Layer&lt;/th&gt;
      &lt;th&gt;Problem&lt;/th&gt;
      &lt;th&gt;Fix&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Turbo form lifecycle&lt;/td&gt;
      &lt;td&gt;Navigator abort race on rapid clicks&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo=&quot;false&quot;&lt;/code&gt; bypasses Turbo&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Server concurrency&lt;/td&gt;
      &lt;td&gt;Read-then-write race in mutation methods&lt;/td&gt;
      &lt;td&gt;Pessimistic locking with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;with_lock&lt;/code&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reload&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Broadcast delivery&lt;/td&gt;
      &lt;td&gt;500ms polling widens timing windows&lt;/td&gt;
      &lt;td&gt;Reduced to 100ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;WebSocket resilience&lt;/td&gt;
      &lt;td&gt;No recovery from missed broadcasts&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handleDisconnected&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handleConnected&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The phantom sounds didn’t need their own fix–they were downstream of the Turbo
lifecycle and spurious broadcast issues that the other fixes addressed.&lt;/p&gt;

&lt;p&gt;No single row in that table would have been sufficient on its own. The Turbo fix
eliminated the most visible race but exposed the server-side concurrency bug.
The pessimistic locking ensured correct server state but couldn’t help if a
broadcast was missed due to a network blip. The Solid Cable tuning reduced
delivery latency but didn’t prevent the race. The WebSocket reconnect recovery
catches everything else–but only after the fact.&lt;/p&gt;

&lt;p&gt;Each layer catches failures that the others miss. That’s defense in depth
applied to application code: you don’t rely on one control, because no single
layer can account for every failure mode. The server ensures correctness. The
transport ensures timely delivery. The client ensures recovery.&lt;/p&gt;

&lt;p&gt;Every one of these bugs was invisible in development. Localhost round-trips in
single-digit milliseconds eliminate the timing windows where races happen.
WebSocket connections to localhost don’t drop. Solid Cable polling latency
doesn’t matter when the broadcast and the response arrive almost simultaneously.&lt;/p&gt;

&lt;p&gt;Production–real devices, real networks, real latency–is where these layers
get tested. If you can’t reproduce something locally but users are reporting
it, the problem is probably in a timing window that your development environment
is too fast to open.&lt;/p&gt;

&lt;p&gt;If any of this sounds familiar, I’d love to hear about it–leave a comment!&lt;/p&gt;</content><author><name></name></author><category term="rails" /><category term="hotwire" /><category term="turbo" /><category term="actioncable" /><category term="software" /><summary type="html">In the previous post, I fixed a race condition between Turbo and ActionCable that caused connected clients to diverge during rapid interactions. The fix was straightforward: bypass Turbo’s form submission lifecycle for actions that broadcast via ActionCable, and return head :no_content instead of redirecting. One update path instead of two. No more interference.</summary></entry><entry><title type="html">The Turbo + ActionCable Trap: When Your Real-Time Rails Feature Fights Itself</title><link href="http://blog.edruder.com/2026/01/31/turbo-actioncable-trap.html" rel="alternate" type="text/html" title="The Turbo + ActionCable Trap: When Your Real-Time Rails Feature Fights Itself" /><published>2026-01-31T00:00:00-08:00</published><updated>2026-01-31T00:00:00-08:00</updated><id>http://blog.edruder.com/2026/01/31/turbo-actioncable-trap</id><content type="html" xml:base="http://blog.edruder.com/2026/01/31/turbo-actioncable-trap.html">&lt;p&gt;Two phones, same app, same page containing a timer. One shows 4 minutes on the
timer. The other shows 9. The server says 4. Refresh the lagging phone and it
snaps to 4 instantly–which makes it maddening to debug, because the bug fixes
itself the moment you look at it.&lt;/p&gt;

&lt;p&gt;We built a shared timer for our weekly poker game—a Rails 8 app using Turbo
for form handling and ActionCable for real-time sync across devices. It worked
perfectly in development. It worked perfectly in production, too, right up until
someone started clicking the “minus 5 minutes” button a few times in quick
succession. Then the clients would diverge.&lt;/p&gt;

&lt;p&gt;The pattern was consistent in some ways but not others: one device would fall
behind by exactly one adjustment–usually the device that was clicked, but
sometimes the passive observer lagged. Any interaction on the lagging device
would immediately fix it. And it only happened with rapid clicks, never with
deliberate single clicks spaced a few seconds apart.&lt;/p&gt;

&lt;p&gt;Here’s what made it hard: Turbo and ActionCable were both doing the right thing.
The problem was that they were both doing it &lt;em&gt;at the same time&lt;/em&gt;, on the same
state, and their page lifecycles were silently interfering with each other.&lt;/p&gt;

&lt;p&gt;It took two rounds of investigation and fixes across multiple layers to fully
resolve–because the first fix addressed a real problem but exposed a deeper
one underneath.&lt;/p&gt;

&lt;h3 id=&quot;the-architecture-that-seems-right&quot;&gt;The Architecture That Seems Right&lt;/h3&gt;

&lt;p&gt;The pattern we used is one you’ll find in a lot of Rails apps that combine Turbo
with real-time features. It looks like this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;User clicks a button (say, “adjust timer down by 5 minutes”)&lt;/li&gt;
  &lt;li&gt;Turbo submits the form as a background HTTP request (no page reload)&lt;/li&gt;
  &lt;li&gt;The server processes the mutation and broadcasts the new state via ActionCable&lt;/li&gt;
  &lt;li&gt;All connected clients receive the broadcast and update their UI&lt;/li&gt;
  &lt;li&gt;&lt;span id=&quot;step-5&quot;&gt;The server responds with a redirect back to the edit page&lt;/span&gt;&lt;/li&gt;
  &lt;li&gt;&lt;span id=&quot;step-6&quot;&gt;Turbo follows the redirect and re-renders the page with fresh server data&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is textbook Rails. Turbo handles forms. ActionCable handles real-time sync.
The controller action looks like what you’d expect:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# In the controller's update action&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;adjust_time&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;minutes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_i&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;adjust_time!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edit_path&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edit_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;alert: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Cannot adjust time&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adjust_time!&lt;/code&gt;, the model broadcasts after persisting the change:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;adjust_time!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ... compute new time, update the record ...&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;broadcast_time_adjusted&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# ActionCable broadcast to all subscribers&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Every connected client has a Stimulus controller subscribed to the ActionCable
channel. When a broadcast arrives, it updates the display. Clean separation of
concerns. Each piece doing its job.&lt;/p&gt;

&lt;p&gt;But notice &lt;a href=&quot;#step-5&quot;&gt;step 5&lt;/a&gt; and &lt;a href=&quot;#step-6&quot;&gt;step 6&lt;/a&gt; above. After the server
broadcasts the new state to &lt;em&gt;all&lt;/em&gt; clients via ActionCable, it &lt;em&gt;also&lt;/em&gt; redirects
the submitting client back to the page–which Turbo follows, replacing the page
body with a fresh render from the server.&lt;/p&gt;

&lt;p&gt;The clicking device gets the new state &lt;em&gt;twice&lt;/em&gt;: once from the ActionCable
broadcast, and again from the Turbo redirect and re-render. In normal operation,
this is invisible–both paths deliver the same data, and whichever arrives
second is effectively a no-op.&lt;/p&gt;

&lt;p&gt;This worked flawlessly in development. Two browser windows side by side, rapid
clicks, perfect sync every time.&lt;/p&gt;

&lt;p&gt;The reason was simple: with a local server, request round-trips complete in
single-digit milliseconds. The timing window where anything could go wrong was
vanishingly small. It wasn’t until the app was running in production–real
devices on real networks with real latency–that the window opened wide enough
for the race to happen.&lt;/p&gt;

&lt;h3 id=&quot;what-went-wrong&quot;&gt;What Went Wrong&lt;/h3&gt;

&lt;p&gt;In production, the bug showed up like this: a user would click the adjustment
button three times in quick succession. One device would end up showing the
correct value–reflecting all three adjustments. The other would show a value
that was off by exactly one adjustment, as if it had only received two of the
three broadcasts.&lt;/p&gt;

&lt;p&gt;A few things made this hard to pin down:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It was intermittent.&lt;/strong&gt; Slow, deliberate clicks never triggered it. You had to
click fast enough that requests overlapped or came in within a few hundred
milliseconds of each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Either device could be the one that lagged.&lt;/strong&gt; Usually it was the device that
was clicked, but sometimes the passive observer fell behind instead. There 
was no consistent “active client is right, passive client is wrong”
pattern–which made it harder to form a hypothesis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It self-healed.&lt;/strong&gt; Any action on the lagging device–clicking a button,
navigating away and back, even just refreshing–would snap it to the correct
value. The server always had the right state. The problem was purely in how
that state reached (or failed to reach) the client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The server was correct.&lt;/strong&gt; Every adjustment was processed and persisted. The
database always reflected all three clicks. This wasn’t a lost-update problem on
the server side–it was a delivery or acceptance problem on the client side.&lt;/p&gt;

&lt;p&gt;If you’ve built real-time features with Turbo and ActionCable, you might
recognize this pattern. My first instinct was to suspect ActionCable
itself–maybe a broadcast was dropped, maybe Solid Cable’s polling missed
something. I went down that road. It wasn’t the answer.&lt;/p&gt;

&lt;h3 id=&quot;the-first-fix-that-wasnt-enough&quot;&gt;The First Fix (That Wasn’t Enough)&lt;/h3&gt;

&lt;p&gt;The dual update path I described above–ActionCable broadcast plus Turbo
redirect/re-render–was the obvious place to start. If the two paths could
interfere with each other, maybe eliminating one would fix the problem.&lt;/p&gt;

&lt;p&gt;Looking at the controller action, every successful adjustment ended with
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redirect_to edit_path&lt;/code&gt;. That meant Turbo was following the redirect, fetching a
fresh page, and replacing the entire page body. During that replacement, the
Stimulus controller that managed the timer display would disconnect (tearing down
its ActionCable subscription) and a new one would connect (creating a fresh
subscription).&lt;/p&gt;

&lt;p&gt;There was a window–brief, but real–where the client had no ActionCable
subscription at all.&lt;/p&gt;

&lt;p&gt;If a broadcast arrived during that window, it would be lost. The client would
never receive it. Meanwhile, the Turbo re-render would show whatever the
server’s state was at &lt;em&gt;that&lt;/em&gt; moment–giving the clicking device a second chance
to get the right answer even if it missed a broadcast.&lt;/p&gt;

&lt;p&gt;This could explain why &lt;em&gt;sometimes&lt;/em&gt; the passive device lagged (it missed a
broadcast and had no re-render fallback), and it could also explain why
&lt;em&gt;sometimes&lt;/em&gt; the clicking device lagged (the re-render showed an intermediate
state while a later broadcast was lost during the subscription gap).&lt;/p&gt;

&lt;p&gt;This seemed like a plausible explanation. The fix was straightforward: replace
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redirect_to edit_path&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;head :no_content&lt;/code&gt; for adjustment actions. Since
ActionCable was already broadcasting the new state to all clients, the redirect
was redundant–its only purpose was to re-render the page, which the broadcast
already handled.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;adjust_time&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;minutes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_i&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;adjust_time!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:no_content&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# was: redirect_to edit_path&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edit_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;alert: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Cannot adjust time&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I deployed the fix and tested it. Still broken!&lt;/p&gt;

&lt;p&gt;The redirect &lt;em&gt;was&lt;/em&gt; a real problem–eliminating it removed the Stimulus
disconnect/reconnect churn and the broadcast-loss window that came with it. But
it wasn’t the whole story.&lt;/p&gt;

&lt;p&gt;With the redirect gone, both clients were now relying entirely on ActionCable
for updates. No more asymmetric recovery path. And yet one client still fell
behind by exactly one adjustment during rapid clicks. Something else was going
on.&lt;/p&gt;

&lt;h3 id=&quot;the-real-problem-turbos-hidden-page-lifecycle&quot;&gt;The Real Problem: Turbo’s Hidden Page Lifecycle&lt;/h3&gt;

&lt;p&gt;With the redirect eliminated, I went deeper. If both clients were now getting
updates exclusively through ActionCable, and the server was sending all three
broadcasts, where was one getting lost?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dead end: client-side validation logic.&lt;/strong&gt; The Stimulus controller had staleness
checks and time-direction validation on incoming broadcasts. I traced every code
path with concrete values for three sequential &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;time_adjusted&lt;/code&gt; broadcasts. Every
one passed every guard. The JavaScript was not rejecting valid broadcasts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dead end: Solid Cable delivery.&lt;/strong&gt; I traced the full broadcast pipeline
end-to-end–from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActionCable.server.broadcast&lt;/code&gt; through Solid Cable’s SQLite
message table, the polling listener thread, the event loop thread pool,
ActionCable’s worker pool, and finally the WebSocket write. There is no mechanism
in that pipeline for silent message loss. If a message is inserted into the cable
database, it will be delivered to all subscribers. Solid Cable was not the
problem.&lt;/p&gt;

&lt;p&gt;That left Turbo. Even with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;head :no_content&lt;/code&gt; instead of a redirect, Turbo still
manages the form submission lifecycle–and its behavior during rapid submissions
is where the problem lives.&lt;/p&gt;

&lt;p&gt;Here’s what Turbo’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Navigator&lt;/code&gt; does when a form is submitted:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;submitForm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;submitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;formSubmission&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormSubmission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;submitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;formSubmission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.stop()&lt;/code&gt; call is the key. If a previous form submission is still
in-flight when a new one starts, Turbo aborts it. The previous fetch request is
cancelled via its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AbortController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is sensible behavior for navigation–you don’t want two competing page
loads. But for rapid-fire mutations that broadcast via ActionCable, the
consequences are subtle.&lt;/p&gt;

&lt;p&gt;Turbo also disables the submit button when a form submission begins, and
re-enables it when the response arrives. For a fast local server returning
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;204 No Content&lt;/code&gt; in a few milliseconds, the button is disabled for an
imperceptibly short time–rapid clicks all get through.&lt;/p&gt;

&lt;p&gt;But for a production server with real network latency, the timing changes.
Clicks might land while the button is disabled (and be silently swallowed), or
they might land in a narrow window where an aborted submission’s cleanup
asynchronously re-enables the button just after the new submission disabled it.&lt;/p&gt;

&lt;p&gt;The result: depending on network conditions, some combination of three rapid
clicks produces only two server requests, or produces three server requests where
one is aborted client-side. The server processes what it receives and broadcasts
for each one. But the number of clicks, the number of server-side mutations, and
the number of broadcasts received by each client can all differ–by exactly
one–depending on timing.&lt;/p&gt;

&lt;h3 id=&quot;the-fix&quot;&gt;The Fix&lt;/h3&gt;

&lt;p&gt;The solution follows directly from the diagnosis: if Turbo’s form submission
lifecycle is interfering with ActionCable-driven updates, take Turbo out of the
equation for these specific actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Bypass Turbo for broadcast-driven actions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo=&quot;false&quot;&lt;/code&gt; to any form that triggers a server-side mutation which
is broadcast to clients via ActionCable:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;button_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timer_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;method: :patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;params: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;action_type: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;adjust_time&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;minutes: &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&amp;gt;&lt;/span&gt;-5 min&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo=&quot;false&quot;&lt;/code&gt;, the form submits as a standard browser request–no
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Navigator&lt;/code&gt;, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.stop()&lt;/code&gt;, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AbortController&lt;/code&gt;, no button disable/enable
dance. Each click fires an independent HTTP request. The browser ignores the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;204 No Content&lt;/code&gt; response (no navigation, no page replacement), and the UI
update comes entirely through ActionCable. Every client gets its update through
the same path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Return &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;head :no_content&lt;/code&gt; instead of redirecting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was my first fix, and it’s still necessary. Without Turbo managing the
form, a redirect would cause a full page reload (standard browser behavior for a
non-Turbo form getting a 302). Returning &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;204 No Content&lt;/code&gt; tells the browser
there’s nothing to do with the response–the ActionCable broadcast handles the
UI update.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;adjust_time&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;minutes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_i&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;adjust_time!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:no_content&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edit_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;alert: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Cannot adjust time&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note that the error case still redirects–if the adjustment fails, I want to
show the user an alert, and there’s no race condition concern because no
broadcast was sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this changes architecturally:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before the fix, adjustment actions had two update paths: the ActionCable
broadcast (for all clients) and the Turbo redirect/re-render (for the clicking
client only). These paths raced against each other and against Turbo’s
Navigator lifecycle.&lt;/p&gt;

&lt;p&gt;After the fix, there is exactly one update path: the ActionCable broadcast.
Every client–whether it clicked the button or not–receives the update the
same way. No asymmetry, no race, no timing dependence.&lt;/p&gt;

&lt;h3 id=&quot;what-i-took-away-from-this&quot;&gt;What I Took Away From This&lt;/h3&gt;

&lt;p&gt;It’s possible that there’s a better way to fix this kind of problem. The fix
described here does seem to work, and work well. But it’s possible that there’s
a better solution–maybe one that doesn’t entail disabling Turbo in certain
contexts. If you think of a better solution, I’d love to hear about it!&lt;/p&gt;

&lt;p&gt;The specific bug was in a poker timer, but the underlying pattern is general.
Any time you have a Rails action that does both of these things:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Broadcasts state via ActionCable (or any WebSocket/real-time channel)&lt;/li&gt;
  &lt;li&gt;Responds with a Turbo-managed redirect or page update&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;…you might have two competing update paths for the same state change. In the
normal case–one user, one action, low latency–they deliver the same data and
you never notice the duplication. But under pressure (rapid actions, multiple
clients, production latency), they interfere with each other.&lt;/p&gt;

&lt;p&gt;Here’s how I think about the interference now:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turbo owns the page lifecycle.&lt;/strong&gt; When Turbo manages a form submission, it
controls the fetch request, the response handling, the page replacement, and the
Stimulus controller lifecycle. If it decides to abort a request, disable a
button, or replace the page body, those decisions cascade through everything else
on the page–including your ActionCable subscriptions and Stimulus state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ActionCable assumes a stable connection.&lt;/strong&gt; Broadcasts are fire-and-forget from
the server’s perspective. If the client’s subscription is torn down (because
Turbo is replacing the page) or the client’s JavaScript is busy processing a
Turbo lifecycle event, the broadcast has no way to retry or confirm delivery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The combination creates asymmetry.&lt;/strong&gt; The device that triggered the action gets
special treatment from Turbo (re-render, fresh server state, Navigator
lifecycle management). Every other device gets only the ActionCable broadcast.
When Turbo’s lifecycle interferes with broadcast delivery or acceptance, the
devices diverge–and they diverge in ways that depend on network timing, making
the bug intermittent and hard to reproduce in development.&lt;/p&gt;

&lt;p&gt;The fix isn’t “use ActionCable better” or “use Turbo better.” It’s recognizing
that for actions whose results are broadcast to all clients in real time, &lt;strong&gt;Turbo
should not be managing the form submission at all.&lt;/strong&gt; Let the browser handle the
HTTP request. Let ActionCable handle the state update. One path, one source of
truth, no interference.&lt;/p&gt;

&lt;h3 id=&quot;signs-you-might-have-this-bug&quot;&gt;Signs You Might Have This Bug&lt;/h3&gt;

&lt;p&gt;If your app matches several of these, you’re probably vulnerable to this class
of problem:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;A Turbo-submitted form triggers an ActionCable broadcast.&lt;/strong&gt; This is the core
pattern. The form response and the broadcast are two delivery paths for the
same state change.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Multiple clients observe the same state.&lt;/strong&gt; If only the submitting user cares
about the result, the dual update paths are harmless–there’s no other client
to diverge from. The bug requires at least two clients.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Users perform rapid or repeated actions.&lt;/strong&gt; Single actions with seconds
between them are unlikely to trigger the race. The timing window opens when
actions overlap or arrive within a few hundred milliseconds of each other.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Your controller redirects after a successful mutation.&lt;/strong&gt; The
redirect-and-rerender cycle is what causes Stimulus disconnect/reconnect and
the ActionCable subscription gap. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;head :no_content&lt;/code&gt; is necessary but not
sufficient on its own–Turbo’s Navigator still manages the form submission.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;You’re using Solid Cable or another polling-based ActionCable adapter.&lt;/strong&gt;
Polling introduces delivery latency that widens the race window. The default
Solid Cable polling interval of 0.5 seconds means broadcasts can queue for up
to half a second before delivery. This isn’t the &lt;em&gt;cause&lt;/em&gt; of the bug–the core
vulnerability exists regardless of your ActionCable adapter–but polling
makes it more likely to manifest. Even with Redis-backed ActionCable, the race
window exists; it’s just narrower.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Your production environment has meaningful network latency.&lt;/strong&gt; This is what
makes the bug invisible in development. Local round-trips in single-digit
milliseconds don’t give the race enough time to trigger.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you recognize this pattern in your app, the fix is the same regardless of what
your feature does: add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo=&quot;false&quot;&lt;/code&gt; to forms whose successful actions
are broadcast via ActionCable, and return &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;head :no_content&lt;/code&gt; instead of
redirecting.&lt;/p&gt;

&lt;h3 id=&quot;coming-up&quot;&gt;Coming Up&lt;/h3&gt;

&lt;p&gt;Bypassing Turbo and eliminating the redirect fixed the primary race condition.
But during the investigation, I found several other issues that contributed to
the unreliability–a read-then-write race in the mutation method, a no-op
WebSocket disconnection handler that silently dropped broadcasts, and a Solid
Cable polling interval that was tuned for low DB load rather than real-time
responsiveness. I also discovered that fixing the Turbo lifecycle issue
inadvertently resolved a separate class of bugs–phantom sound effects triggered
by stale Stimulus controller state during page replacement–that I hadn’t
fully diagnosed yet.&lt;/p&gt;

&lt;p&gt;In the &lt;a href=&quot;/2026/02/01/real-time-rails-defense-in-depth.html&quot;&gt;next post&lt;/a&gt;, I’ll cover the defense-in-depth fixes that made the
real-time feature truly robust: pessimistic locking, WebSocket reconnect
recovery, and Solid Cable tuning.&lt;/p&gt;</content><author><name></name></author><category term="rails" /><category term="hotwire" /><category term="turbo" /><category term="actioncable" /><category term="software" /><summary type="html">Two phones, same app, same page containing a timer. One shows 4 minutes on the timer. The other shows 9. The server says 4. Refresh the lagging phone and it snaps to 4 instantly–which makes it maddening to debug, because the bug fixes itself the moment you look at it.</summary></entry><entry><title type="html">Today I Learned How to Keep Christmas Tree Lights Working Forever</title><link href="http://blog.edruder.com/2024/01/24/christmas-lights.html" rel="alternate" type="text/html" title="Today I Learned How to Keep Christmas Tree Lights Working Forever" /><published>2024-01-24T00:00:00-08:00</published><updated>2024-01-24T00:00:00-08:00</updated><id>http://blog.edruder.com/2024/01/24/christmas-lights</id><content type="html" xml:base="http://blog.edruder.com/2024/01/24/christmas-lights.html">&lt;p&gt;I have been decorating Christmas trees with &lt;strong&gt;incandescent&lt;/strong&gt; mini-lights almost
every year for many years. (&lt;a href=&quot;https://www.youtube.com/watch?v=OSOxeP-GF4Q&quot;&gt;LED lights are different.&lt;/a&gt;)
Over that time, I’ve bought, maintained, and thrown away dozens of strands of
lights. I’m cheap, so I always try to repair a strand when some lights go out,
and I generally keep strands working for years. Eventually though, each strand
gets to a point where I can’t get it working in a reasonable time, and I throw
it away and buy a new one.&lt;/p&gt;

&lt;p&gt;That is, until this year, when I learned how to keep Christmas tree lights going
virtually forever!&lt;/p&gt;

&lt;h3 id=&quot;get-a-lightkeeper-pro&quot;&gt;Get a LightKeeper Pro&lt;/h3&gt;

&lt;p&gt;A while ago, I discovered a great tool to keep tree lights working when a light
bulb or two goes out–the &lt;a href=&quot;https://lightkeeperpro.com/home/&quot;&gt;LightKeeper Pro&lt;/a&gt; (LKP)! It’s a
super-useful tool–I think that everyone who lights a Christmas tree every year
should have one. It has several functions, but the one I use the most is the
&lt;em&gt;quick fix&lt;/em&gt;–you plug your lights into the gun-shaped tool, pull the trigger
a few
times, and the section of your strand that was dark magically lights up!&lt;/p&gt;

&lt;p&gt;It’s worth mentioning that most Christmas tree strands are composed of several
sections of lights that are connected together to make a longer string of
lights. For example, a 150-light string is actually made up of three 50-light
sections connected end-to-end. (There are strings of lights that are composed of
35-light sections, but I don’t have any of those. These suggestions still
apply.) When a bulb in one of the sections blows out, it will only affect its
own section of lights. That’s why most of a long string of lights will remain
working when one section goes dark. When I say “strand”, I mean one of these
50-(or 35-)light sections of a longer string of lights.&lt;/p&gt;

&lt;h3 id=&quot;the-lightkeeper-pro-isnt-enough&quot;&gt;The LightKeeper Pro Isn’t Enough&lt;/h3&gt;

&lt;p&gt;Using the LKP quick-fix alone doesn’t keep your lights going forever though. I
had some tree lights that had a bulb or two burn out every year or two and I’d
use the LKP to get them working again. But then the entire strand of light bulbs
blew out–every single bulb was blackened on the inside!&lt;/p&gt;

&lt;p&gt;This year, as usual, the LKP fixed some of my lights before I put them up. After
I fixed them and put them up, we finished decorating the tree. The very next
day, a strand went out and the LKP didn’t help–all the bulbs were blown. Another
strand completely burned out the next day! While I’d seen this happen before
once or twice, this was the first time it happened to me after the tree was
completely decorated. We decided to live with the burned-out strands for the
duration of the season–removing the lights and ornaments, fixing the lights, 
then putting up the lights and ornaments &lt;em&gt;again&lt;/em&gt; would have been too much work.&lt;/p&gt;

&lt;p&gt;As I chewed on this frustration, I decided to figure out what caused this, and
it didn’t take long to find out.&lt;/p&gt;

&lt;h3 id=&quot;replace-all-burned-out-bulbs&quot;&gt;Replace All Burned Out Bulbs&lt;/h3&gt;

&lt;p&gt;The short answer is that every season, before you put up your lights, &lt;strong&gt;replace
all the burned out bulbs&lt;/strong&gt;. It turns out that as long as you start each season
with all of your mini light bulbs working, the LKP quick fix will keep you going
for the season.&lt;/p&gt;

&lt;p&gt;You usually get a few replacement bulbs with each new strand. When you run
through those, you can buy mini light bulbs in bulk online or at many hardware
stores. (The number of bulbs in a strand determines the voltage of the bulbs you
need to buy–2.5V for 50-bulb, 3.5V for 35-bulb strands–make sure to get the
right ones.)&lt;/p&gt;

&lt;h3 id=&quot;how-this-works&quot;&gt;How This Works&lt;/h3&gt;

&lt;p&gt;If you just want to keep your lights working, you can stop reading. If you’re
curious &lt;em&gt;why&lt;/em&gt; these suggestions work, here are the details.&lt;/p&gt;

&lt;h4 id=&quot;how-does-the-lightkeeper-pro-work&quot;&gt;How Does The LightKeeper Pro Work?&lt;/h4&gt;

&lt;p&gt;It’s not magic, of course. I learned that each bulb in a strand has a little
&lt;em&gt;shunt&lt;/em&gt; built into it. The shunt’s job is to keep the rest of the bulbs in the
strand lit even when its bulb burns out. Usually the shunt just works–when a
bulb burns out, the shunt kicks in and the rest of the bulbs continue to work.
Fairly frequently though, a bulb’s shunt doesn’t work right away, and the entire
strand goes dark. That’s where the LKP comes in–plugging the strand into the
tool and pulling the trigger generates a jolt of electricity that causes the
reluctant shunts to start working, and the strand lights up!&lt;/p&gt;

&lt;h4 id=&quot;why-replace-all-the-dead-bulbs&quot;&gt;Why Replace All The Dead Bulbs?&lt;/h4&gt;

&lt;p&gt;If you just keep clicking the LKP to get your strands working every year, more
and more bulbs in each strand will be burnt out. Not very noticeable, but each
strand is getting closer and closer to catastrophe. Here’s what’s happening.&lt;/p&gt;

&lt;p&gt;When a strand is new and every bulb is lit, there’s a certain amount of electric
current running through the strand. When one bulb burns out and its shunt kicks
in, the same amount of current is going through the strand, but it’s lighting
one fewer bulb. This causes each of the remaining bulbs to burn a &lt;em&gt;little&lt;/em&gt;
brighter. When another bulb burns out, the remaining bulbs burn a little &lt;em&gt;more&lt;/em&gt;
brightly. When a few more bulbs (5? or 6? &lt;em&gt;maybe&lt;/em&gt; more?) have blown out, the
remaining bulbs are running as hot as they can. The next bulb to die sets off a
cascade of blown bulbs until every one is burned out.&lt;/p&gt;

&lt;p&gt;A strand of lights can withstand a few dead bulbs, so just start the season off
with no dead bulbs and your season will be merry!&lt;/p&gt;

&lt;h3 id=&quot;the-lightkeeper-pro-is-even-better&quot;&gt;The LightKeeper Pro Is Even Better!&lt;/h3&gt;

&lt;p&gt;There are a few more ways that your strands can go dark and the LKP can help you
figure out the problem:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Every string of lights has a little fuse in the plug which can blow–the LKP
has a fuse tester.&lt;/li&gt;
  &lt;li&gt;Sometimes, the wires at the base of a bulb can break. The LKP has a voltage
detector that can tell you where the bad bulb is.&lt;/li&gt;
  &lt;li&gt;The LKP has a bulb tester, too.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://lightkeeperpro.com/home/&quot;&gt;LightKeeper Pro home page&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=EzbQCRkZKck&quot;&gt;How to fix Christmas lights video&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=OSOxeP-GF4Q&quot;&gt;How to fix LED Christmas lights video&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name></name></author><summary type="html">I have been decorating Christmas trees with incandescent mini-lights almost every year for many years. (LED lights are different.) Over that time, I’ve bought, maintained, and thrown away dozens of strands of lights. I’m cheap, so I always try to repair a strand when some lights go out, and I generally keep strands working for years. Eventually though, each strand gets to a point where I can’t get it working in a reasonable time, and I throw it away and buy a new one.</summary></entry><entry><title type="html">Easy Property files for Bash scripts</title><link href="http://blog.edruder.com/2020/09/13/easy-preferences-for-bash-scripts.html" rel="alternate" type="text/html" title="Easy Property files for Bash scripts" /><published>2020-09-13T00:00:00-07:00</published><updated>2020-09-13T00:00:00-07:00</updated><id>http://blog.edruder.com/2020/09/13/easy-preferences-for-bash-scripts</id><content type="html" xml:base="http://blog.edruder.com/2020/09/13/easy-preferences-for-bash-scripts.html">&lt;p&gt;Recently, I wanted to have a setting/preference to control an aspect of a Bash script. I considered
using an environment variable, but I wanted the preference to be able to be different based on the
directory that the script is run from—the “local” directory’s setting would override the
“global” setting.&lt;/p&gt;

&lt;p&gt;I’ve used a hierarchy of YAML files from Java—but YAML is overkill and I just needed two
levels of hierarchy. A simple “properties file”, with settings in the local file overriding the
settings in the global file in the home directory, works. A properties file is a text file,
usually with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.properties&lt;/code&gt; extension, with one property per line, and each line looking like
this: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;property=value&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Searching online, I found &lt;a href=&quot;https://fabianlee.org/2019/10/05/bash-setting-and-replacing-values-in-a-properties-file-use-sed/&quot;&gt;a simple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt; command&lt;/a&gt; that nicely plucks specific properties from
a file. With that, I wrote a simple Bash function that returns the value of a property from a
properties file, looking first in the properties file in the directory where the script was
invoked, then in the properties file in the home directory.&lt;/p&gt;

&lt;p&gt;Here’s the function:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;prop&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;

  &lt;span class=&quot;nv&quot;&gt;local_properties&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;/&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$properties_filename&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$local_properties&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-En&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/^&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$name&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;=([^&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;]+)&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/p&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$local_properties&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;fi

  if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;global_properties&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~/&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$properties_filename&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$global_properties&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
      &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-En&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/^&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$name&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;=([^&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;]+)&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/p&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$global_properties&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fi
  fi

  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The function expects the variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;properties_filename&lt;/code&gt; to be defined.&lt;/p&gt;

&lt;p&gt;It’s invoked like this:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;prop &lt;span class=&quot;s1&quot;&gt;'allow_something'&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;true&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# do something&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;This is on macOS, which has slightly different syntax for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt; options than is described in
  the original article (macOS: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-En&lt;/code&gt; vs. Linux: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-rn&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;The function interprets an “empty” value as “no property is defined”, so you probably don’t want
  to define a property without a value (i.e., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;property=&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;I usually make my preferences files invisible.&lt;/li&gt;
  &lt;li&gt;I usually don’t commit my preferences files to source control.&lt;/li&gt;
&lt;/ul&gt;</content><author><name></name></author><summary type="html">Recently, I wanted to have a setting/preference to control an aspect of a Bash script. I considered using an environment variable, but I wanted the preference to be able to be different based on the directory that the script is run from—the “local” directory’s setting would override the “global” setting.</summary></entry><entry><title type="html">Using a Bootstrap DateTime Picker in Simple Form and Rails 5.1</title><link href="http://blog.edruder.com/2018/01/27/bootstrap-date-time-picker-in-simple-form.html" rel="alternate" type="text/html" title="Using a Bootstrap DateTime Picker in Simple Form and Rails 5.1" /><published>2018-01-27T00:00:00-08:00</published><updated>2018-01-27T00:00:00-08:00</updated><id>http://blog.edruder.com/2018/01/27/bootstrap-date-time-picker-in-simple-form</id><content type="html" xml:base="http://blog.edruder.com/2018/01/27/bootstrap-date-time-picker-in-simple-form.html">&lt;p&gt;I’m using &lt;a href=&quot;https://getbootstrap.com/&quot;&gt;Bootstrap&lt;/a&gt; to make the pages of my Rails app look halfway
decent–I’m a programmer who’s creatively challenged. (I can already feel the
scorn from my designer friends, but hey–I actually like how Bootstrap looks!)
I’m also using &lt;a href=&quot;https://github.com/plataformatec/simple_form&quot;&gt;Simple Form&lt;/a&gt; to clean up the HTML forms in my app.&lt;/p&gt;

&lt;p&gt;In one of my forms, the user needs to choose a date &amp;amp; time, and the default way
to enter a date &amp;amp; time in Rails is horrendously ugly, even to my soulless, dead
eyes. A little Googling for ‘bootstrap datetime picker rails’ landed me on the
&lt;a href=&quot;https://github.com/TrevorS/bootstrap3-datetimepicker-rails&quot;&gt;bootstrap3-datetimepicker-rails&lt;/a&gt; gem page. This gem
just wraps the &lt;a href=&quot;https://eonasdan.github.io/bootstrap-datetimepicker/&quot;&gt;bootstrap-datetime-picker&lt;/a&gt;, whose minimal
setup looked perfectly suitable to me. Piece of cake, I thought!&lt;/p&gt;

&lt;p&gt;It was straightforward to install and configure the gem using the instructions
on &lt;a href=&quot;https://github.com/TrevorS/bootstrap3-datetimepicker-rails&quot;&gt;its home page&lt;/a&gt;. The hard part was trying to figure
out how to use the picker in my Simple Form form.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap-datetime-picker&lt;/code&gt; &lt;a href=&quot;https://eonasdan.github.io/bootstrap-datetimepicker/&quot;&gt;home page&lt;/a&gt; describes the
markup that renders the picker. From the top of that page, this is what I
wanted Simple Form to output:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-group&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input-group date&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;datetimepicker1&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-control&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input-group-addon&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;glyphicon glyphicon-calendar&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(There is some markup wrapping this, but this is the part that is important.)&lt;/p&gt;

&lt;p&gt;I read the (very complete!) &lt;a href=&quot;https://github.com/plataformatec/simple_form&quot;&gt;Simple Form documentation&lt;/a&gt;, but I
didn’t see how to specify a stock Simple Form field that would result in
something resembling the markup above. I read several of the &lt;a href=&quot;https://github.com/plataformatec/simple_form/wiki/Adding-custom-components&quot;&gt;Simple
Form&lt;/a&gt; &lt;a href=&quot;https://github.com/plataformatec/simple_form/wiki/Adding-custom-input-components&quot;&gt;wiki&lt;/a&gt; &lt;a href=&quot;https://github.com/plataformatec/simple_form/wiki/Bootstrap-component-helpers&quot;&gt;pages&lt;/a&gt; about
building custom input components, and I decided that that’s what I needed to
create to get the markup that would render the datetime picker properly.&lt;/p&gt;

&lt;p&gt;Unfortunately, that documentation didn’t get me to the point where I knew how
to write the custom component, but with that and &lt;a href=&quot;http://arjanvandergaag.nl/blog/simpleform-custom-inputs.html&quot;&gt;several&lt;/a&gt;
&lt;a href=&quot;https://jeremysmith.co/posts/2015-12-09-custom-currency-input-for-simple-form/&quot;&gt;other&lt;/a&gt; &lt;a href=&quot;https://www.foraker.com/blog/create-reusable-custom-simple-form-inputs&quot;&gt;examples&lt;/a&gt; I found online, my mental
picture of what I needed to do became clearer. I didn’t find a good
soup-to-nuts description of how to build a custom input component, starting
with the markup you want, but I figured I could dive in and figure it out.&lt;/p&gt;

&lt;h3 id=&quot;a-solution&quot;&gt;A Solution&lt;/h3&gt;

&lt;h4 id=&quot;first-the-markup&quot;&gt;First, the Markup&lt;/h4&gt;

&lt;p&gt;I have the markup I’m aiming for (above). Here’s the Simple Form field that I
think should create it (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;published_at&lt;/code&gt; is the Active Record datetime field
to be created/edited):&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%= f.input :published_at, as: :date_time_picker %&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I started with a shell of a custom input component, from the &lt;a href=&quot;https://github.com/plataformatec/simple_form#custom-inputs&quot;&gt;Custom inputs
section&lt;/a&gt; of the Simple Form documentation. If you put
the file in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/inputs&lt;/code&gt; directory (which is Simple Form-specific–you’ll
probably need to create it) and restart your server, Simple Form will pick it
up:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/inputs/date_time_picker_input.rb&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DateTimePickerInput&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SimpleForm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Inputs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;merged_input_options&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merge_wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input_html_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;vi&quot;&gt;@builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attribute_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merged_input_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Looking at the markup I’m shooting for, I’m going to need a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text&lt;/code&gt; input, and
I’m guessing that’s what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@builder.text_field&lt;/code&gt; is going to create, so let’s
just try it out! When I restart my server and render a form using a this custom
input component (using exactly the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;f.input&lt;/code&gt; line, above), this is the HTML
that’s rendered:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-group date_time_picker required content_published_at&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;label&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;control-label date_time_picker required&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;for=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content_published_at&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    Published at
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-control date_time_picker required&quot;&lt;/span&gt;
         &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt;
         &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;2018-01-01 23:38:00 UTC&quot;&lt;/span&gt;
         &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content[published_at]&quot;&lt;/span&gt;
         &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content_published_at&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Not a bad start! It looks like Simple Form will generate a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;label&lt;/code&gt; by default,
and I don’t want one. Poking around in the documentation and examples, I see
that there is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;label&lt;/code&gt; method that I can define to customize the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;label&lt;/code&gt; tag
of this component. What if I define one that returns nothing?&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/inputs/date_time_picker_input.rb&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DateTimePickerInput&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SimpleForm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Inputs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;merged_input_options&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merge_wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input_html_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;vi&quot;&gt;@builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attribute_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merged_input_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And its output:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-group date_time_picker required content_published_at&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-control date_time_picker required&quot;&lt;/span&gt;
         &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt;
         &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;2018-01-01 23:38:00 UTC&quot;&lt;/span&gt;
         &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content[published_at]&quot;&lt;/span&gt;
         &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content_published_at&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Bingo! (The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_&lt;/code&gt; parameter is there because the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;label&lt;/code&gt; method requires one
parameter, but we aren’t using it and don’t care about it. This is a Ruby
convention that’s pretty handy.) Moving on…&lt;/p&gt;

&lt;p&gt;I see that the outer &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;div&lt;/code&gt; with a class of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form-group&lt;/code&gt; is already there
without me having to do anything. We can ignore the other classes on that outer
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;div&lt;/code&gt;, but you can see the name of our component there, the model name/field
name (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content_published_at&lt;/code&gt;) that Rails wants, etc.&lt;/p&gt;

&lt;p&gt;Comparing the HTML to the exemplar, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; needs to be wrapped in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;div&lt;/code&gt;.
From some of the examples I saw in the documentation and elsewhere, I can wrap
a div around the input using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;template.content_tag(:div)&lt;/code&gt;. Let’s give that a
shot in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; method:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/inputs/date_time_picker_input.rb&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;merged_input_options&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merge_wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input_html_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'input-group date'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'datetimepicker1'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attribute_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merged_input_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s the HTML that’s generated:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-group date_time_picker required content_published_at&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input-group date&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;datetimepicker1&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-control date_time_picker required&quot;&lt;/span&gt;
           &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt;
           &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;2018-01-01 23:38:00 UTC&quot;&lt;/span&gt;
           &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content[published_at]&quot;&lt;/span&gt;
           &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content_published_at&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Closer!&lt;/p&gt;

&lt;p&gt;Thinking about this for a second, the custom component should not be assigning
an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; to that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;div&lt;/code&gt;–this is a general purpose component. We’ll figure out
how to set the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; later, if we need it–for now, I’m going to delete the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now I need to create the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;span&lt;/code&gt; that follows the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt;. And that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;span&lt;/code&gt; needs
to contain a nested &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;span&lt;/code&gt; inside of it. Hmm–that’s a pattern we’ve seen
before. The nested &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;span&lt;/code&gt; can be created like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:span&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'input-group-addon'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:span&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'glyphicon glyphicon-calendar'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Adding that into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; method, we have:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/inputs/date_time_picker_input.rb&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;merged_input_options&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merge_wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input_html_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'input-group date'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attribute_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merged_input_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:span&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'input-group-addon'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:span&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'glyphicon glyphicon-calendar'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When we run this, we get this HTML:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-group date_time_picker required content_published_at&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input-group date&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input-group-addon&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;glyphicon glyphicon-calendar&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Wait! What happened to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Well, the way these tag builder blocks work is that the “return value” of the
block is what’s wrapped by the tag. In this case, the return value of the block
is whatever the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;template.content_tag(:span)&lt;/code&gt; builder returns, which is the
nested spans that we see. The output of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@builder.text_field()&lt;/code&gt; call fell
on the floor and wasn’t used at all.&lt;/p&gt;

&lt;p&gt;The return value of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;template.content_tag(:div, class: 'input-group date')&lt;/code&gt;
builder block needs to be contain both the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;span&lt;/code&gt;, in that order.
The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;span&lt;/code&gt; builders just return strings, so we can simply
concatenate them! Here’s a simple way to do that:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/inputs/date_time_picker_input.rb&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;merged_input_options&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merge_wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input_html_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapper_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'input-group date'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attribute_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merged_input_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;span&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:span&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'input-group-addon'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:span&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'glyphicon glyphicon-calendar'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;span&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;html_safe&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And the HTML:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-group date_time_picker required content_published_at&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input-group date&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-control date_time_picker required&quot;&lt;/span&gt;
           &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt;
           &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;2018-01-01 23:38:00 UTC&quot;&lt;/span&gt;
           &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content[published_at]&quot;&lt;/span&gt;
           &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content_published_at&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;input-group-addon&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;glyphicon glyphicon-calendar&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Awesome!&lt;/p&gt;

&lt;p&gt;Because we haven’t hooked up the JavaScript part of this component, it doesn’t
do anything, yet. Let’s do that now.&lt;/p&gt;

&lt;h4 id=&quot;the-javascript&quot;&gt;The JavaScript&lt;/h4&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap-datetime-picker&lt;/code&gt; &lt;a href=&quot;https://eonasdan.github.io/bootstrap-datetimepicker/&quot;&gt;home page&lt;/a&gt; describes the
simple JavaScript that hooks up and gives life to the picker. From near the top
of that page, this is all that it takes:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#datetimepicker1&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;datetimepicker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This JavaScript uses &lt;a href=&quot;https://jquery.com/&quot;&gt;jQuery&lt;/a&gt; to call the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;datetimepicker&lt;/code&gt; function
(provided by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap-datetime-picker&lt;/code&gt;, which is already installed) on the
DOM element whose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;datetimepicker1&lt;/code&gt;. Whoops–I deleted that!&lt;/p&gt;

&lt;p&gt;Our component doesn’t have an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt;, but does it need one? Let’s say we had a
form that had two or more of these pickers in it–we’d want all of them to be
turned on. The HTML of our component has plenty of identifying classes attached
to various elements of it–we can find all of the picker components in the DOM
using those classes.&lt;/p&gt;

&lt;p&gt;A slight change to the JavaScript will enable all of the pickers in the DOM:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.date_time_picker &amp;gt; .input-group.date&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;datetimepicker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This generic JavaScript is written this way so that the code inside of the
anonymous function gets called when the page has finished loading. (Exactly how
that works is beyond the scope of this post, but a little Googling around
should find tons of articles about it.) Rails 5.1 has a different way to run
JavaScript when the page is loaded (the details of which are also beyond the
scope of this post):&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;turbolinks:load&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.date_time_picker &amp;gt; .input-group.date&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;datetimepicker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s the equivalent CoffeeScript (which is the default flavor of JavaScript
as of Rails 5.1):&lt;/p&gt;

&lt;div class=&quot;language-coffeescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;turbolinks:load&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'.date_time_picker &amp;gt; .input-group.date'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;datetimepicker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With that code added to your page’s JavaScript (I’m not going to explain how to
do that, but there’s lots of Rails documentation and tutorials online), the
picker component works! If you’ve been following along, hopefully you see something
like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://blog.edruder.com/assets/images/picker.png&quot; alt=&quot;picker&quot; /&gt;&lt;/p&gt;

&lt;p&gt;When I started using this picker, it kind of worked, but had some problems.&lt;/p&gt;

&lt;p&gt;The first was that when it was used to edit an existing value, the picker would
start out blank, instead of populated with the value. When I watched closely, I
saw that the right value flickered quickly in the picker, but was erased almost
immediately. For some reason, the value &lt;strong&gt;does&lt;/strong&gt; populate the picker initially,
but when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;datetimepicker()&lt;/code&gt; function is called, it gets erased.&lt;/p&gt;

&lt;p&gt;My quick fix to that problem is to modify the JavaScript to grab the value of
the picker, call the initializer, then restore the value. Seems to work.
(There’s probably a better way to do this. Let me know!)&lt;/p&gt;

&lt;p&gt;The next problem I saw was that the date that was saved in the Active Record
datetime field was a little funky–sometimes it was just a little wrong,
sometimes it wasn’t stored at all. Turns out that the format of the date/time
string has to match the format that the Rails database expects it. It will try
to parse the string using its format, and sometimes it will be just a little
wrong, and other times it would be an illegal date, and nothing gets stored!&lt;/p&gt;

&lt;p&gt;The way I chose to fix this was to change the format of the string that’s
returned by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap-datetime-picker&lt;/code&gt; to match the format that my
database (MySQL) expects. (I think the format that MySQL expects–&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;YYYY-MM-DD
HH:mm:ss&lt;/code&gt;–is fairly common, if not ubiquitous, among databases.)&lt;/p&gt;

&lt;p&gt;Here’s the final JavaScript that works well for me:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;turbolinks:load&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$pickerInput&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.date_time_picker input.date_time_picker&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;initialValue&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$pickerInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;.date_time_picker &amp;gt; .input-group.date&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;datetimepicker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;YYYY-MM-DD HH:mm:ss&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$pickerInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;initialValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s the equivalent CoffeeScript:&lt;/p&gt;

&lt;div class=&quot;language-coffeescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;turbolinks:load&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$pickerInput&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'.date_time_picker input.date_time_picker'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;initialValue&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$pickerInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'.date_time_picker &amp;gt; .input-group.date'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;datetimepicker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'YYYY-MM-DD HH:mm:ss'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$pickerInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;initialValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Please leave a note if you have questions, answers, suggestions, or a better
solution!&lt;/p&gt;</content><author><name></name></author><category term="rails" /><category term="bootstrap" /><category term="simple_form" /><category term="software" /><summary type="html">I’m using Bootstrap to make the pages of my Rails app look halfway decent–I’m a programmer who’s creatively challenged. (I can already feel the scorn from my designer friends, but hey–I actually like how Bootstrap looks!) I’m also using Simple Form to clean up the HTML forms in my app.</summary></entry><entry><title type="html">Rendering Dynamic Markdown in Rails</title><link href="http://blog.edruder.com/2018/01/27/rendering-dynamic-markdown.html" rel="alternate" type="text/html" title="Rendering Dynamic Markdown in Rails" /><published>2018-01-27T00:00:00-08:00</published><updated>2018-01-27T00:00:00-08:00</updated><id>http://blog.edruder.com/2018/01/27/rendering-dynamic-markdown</id><content type="html" xml:base="http://blog.edruder.com/2018/01/27/rendering-dynamic-markdown.html">&lt;p&gt;I wrote about how I enabled Markdown views in my Rails 5 app
&lt;a href=&quot;/2017/12/19/add-markdown-to-rails-5.html&quot;&gt;previously&lt;/a&gt;. I wanted to add the ability to create and edit
Markdown-formatted articles, posts, etc. in this app, and it turned out to be
pretty easy.&lt;/p&gt;

&lt;p&gt;The Markdown that I want to render will be created dynamically by the users of
my app and stored in the Rails database, as opposed to being in static &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.md&lt;/code&gt;
files somewhere in my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;views&lt;/code&gt; directory. I already had the
&lt;a href=&quot;https://github.com/vmg/redcarpet&quot;&gt;redcarpet&lt;/a&gt; gem installed and configured in my app–I just needed to
figure out how to use it to render arbitrary Markdown in a view.&lt;/p&gt;

&lt;p&gt;My Markdown handler had changed slightly from its initial incarnation–I had
tweaked the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Redcarpet&lt;/code&gt; settings a little and done a tiny bit of refactoring.
Here’s what I started with:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/initializers/markdown.rb&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'redcarpet'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ActionView&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Template::Handlers&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Markdown&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;class_attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:default_format&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;default_format&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Mime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;compiled_source&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;erb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.render(begin;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;compiled_source&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;;end)&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;html_safe&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;kp&quot;&gt;private&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;extensions&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@md_options&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;autolink: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;fenced_code_blocks: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;highlight: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;quotes: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;strikethrough: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;tables: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;underline: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;markdown&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@markdown&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Redcarpet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;renderer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extensions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;renderer_options&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@renderer_options&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;filter_html: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;hard_wrap: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;renderer&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@renderer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;HTMLWithPants&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;renderer_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;erb&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@erb&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActionView&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;registered_template_handler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:erb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HTMLWithPants&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Redcarpet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Render&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;HTML&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Redcarpet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Render&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SmartyPants&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;ActionView&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;register_template_handler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:md&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActionView&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Handlers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice that all of the Markdown stuff is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;private&lt;/code&gt;–a Rails template handler
just needs to respond to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:call&lt;/code&gt;–it doesn’t need to expose its implementation
details (and shouldn’t). Because of what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;call&lt;/code&gt; returns in this particular
handler–a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;String&lt;/code&gt; that gets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eval&lt;/code&gt;ed somewhere in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActionView&lt;/code&gt;, and the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eval&lt;/code&gt;ed code calls the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt; method–the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt; method needs to be
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;public&lt;/code&gt;, too.&lt;/p&gt;

&lt;p&gt;What I want to end up with is an application-wide &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;markdown(text)&lt;/code&gt; method that
accepts Markdown-formatted text and returns HTML that can be displayed by any
view. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Redcarpet::Markdown&lt;/code&gt; object has a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render(text)&lt;/code&gt; method that’s just
what we need–I just need to make that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;markdown&lt;/code&gt; method in my handler &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;public&lt;/code&gt;
and use it in the helper! The result:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/initializers/markdown.rb&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# moved into the public section&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;markdown&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@markdown&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Redcarpet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;renderer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extensions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;kp&quot;&gt;private&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# app/helpers/application_helper.rb&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;ActionView&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Handlers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;html_safe&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Not the prettiest line of Ruby code (and it violates the &lt;a href=&quot;https://en.wikipedia.org/wiki/Law_of_Demeter&quot;&gt;Law of
Demeter&lt;/a&gt;), but it works–&lt;strong&gt;and&lt;/strong&gt; it uses the
already-instantiated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Redcarpet::Markdown&lt;/code&gt; object. I can pretty it up, later.&lt;/p&gt;

&lt;p&gt;If you want to render the Markdown from your users differently than the
Markdown in your own views, you might want to instantiate (and cache) a
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Redcarpet::Markdown&lt;/code&gt; object with different settings. E.g., you might want to
be more strict about what is allowed in the Markdown that your users can enter,
compared to what you allow in your own views.&lt;/p&gt;

&lt;p&gt;That &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.html_safe&lt;/code&gt; is important–without it, the HTML generated from the
Markdown will be “escaped” by Rails, displaying a bunch of ugly, raw HTML in
your view.&lt;/p&gt;

&lt;p&gt;With this helper, you’re able to put a call to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;markdown&lt;/code&gt; in any views that you
like! For example, I have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ContentsController&lt;/code&gt; whose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;show&lt;/code&gt; method reads a
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Content&lt;/code&gt; record from the database into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@content&lt;/code&gt; variable. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Content&lt;/code&gt;
model has a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;body&lt;/code&gt; field containing Markdown formatted text, so in the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;views/contents/show.html.erb&lt;/code&gt; file, there’s a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;markdown(@content.body)&lt;/code&gt; call
that inserts beautiful HTML from the Markdown text that the user entered!&lt;/p&gt;

&lt;p&gt;Please leave a note if you have questions or suggestions.&lt;/p&gt;</content><author><name></name></author><category term="rails" /><category term="markdown" /><category term="software" /><summary type="html">I wrote about how I enabled Markdown views in my Rails 5 app previously. I wanted to add the ability to create and edit Markdown-formatted articles, posts, etc. in this app, and it turned out to be pretty easy.</summary></entry><entry><title type="html">Adding support for Markdown views to Rails 5</title><link href="http://blog.edruder.com/2017/12/19/add-markdown-to-rails-5.html" rel="alternate" type="text/html" title="Adding support for Markdown views to Rails 5" /><published>2017-12-19T00:00:00-08:00</published><updated>2017-12-19T00:00:00-08:00</updated><id>http://blog.edruder.com/2017/12/19/add-markdown-to-rails-5</id><content type="html" xml:base="http://blog.edruder.com/2017/12/19/add-markdown-to-rails-5.html">&lt;p&gt;I thought that adding &lt;a href=&quot;https://daringfireball.net/projects/markdown/syntax&quot;&gt;Markdown&lt;/a&gt; support to a Ruby on Rails 5 project
would be a straightforward thing.  I’ve read a bunch of blogs over the years
that described various ways to use Markdown in Rails apps. There are many
Markdown gems–&lt;a href=&quot;https://github.com/vmg/redcarpet&quot;&gt;redcarpet&lt;/a&gt;, &lt;a href=&quot;https://github.com/gettalong/kramdown&quot;&gt;kramdown&lt;/a&gt;,
&lt;a href=&quot;https://github.com/davidfstr/rdiscount&quot;&gt;rdiscount&lt;/a&gt;, to name a few. And there are several ways to use
Markdown–from allowing a content management system to author new posts in
Markdown to supporting Markdown-formatted views in a Rails app itself. I was
interested in the latter, and expected it to be pretty easy.&lt;/p&gt;

&lt;p&gt;Wrong.&lt;/p&gt;

&lt;p&gt;My first road block was conceptual. I’ve been using Rails for a long time, off
and on, but haven’t used it consistently since pre-Rails 3. My recollection,
which may be just wrong, is that at some point, Rails’ view rendering allowed
chaining of several template renderers–if you named your view something like,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;posts.html.md.erb&lt;/code&gt;, and you had the handlers installed, the view source would
first be handled by the &lt;a href=&quot;http://www.stuartellis.name/articles/erb/&quot;&gt;ERB&lt;/a&gt; handler, then the Markdown handler, then
Rails’ standard HTML view processing.  Turns out, this is not the way that
Rails’ template handlers work (and they may never have worked that way).&lt;/p&gt;

&lt;p&gt;As far as I can figure out, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActionView&lt;/code&gt; template handlers aren’t chainable–
they are intended to convert from one type of template (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ERB&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Markdown&lt;/code&gt; or
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Haml&lt;/code&gt;, say) into the final &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MIME&lt;/code&gt; type that your template is targeting (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HTML&lt;/code&gt;
or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JSON&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;XML&lt;/code&gt;, for example), and no more. If you want to create views in
Markdown that can have ERB inside of them, then your Markdown handler needs to
know how to handle ERB itself.&lt;/p&gt;

&lt;p&gt;I searched all over the Web and found several helpful blog posts, but none that
did &lt;strong&gt;exactly&lt;/strong&gt; what I wanted (&lt;a href=&quot;https://gist.github.com/davidjrice/3014948&quot;&gt;a&lt;/a&gt; &lt;a href=&quot;http://lugolabs.com/articles/18-render-markdown-views-with-redcarpet-and-pygment-in-rails&quot;&gt;few&lt;/a&gt; came pretty
close, though).  The Rails Guides don’t discuss how to create your own template
handlers, and even the Rails source wasn’t much help to me. I wanted a clean
initializer and I wanted to enable ERB in the Markdown templates.&lt;/p&gt;

&lt;h3 id=&quot;a-solution&quot;&gt;A Solution&lt;/h3&gt;

&lt;p&gt;I chose to use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redcarpet&lt;/code&gt; Markdown gem because it’s fast, feature-rich,
and well-maintained. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redcarpet&lt;/code&gt; needs to be available in all of the Rails
app’s environments, so don’t put it into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt;-only stanza of your
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt;!  (Been there, done that.)&lt;/p&gt;

&lt;p&gt;I like to use TDD (though I’m not super-consistent about it), and it’s easy to
create a test for this–just create a view in Markdown and verify that the
HTML that’s rendered contains the right tag(s)! I’m using RSpec, and my test
looks like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# spec/views/welcome/about.html.md_spec.rb&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'rails_helper'&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;RSpec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;describe&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'welcome/about.html.md'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'renders Markdown'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rendered&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;%r{&amp;lt;h3&amp;gt;About My Website&amp;lt;/h3&amp;gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It will fail, of course, because there is no such view.&lt;/p&gt;

&lt;p&gt;Then, create the Markdown view:&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/welcome/about.html.md --&amp;gt;&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;### About My Website&lt;/span&gt;

It's really cool!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The spec will still fail, but in a different way, since now the template is
asking for a handler that doesn’t exist.&lt;/p&gt;

&lt;p&gt;Finally, the Markdown handler!&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/initializers/redcarpet.rb&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'redcarpet'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ActionView&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Template::Handlers&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Markdown&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;class_attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:default_format&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;default_format&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Mime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;compiled_source&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;erb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.render(begin;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;compiled_source&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;;end)&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;html_safe&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;kp&quot;&gt;private&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;md_options&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@md_options&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;autolink: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;fenced_code_blocks: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;strikethrough: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;tables: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;markdown&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@markdown&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Redcarpet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;HTMLWithPants&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;md_options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;erb&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@erb&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActionView&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;registered_template_handler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:erb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HTMLWithPants&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Redcarpet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Render&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;HTML&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Redcarpet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Render&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SmartyPants&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;ActionView&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;register_template_handler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:md&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActionView&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Handlers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A little explanation, starting from the bottom.
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActionView::Template.register_template_handler&lt;/code&gt; is the method that registers a
new template handler. It expects the filename extension that the handler
applies to (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:md&lt;/code&gt;, for files that end in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.md&lt;/code&gt;), and an object that implements
a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;call&lt;/code&gt; method–in our case, the class that we defined,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActionView::Template::Handlers::Markdown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;class &amp;lt;&amp;lt; self&lt;/code&gt; block wraps the class methods, of which only two are
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;public&lt;/code&gt;–&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;call&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt;. (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt; only needs to be public because it’s
going to be called by Rails, which we’ll see in a second.) The magic happens in
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;call&lt;/code&gt; class method–the class itself implements the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;call&lt;/code&gt; method that
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;register_template_handler&lt;/code&gt; requires.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;call&lt;/code&gt; method needs to return a &lt;strong&gt;string&lt;/strong&gt; containing the &lt;em&gt;code&lt;/em&gt; that needs
to be executed by Rails to generate HTML (in this template handler, because of
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.default_format = Mime[:html]&lt;/code&gt; line). (There isn’t much, if any,
documentation on this–I got this from the articles I found online.)&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compiled_source = erb.call(template)&lt;/code&gt; line is what handles ERB in the
Markdown template. The string it returns is then handled by the Markdown
renderer that’s in our code.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;#{name}.render(begin;#{compiled_source};end)&quot;&lt;/code&gt; is the return value of the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;call&lt;/code&gt; method–that’s the string of code that Rails will eventually evaluate
and run. Because the code will be run in a Rails context, it needs to
fully-qualify the method to be run. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; method returns the full name of
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self&lt;/code&gt;, which is the class, so it returns
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActionView::Template::Handlers::Markdown&lt;/code&gt;.  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#{name}.render&lt;/code&gt;, then,
fully-qualifies our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;The parameter to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt;–&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;begin;#{compiled_source};end&lt;/code&gt;–is also mysterious.
It wraps a Ruby block around the string that the ERB handler returned (that is
also Ruby code), which makes it digestible to the Markdown &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;I’m still a little fuzzy on why the code that invokes the handler needs to be
passed around as a Ruby string, but this solution works nicely for me!&lt;/p&gt;

&lt;p&gt;Please leave a note if you have questions, suggestions, or a better
explanation.&lt;/p&gt;</content><author><name></name></author><category term="rails" /><category term="markdown" /><category term="software" /><summary type="html">I thought that adding Markdown support to a Ruby on Rails 5 project would be a straightforward thing. I’ve read a bunch of blogs over the years that described various ways to use Markdown in Rails apps. There are many Markdown gems–redcarpet, kramdown, rdiscount, to name a few. And there are several ways to use Markdown–from allowing a content management system to author new posts in Markdown to supporting Markdown-formatted views in a Rails app itself. I was interested in the latter, and expected it to be pretty easy.</summary></entry><entry><title type="html">Why can’t Smart Routers be Really Smart?</title><link href="http://blog.edruder.com/2017/01/01/why-cant-smart-routers-be-smart.html" rel="alternate" type="text/html" title="Why can’t Smart Routers be Really Smart?" /><published>2017-01-01T00:00:00-08:00</published><updated>2017-01-01T00:00:00-08:00</updated><id>http://blog.edruder.com/2017/01/01/why-cant-smart-routers-be-smart</id><content type="html" xml:base="http://blog.edruder.com/2017/01/01/why-cant-smart-routers-be-smart.html">&lt;p&gt;In 2016 the media reported on &lt;a href=&quot;https://www.tripwire.com/state-of-security/security-data-protection/cyber-security/5-significant-ddos-attacks-2016/&quot;&gt;a number of Distributed Denial of Service (DDoS)
attacks&lt;/a&gt; that significantly disrupted aspects of the
Internet, wreaking havoc that ranged from minor inconveniences for many, to
serious financial losses for some. The DDoS attacks were caused by
&lt;a href=&quot;https://www.eset.com/us/about/newsroom/corporate-blog/ddos-attacks-explained/&quot;&gt;botnets&lt;/a&gt;, which are composed of large numbers of unsecured,
hacked devices that are connected to the Internet. Attackers take advantage of
numerous well-known and lesser-known vulnerabilities in a multitude of
connected computers, routers, surveillance cameras, and other devices. When
they find a device they can hack into, they take control of the device for
their own, often destructive, purposes.&lt;/p&gt;

&lt;p&gt;Sales of Internet-connected devices (a.k.a., the &lt;a href=&quot;https://en.wikipedia.org/wiki/Internet_of_things&quot;&gt;Internet of Things&lt;/a&gt; or
IoT) are exploding, and there are no signs that the trend is slowing. On the
contrary, more and more connected devices are being introduced and sold into
homes and businesses because they are so useful and/or cool–they are here to
stay. Unfortunately, we haven’t yet figured out how to protect these devices
from attackers, or how to protect ourselves from hacked devices.&lt;/p&gt;

&lt;p&gt;There are thousands of vulnerable products in millions of homes and businesses,
and each of them need to be fixed by its manufacturer. That will be a big,
distributed effort that will take a long time. Until that happens, how can we
prevent attackers from finding and hacking vulnerable Internet-connected
devices?&lt;/p&gt;

&lt;p&gt;All of the vulnerable devices have one thing in common–they connect to the
Internet via a relatively inexpensive, sophisticated, commercial
&lt;a href=&quot;https://en.wikipedia.org/wiki/Router_(computing)&quot;&gt;router&lt;/a&gt;. Every household and most businesses have one router through
which 100% of their Internet traffic flows–you can think of it as an Internet
gatekeeper. Sometimes that router is built into the modem that is provided by
their &lt;a href=&quot;https://en.wikipedia.org/wiki/Internet_service_provider&quot;&gt;Internet Service Provider&lt;/a&gt; (ISP), sometimes it’s a separate box
connected to the ISP’s modem. Routers are quite complicated–they often have
built-in &lt;a href=&quot;https://en.wikipedia.org/wiki/Firewall_(computing)&quot;&gt;firewalls&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/Virtual_private_network&quot;&gt;VPNs&lt;/a&gt; for security, logic to
intelligently route traffic to the devices “behind” them and much more.&lt;/p&gt;

&lt;p&gt;My question is, why hasn’t one or more of the very sophisticated companies that
make and sell routers–Cisco, D-Link, Netgear, &lt;strike&gt;Apple&lt;/strike&gt;, Google,
etc.–made an inexpensive commercial router that’s &lt;strong&gt;really&lt;/strong&gt; smart? A Really
Smart Router (RSR) would have at least these characteristics:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;An RSR makes sure that it has a strong password.&lt;/li&gt;
  &lt;li&gt;An RSR itself is very well protected.&lt;/li&gt;
  &lt;li&gt;An RSR checks the devices connected to it for known vulnerabilities.&lt;/li&gt;
  &lt;li&gt;An RSR automatically and securely keeps its software up-to-date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these features probably needs to be able to be overridden by their
owner, for reasons they think are strong enough to reduce the security of their
network, but they should be turned on by default.&lt;/p&gt;

&lt;h3 id=&quot;strong-password&quot;&gt;Strong Password&lt;/h3&gt;

&lt;p&gt;Almost all routers have a simple/weak password assigned when they are brand
new–it’s printed in the router’s manual so that new owners can easily set up
their new router. The manual usually advises the new owner IN ALL CAPS to
change the password to something stronger, but leaves it at that. People being
people, many new router owners never change their router’s password, making it
an easy target for attackers.&lt;/p&gt;

&lt;p&gt;An RSR would refuse to work until its password was set to something hard for an
attacker to figure out. “Hard to figure out” can be satisfied a number of ways.
Maybe the password can’t be too short, so that it can’t be guessed quickly.
Maybe the password can’t be in a dictionary of common words or have any form of
the initial simple password in it. Maybe an RSR doesn’t allow login attempts to
happen quickly, so that attackers can’t “brute force” guess thousands or
millions of passwords in a very short time.&lt;/p&gt;

&lt;h3 id=&quot;well-protected&quot;&gt;Well Protected&lt;/h3&gt;

&lt;p&gt;Once protected by a strong password, I think that most routers are pretty
resistant to exploitation by attackers. However, I’m sure that more than a few
have known vulnerabilities that haven’t been fixed, or even have “back
doors”–ways into the router that bypass the password check that are
intentionally built into the router.&lt;/p&gt;

&lt;p&gt;An RSR would be a fortress–it would have no back doors, its software would be
aggressively tested for vulnerabilities, and any found would be fixed quickly.&lt;/p&gt;

&lt;h3 id=&quot;checks-connected-devices&quot;&gt;Checks Connected Devices&lt;/h3&gt;

&lt;p&gt;This is a biggie. Many security experts and companies are well aware of almost
all of the vulnerabilities that attackers use to create botnets. The experts
know what the devices “look like”; they know how to exploit their
vulnerabilities.&lt;/p&gt;

&lt;p&gt;On some frequency–once a day, once a week, when it detects a new device–a
Really Smart Router checks &lt;strong&gt;every&lt;/strong&gt; device connected to it against a
comprehensive list of known vulnerabilities. If it finds a vulnerable device,
it informs the owner and disconnects the device from the Internet completely.
The owner would receive enough information to know what to do next–which
device is the problem, what the problem is, and how to fix it. (Some devices
are not easily updatable–they may need to be recycled.)&lt;/p&gt;

&lt;p&gt;The devil is in the details, but this doesn’t seem like a terribly difficult
feature to implement.&lt;/p&gt;

&lt;p&gt;An RSR wouldn’t store every known vulnerability inside of itself (even if it
had unlimited space, the list of vulnerabilities is always growing)–it would
“phone home” to its manufacturer with the “signature” of the devices connected
to it, and its manufacturer would send it the list of vulnerabilities to check
for on each of them.&lt;/p&gt;

&lt;p&gt;I’m frankly surprised that no router manufacturer that I know of does this
already.&lt;/p&gt;

&lt;h3 id=&quot;automatically-updated&quot;&gt;Automatically Updated&lt;/h3&gt;

&lt;p&gt;Even a Really Smart Router that did all of these things could not be perfect.
New Internet-connected devices are created all the time, maybe with types of
vulnerabilities that the RSR doesn’t initially know how to check for. The RSR’s
software itself may have vulnerabilities that don’t get discovered until after
many of them are installed in customers’ homes and businesses.&lt;/p&gt;

&lt;p&gt;Most if not all routers already have a way to update their software. However,
not many routers make updating their software easy or convenient, or have an
option to automatically update it. People being people, I think RSRs must have
an option to automatically check for and install new software without requiring
the user to press a button or visit a web page–it needs to be 100% automatic.&lt;/p&gt;

&lt;p&gt;The updating process itself needs to be secure, too–it must be impossible for
an attacker to pretend that it’s the RSR’s manufacturer, and fool it into
downloading and installing the attacker’s software!&lt;/p&gt;

&lt;p&gt;Our phones and computers can do this–heck, even smart thermostats update
themselves to the latest software automatically! There’s no good reason our
routers don’t.&lt;/p&gt;

&lt;h3 id=&quot;we-need-really-smart-routers&quot;&gt;We need Really Smart Routers!&lt;/h3&gt;

&lt;p&gt;Every year, millions of new connected devices are installed into people’s
houses and businesses. DDoS attacks will continue to happen more and more
often, causing more and more disruption, unless vulnerable devices are removed
from the Internet when they’re discovered.&lt;/p&gt;

&lt;p&gt;I think that we need Really Smart Routers, and badly!&lt;/p&gt;

&lt;p&gt;What do you think?&lt;/p&gt;</content><author><name></name></author><summary type="html">In 2016 the media reported on a number of Distributed Denial of Service (DDoS) attacks that significantly disrupted aspects of the Internet, wreaking havoc that ranged from minor inconveniences for many, to serious financial losses for some. The DDoS attacks were caused by botnets, which are composed of large numbers of unsecured, hacked devices that are connected to the Internet. Attackers take advantage of numerous well-known and lesser-known vulnerabilities in a multitude of connected computers, routers, surveillance cameras, and other devices. When they find a device they can hack into, they take control of the device for their own, often destructive, purposes.</summary></entry></feed>