<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://tad.thorley.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://tad.thorley.dev/" rel="alternate" type="text/html" /><updated>2026-04-20T14:09:18+00:00</updated><id>https://tad.thorley.dev/feed.xml</id><title type="html">Tad Thorley ※ Developer</title><subtitle>The Professional Website of Tad Thorley</subtitle><author><name>Tad Thorley</name></author><entry><title type="html">ActionFigure: Fully-Articulated Controller Actions for Rails</title><link href="https://tad.thorley.dev/2026/03/25/action-figure.html" rel="alternate" type="text/html" title="ActionFigure: Fully-Articulated Controller Actions for Rails" /><published>2026-03-25T00:00:00+00:00</published><updated>2026-03-25T00:00:00+00:00</updated><id>https://tad.thorley.dev/2026/03/25/action-figure</id><content type="html" xml:base="https://tad.thorley.dev/2026/03/25/action-figure.html"><![CDATA[<p>I worked on a codebase that had taken the “skinny controllers” and “skinny models” idea to heart by putting everything into service objects. I think that is a great idea if you are purposeful with your service objects. The problem was we weren’t. The stack from controller receiving a request to models updating the database was a dozen or more service objects deep in some places. It was service objects calling service objects calling service objects.</p>

<p>The problem wasn’t that the individual classes were particularly bad. Most of them were fine in isolation. The problem was that it was hard to know which service objects were being used by other service objects and it was hard to discern the impact that changing a service object had. It was safer to add a new service object and you had two choices: near the controller (the beginning of the chain) or the model (the end).</p>

<p>So the chains got longer. The service objects in the middle became a tangled web that nobody wanted to touch. The roles blurred until “service object” just meant “a class that does something, somewhere.”</p>

<p>I started thinking about what better boundaries and purpose would look like.</p>

<h3 id="the-service-object-problem">The service object problem</h3>

<p>Service objects are just plain old Ruby objects. They can take anything, return anything, and call anything. There’s no constraint that tells you “this is my particular, exclusive job.” So when you’re staring at a chain of them, you have to read every one to understand what’s actually happening.</p>

<p>The lack of a clear role means they could do a bit of everything. Should this do validation? Is it some key business logic? Can I trust the inputs? Do I dare modify the data? Which models will this touch? The answer is usually “who knows?” which is how you end up with logic accreting at the edges — controllers and models — because at least you know what those are for.</p>

<p>Looking at the tests didn’t help. We believed in strong unit testing and mocking the boundaries. The test suite would tell you each link worked in isolation, but nothing about what the pipeline actually did end-to-end. You could have a green test suite and a broken feature.</p>

<p>The problem isn’t service objects per se. It’s the lack of clear roles and boundaries. What if each class had exactly one job, with explicit inputs, validation, and output?</p>

<h3 id="design-philosophy">Design philosophy</h3>

<p>The idea behind <a href="https://github.com/phaedryx/action_figure">ActionFigure</a> is <strong>fully-articulated controller actions</strong>. When you open an action class, the full story is right there — what it accepts, how it validates, what it does, what it returns. You should be able to quickly see what it does without tracing through a chain of other objects.</p>

<p><strong>Functional style.</strong> Actions are essentially functions. Clear inputs go in via keyword arguments, a render-ready hash comes out. No side-effecting instance state, no mutable shared context being passed down a chain.</p>

<p><strong>Explicit over implicit.</strong> The shape and types of inputs are declared, not inferred. The response format can be chosen, not assumed. There’s no magic method resolution and no convention-based behavior — what you see is what you get.</p>

<p><strong>Self-contained units.</strong> Each action is a complete, readable unit. No hidden state inherited from a base class, no relying on middleware you can’t see. Class-level state like <code class="highlight ruby"><span class="n">params_schema</span></code> and <code class="highlight ruby"><span class="n">rules</span></code> is intentionally not inherited by subclasses. Share behavior through plain Ruby — extract a method, call a service — not through deep class hierarchies.</p>

<p><strong>Reusable across controllers.</strong> Because actions are decoupled from the controllers that call them, versioned controllers can share the same action class. <code class="highlight ruby"><span class="no">V1</span><span class="o">::</span><span class="no">UsersController</span></code> and <code class="highlight ruby"><span class="no">V2</span><span class="o">::</span><span class="no">UsersController</span></code> can both call <code class="highlight ruby"><span class="no">Users</span><span class="o">::</span><span class="no">CreateAction</span><span class="p">.</span><span class="nf">create</span></code>. Versioning lives in the routing and controller layer where it belongs, not duplicated across business logic.</p>

<h3 id="introducing-actionfigure">Introducing ActionFigure</h3>

<p>Here’s what an action class looks like. This one creates a user and returns a JSON:API formatted response:</p>

<figure class="code-example">
  <figcaption class="title">app/actions/users/create_action.rb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="source ruby"><pre>
<span class="k">class</span> <span class="nc">Users::CreateAction</span>
  <span class="kp">include</span> <span class="no">ActionFigure</span><span class="p">[</span><span class="ss">:jsonapi</span><span class="p">]</span>

  <span class="n">params_schema</span> <span class="k">do</span>
    <span class="n">required</span><span class="p">(</span><span class="ss">:user</span><span class="p">).</span><span class="nf">hash</span> <span class="k">do</span>
      <span class="n">required</span><span class="p">(</span><span class="ss">:name</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
      <span class="n">required</span><span class="p">(</span><span class="ss">:email</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="n">params</span><span class="p">:,</span> <span class="n">company</span><span class="p">:)</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">company</span><span class="p">.</span><span class="nf">users</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:user</span><span class="p">])</span>
    <span class="k">return</span> <span class="no">UnprocessableContent</span><span class="p">(</span><span class="ss">errors: </span><span class="n">user</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">messages</span><span class="p">)</span> <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">any?</span>

    <span class="no">Created</span><span class="p">(</span><span class="ss">resource: </span><span class="n">user</span><span class="p">.</span><span class="nf">as_json</span><span class="p">(</span><span class="ss">only: </span><span class="sx">%i[id name email]</span><span class="p">))</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>The <code class="highlight ruby"><span class="n">params_schema</span></code> block declares the shape and types of the input. The action method does the work. ActionFigure automatically detects <code class="highlight ruby"><span class="c1">#create</span></code> as the entry point and generates a matching class method, so the controller calls <code class="highlight ruby"><span class="no">Users</span><span class="o">::</span><span class="no">CreateAction</span><span class="p">.</span><span class="nf">create</span></code>. The response helpers — <code class="highlight ruby"><span class="no">Created</span></code>, <code class="highlight ruby"><span class="no">UnprocessableContent</span></code> — return render-ready hashes that go straight to <code class="highlight ruby"><span class="n">render</span></code> in the controller.</p>

<p>The controller becomes a one-liner:</p>

<figure class="code-example">
  <figcaption class="title">app/controllers/users_controller.rb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="source ruby"><pre>
<span class="k">class</span> <span class="nc">UsersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">create</span>
    <span class="n">render</span> <span class="no">Users</span><span class="o">::</span><span class="no">CreateAction</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="n">params</span><span class="p">:,</span> <span class="ss">company: </span><span class="n">current_company</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Extra keyword arguments like <code class="highlight ruby"><span class="n">company</span><span class="p">:</span></code> are passed through to <code class="highlight ruby"><span class="c1">#create</span></code> as context. The action doesn’t know or care that it’s being called from a controller — you can test it with a plain method call.</p>

<h3 id="the-validation-pipeline">The validation pipeline</h3>

<p>In a typical Rails app the controller has <code class="highlight ruby"><span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:user</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:email</span><span class="p">)</span></code> — a whitelist that tells you which keys are allowed but nothing about their shape or types. You have to make those checks elsewhere.</p>

<p>ActionFigure replaces this with a two-layer pipeline that lives in one place.</p>

<p><strong>Layer one: <code class="highlight ruby"><span class="n">params_schema</span></code>.</strong> This is where you declare the shape and types of the input. It makes both obvious at a glance:</p>

<figure class="code-example">
  <figcaption class="title">params_schema — shape and types in one place</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="source ruby"><pre>
<span class="n">params_schema</span> <span class="k">do</span>
  <span class="n">required</span><span class="p">(</span><span class="ss">:user</span><span class="p">).</span><span class="nf">hash</span> <span class="k">do</span>
    <span class="n">optional</span><span class="p">(</span><span class="ss">:title</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
    <span class="n">required</span><span class="p">(</span><span class="ss">:name</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="n">string</span><span class="p">)</span>
    <span class="n">optional</span><span class="p">(</span><span class="ss">:email</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
    <span class="n">optional</span><span class="p">(</span><span class="ss">:guid</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
    <span class="n">required</span><span class="p">(</span><span class="ss">:address</span><span class="p">).</span><span class="nf">hash</span> <span class="k">do</span>
      <span class="n">required</span><span class="p">(</span><span class="ss">:street1</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
      <span class="n">optional</span><span class="p">(</span><span class="ss">:street2</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
      <span class="n">required</span><span class="p">(</span><span class="ss">:city</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
      <span class="n">required</span><span class="p">(</span><span class="ss">:state</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
      <span class="n">required</span><span class="p">(</span><span class="ss">:zipcode</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
  <span class="n">optional</span><span class="p">(</span><span class="ss">:referral_id</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:integar</span><span class="p">)</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Compare that to <code class="highlight ruby"><span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:user</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">address: </span><span class="p">[</span><span class="ss">:street1</span><span class="p">,</span> <span class="ss">:street2</span><span class="p">,</span> <span class="ss">:city</span><span class="p">,</span> <span class="ss">:state</span><span class="p">,</span> <span class="ss">:zipcode</span><span class="p">])</span></code>. The <code class="highlight ruby"><span class="n">permit</span></code> call tells you the allowed keys, but not that <code class="highlight ruby"><span class="n">title</span></code> and <code class="highlight ruby"><span class="n">street2</span></code> are optional while the rest are required, not that <code class="highlight ruby"><span class="n">referral_id</span></code> should be an integer, and there’s no clean way to express that <code class="highlight ruby"><span class="n">referral_id</span></code> is an optional sibling of the required <code class="highlight ruby"><span class="n">user</span></code> hash. With <code class="highlight ruby"><span class="n">params_schema</span></code>, the shape is obvious and the types are explicit.</p>

<p>Type coercion happens automatically — the string <code class="highlight ruby"><span class="s2">"25"</span></code> that comes over HTTP becomes the integer <code class="highlight ruby"><span class="mi">25</span></code>. ActionFigure also accepts <code class="highlight ruby"><span class="no">ActionController</span><span class="o">::</span><span class="no">Parameters</span></code> directly. The schema acts as the whitelist, so there are no manual <code class="highlight ruby"><span class="n">permit</span></code> calls.</p>

<p>If the schema fails, the action method never runs. The action returns an <code class="highlight ruby"><span class="no">UnprocessableContent</span></code> response immediately.</p>

<p><strong>Layer two: <code class="highlight ruby"><span class="n">rules</span></code>.</strong> This is where cross-field and contextual validation lives. Rules only run after the schema passes, so you can trust that the data is structurally valid:</p>

<figure class="code-example">
  <figcaption class="title">cross-param rules</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="source ruby"><pre>
<span class="n">rules</span> <span class="k">do</span>
  <span class="n">one_rule</span><span class="p">(</span><span class="ss">:email</span><span class="p">,</span> <span class="ss">:guid</span><span class="p">,</span> <span class="s2">"provide either an email or a guid, but not both"</span><span class="p">)</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>ActionFigure provides four cross-param rule helpers:</p>

<ul>
  <li><code class="highlight ruby"><span class="n">one_rule</span></code> — exactly one of the listed fields must be present</li>
  <li><code class="highlight ruby"><span class="n">all_rule</span></code> — all or none of the listed fields must be present</li>
  <li><code class="highlight ruby"><span class="n">any_rule</span></code> — at least one of the listed fields must be present</li>
  <li><code class="highlight ruby"><span class="n">exclusive_rule</span></code> — at most one of the listed fields may be present</li>
</ul>

<p>These are convenience helpers for common cases, but the <code class="highlight ruby"><span class="n">rules</span></code> block supports any custom validation that works with <code class="highlight ruby"><span class="n">dry</span><span class="o">-</span><span class="n">validation</span></code> — so you’re not limited to these four:</p>

<figure class="code-example">
  <figcaption class="title">custom rule</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="source ruby"><pre>
<span class="n">rules</span> <span class="k">do</span>
  <span class="n">rule</span><span class="p">(</span><span class="ss">:email</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">key</span><span class="p">.</span><span class="nf">failure</span><span class="p">(</span><span class="s2">"is not a valid email"</span><span class="p">)</span> <span class="k">unless</span> <span class="n">values</span><span class="p">[</span><span class="ss">:email</span><span class="p">].</span><span class="nf">include?</span><span class="p">(</span><span class="s2">"@"</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="n">rule</span><span class="p">(</span><span class="ss">:email</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">key</span><span class="p">.</span><span class="nf">failure</span><span class="p">(</span><span class="s2">"is already taken"</span><span class="p">)</span> <span class="k">if</span> <span class="no">User</span><span class="p">.</span><span class="nf">exists?</span><span class="p">(</span><span class="ss">email: </span><span class="n">values</span><span class="p">[</span><span class="ss">:email</span><span class="p">])</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>These run in the validation layer, before the action method. Invalid combinations never reach your business logic. When the action method runs, you can trust that the inputs are structurally valid and logically consistent.</p>

<p>You can also enable strict mode with <code class="highlight ruby"><span class="n">whiny_extra_params</span></code> in the configuration. By default, extra parameters are silently stripped. With strict mode, they trigger a validation failure:</p>

<figure class="code-example">
  <figcaption class="title">strict parameter checking</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="source ruby"><pre>
<span class="no">ActionFigure</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">whiny_extra_params</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">development?</span>
<span class="k">end</span>

<span class="c1"># Called with: { item_id: 1, quantity: 2, admin: true }</span>
<span class="c1"># Result: UnprocessableContent with { admin: ["is not allowed"] }</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>The throughline: instead of validation scattered across controller, model, and service objects, the entire input contract is in one place.</p>

<h3 id="pluggable-response-formatters">Pluggable response formatters</h3>

<p>Action logic shouldn’t be coupled to a particular response envelope. ActionFigure separates the two: your action method uses response helpers like <code class="highlight ruby"><span class="no">Ok</span></code> and <code class="highlight ruby"><span class="no">Created</span></code>, and the formatter decides what shape the JSON takes.</p>

<p>The JSON:API formatter, for example, wraps resources in the standard <code class="highlight ruby"><span class="p">{</span> <span class="ss">data: </span><span class="p">{</span> <span class="n">type</span><span class="p">,</span> <span class="nb">id</span><span class="p">,</span> <span class="n">attributes</span> <span class="p">}</span> <span class="p">}</span></code> structure and formats errors with JSON pointers:</p>

<figure class="code-example">
  <figcaption class="title">JSON:API success response</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="source ruby"><pre>
<span class="no">Created</span><span class="p">(</span><span class="ss">resource: </span><span class="n">user</span><span class="p">.</span><span class="nf">as_json</span><span class="p">(</span><span class="ss">only: </span><span class="sx">%i[id name email]</span><span class="p">))</span>
<span class="c1"># =&gt; {</span>
<span class="c1">#   json: {</span>
<span class="c1">#     data: { id: "1", name: "Tad", email: "tad@example.com" }</span>
<span class="c1">#   },</span>
<span class="c1">#   status: :created</span>
<span class="c1"># }</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">JSON:API error response</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="source ruby"><pre>
<span class="no">UnprocessableContent</span><span class="p">(</span><span class="ss">errors: </span><span class="p">{</span> <span class="ss">name: </span><span class="p">[</span><span class="s2">"is missing"</span><span class="p">]</span> <span class="p">})</span>
<span class="c1"># =&gt; {</span>
<span class="c1">#   json: {</span>
<span class="c1">#     errors: [{</span>
<span class="c1">#       status: "422",</span>
<span class="c1">#       detail: "is missing",</span>
<span class="c1">#       source: { pointer: "/data/attributes/name" }</span>
<span class="c1">#     }]</span>
<span class="c1">#   },</span>
<span class="c1">#   status: :unprocessable_content</span>
<span class="c1"># }</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>ActionFigure ships with four formatters. Here’s how the same <code class="highlight ruby"><span class="no">Ok</span><span class="p">(</span><span class="ss">resource: </span><span class="p">{</span> <span class="ss">name: </span><span class="s2">"Tad"</span> <span class="p">})</span></code> call looks in each:</p>

<table class="styled">
  <caption>Formatter comparison</caption>
  <thead>
    <tr>
      <th>Formatter</th>
      <th>Include with</th>
      <th>Ok response shape</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Default</td>
      <td><code class="language-plaintext highlighter-rouge">ActionFigure[:default]</code></td>
      <td><code class="language-plaintext highlighter-rouge">{ name: "Tad" }</code></td>
    </tr>
    <tr>
      <td>JSend</td>
      <td><code class="language-plaintext highlighter-rouge">ActionFigure[:jsend]</code></td>
      <td><code class="language-plaintext highlighter-rouge">{ status: "success", data: { name: "Tad" } }</code></td>
    </tr>
    <tr>
      <td>JSON:API</td>
      <td><code class="language-plaintext highlighter-rouge">ActionFigure[:jsonapi]</code></td>
      <td><code class="language-plaintext highlighter-rouge">{ data: { name: "Tad" } }</code></td>
    </tr>
    <tr>
      <td>Wrapped</td>
      <td><code class="language-plaintext highlighter-rouge">ActionFigure[:wrapped]</code></td>
      <td><code class="language-plaintext highlighter-rouge">{ data: { name: "Tad" }, errors: nil, status: "success" }</code></td>
    </tr>
  </tbody>
</table>

<p>You select a formatter per-class with <code class="highlight ruby"><span class="kp">include</span> <span class="no">ActionFigure</span><span class="p">[</span><span class="ss">:jsonapi</span><span class="p">]</span></code>, or set a global default:</p>

<figure class="code-example">
  <figcaption class="title">global formatter configuration</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="source ruby"><pre>
<span class="no">ActionFigure</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">format</span> <span class="o">=</span> <span class="ss">:jsonapi</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Every formatter provides nine response helpers that map to HTTP status codes:</p>

<table class="styled">
  <caption>Response helpers</caption>
  <thead>
    <tr>
      <th>Helper</th>
      <th>HTTP Status</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Ok</code></td>
      <td>200</td>
      <td> </td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Created</code></td>
      <td>201</td>
      <td> </td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Accepted</code></td>
      <td>202</td>
      <td> </td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">NoContent</code></td>
      <td>204</td>
      <td>No JSON body</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PaymentRequired</code></td>
      <td>402</td>
      <td>Business state like “subscription overdue” or “quota exceeded”</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Forbidden</code></td>
      <td>403</td>
      <td> </td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">NotFound</code></td>
      <td>404</td>
      <td> </td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Conflict</code></td>
      <td>409</td>
      <td>Resource already exists or state conflict</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">UnprocessableContent</code></td>
      <td>422</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>You can also build your own. A custom formatter is a module that implements the eight required methods (<code class="highlight ruby"><span class="no">Ok</span></code>, <code class="highlight ruby"><span class="no">Created</span></code>, <code class="highlight ruby"><span class="no">Accepted</span></code>, <code class="highlight ruby"><span class="no">UnprocessableContent</span></code>, <code class="highlight ruby"><span class="no">NotFound</span></code>, <code class="highlight ruby"><span class="no">Forbidden</span></code>, <code class="highlight ruby"><span class="no">Conflict</span></code>, <code class="highlight ruby"><span class="no">PaymentRequired</span></code>) and registers itself:</p>

<figure class="code-example">
  <figcaption class="title">custom formatter</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
</pre></td><td class="source ruby"><pre>
<span class="k">module</span> <span class="nn">MyFormatter</span>
  <span class="kp">include</span> <span class="no">ActionFigure</span><span class="o">::</span><span class="no">Formatter</span>

  <span class="k">def</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">resource</span><span class="p">:,</span> <span class="ss">meta: </span><span class="kp">nil</span><span class="p">)</span>
    <span class="p">{</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">result: </span><span class="n">resource</span><span class="p">,</span> <span class="ss">ok: </span><span class="kp">true</span> <span class="p">},</span> <span class="ss">status: :ok</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">Created</span><span class="p">(</span><span class="n">resource</span><span class="p">:,</span> <span class="ss">meta: </span><span class="kp">nil</span><span class="p">)</span>
    <span class="p">{</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">result: </span><span class="n">resource</span><span class="p">,</span> <span class="ss">ok: </span><span class="kp">true</span> <span class="p">},</span> <span class="ss">status: :created</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">Accepted</span><span class="p">(</span><span class="ss">resource: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">meta: </span><span class="kp">nil</span><span class="p">)</span>
    <span class="p">{</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">ok: </span><span class="kp">true</span> <span class="p">},</span> <span class="ss">status: :accepted</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">UnprocessableContent</span><span class="p">(</span><span class="n">errors</span><span class="p">:)</span>
    <span class="p">{</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">errors: </span><span class="n">errors</span><span class="p">,</span> <span class="ss">ok: </span><span class="kp">false</span> <span class="p">},</span> <span class="ss">status: :unprocessable_content</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">NotFound</span><span class="p">(</span><span class="n">errors</span><span class="p">:)</span>
    <span class="p">{</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">errors: </span><span class="n">errors</span><span class="p">,</span> <span class="ss">ok: </span><span class="kp">false</span> <span class="p">},</span> <span class="ss">status: :not_found</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">Forbidden</span><span class="p">(</span><span class="n">errors</span><span class="p">:)</span>
    <span class="p">{</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">errors: </span><span class="n">errors</span><span class="p">,</span> <span class="ss">ok: </span><span class="kp">false</span> <span class="p">},</span> <span class="ss">status: :forbidden</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">Conflict</span><span class="p">(</span><span class="n">errors</span><span class="p">:)</span>
    <span class="p">{</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">errors: </span><span class="n">errors</span><span class="p">,</span> <span class="ss">ok: </span><span class="kp">false</span> <span class="p">},</span> <span class="ss">status: :conflict</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">PaymentRequired</span><span class="p">(</span><span class="n">errors</span><span class="p">:)</span>
    <span class="p">{</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">errors: </span><span class="n">errors</span><span class="p">,</span> <span class="ss">ok: </span><span class="kp">false</span> <span class="p">},</span> <span class="ss">status: :payment_required</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">ActionFigure</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">register</span><span class="p">(</span><span class="ss">my_format: </span><span class="no">MyFormatter</span><span class="p">)</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">format</span> <span class="o">=</span> <span class="ss">:my_format</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Registration validates that your module implements all required methods at load time, so you find out immediately if you’ve missed one.</p>

<h3 id="under-the-hood">Under the hood</h3>

<p>If you’re curious about how the pieces fit together, here’s what’s happening inside.</p>

<p><strong>Automatic entry point discovery.</strong> ActionFigure uses a <code class="highlight ruby"><span class="n">method_added</span></code> hook to watch for public instance methods defined on the class. The first public method becomes the entry point and a matching class-level method is created automatically. So when you define <code class="highlight ruby"><span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="n">params</span><span class="p">:,</span> <span class="n">company</span><span class="p">:)</span></code>, ActionFigure generates <code class="highlight ruby"><span class="no">Users</span><span class="o">::</span><span class="no">CreateAction</span><span class="p">.</span><span class="nf">create</span></code> as the class method that runs the full validation pipeline before invoking your method.</p>

<p>If a class defines more than one public method, ActionFigure raises an <code class="highlight ruby"><span class="no">IndeterminantEntryPointError</span></code> — use the <code class="highlight ruby"><span class="n">entry_point</span></code> macro to disambiguate, or make the helper method private:</p>

<figure class="code-example">
  <figcaption class="title">disambiguating with entry_point</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="source ruby"><pre>
<span class="k">class</span> <span class="nc">Orders::SearchAction</span>
  <span class="kp">include</span> <span class="no">ActionFigure</span><span class="p">[</span><span class="ss">:jsend</span><span class="p">]</span>

  <span class="n">entry_point</span> <span class="ss">:search</span>

  <span class="n">params_schema</span> <span class="k">do</span>
    <span class="n">optional</span><span class="p">(</span><span class="ss">:order_id</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
    <span class="n">optional</span><span class="p">(</span><span class="ss">:tracking_number</span><span class="p">).</span><span class="nf">filled</span><span class="p">(</span><span class="ss">:string</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">search</span><span class="p">(</span><span class="n">params</span><span class="p">:,</span> <span class="n">company</span><span class="p">:)</span>
    <span class="n">orders</span> <span class="o">=</span> <span class="n">company</span><span class="p">.</span><span class="nf">orders</span>
    <span class="n">orders</span> <span class="o">=</span> <span class="n">orders</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">id: </span><span class="n">params</span><span class="p">[</span><span class="ss">:order_id</span><span class="p">])</span> <span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="ss">:order_id</span><span class="p">]</span>
    <span class="n">orders</span> <span class="o">=</span> <span class="n">orders</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">tracking_number: </span><span class="n">params</span><span class="p">[</span><span class="ss">:tracking_number</span><span class="p">])</span> <span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="ss">:tracking_number</span><span class="p">]</span>
    <span class="no">Ok</span><span class="p">(</span><span class="ss">resource: </span><span class="n">orders</span><span class="p">.</span><span class="nf">as_json</span><span class="p">(</span><span class="ss">only: </span><span class="sx">%i[id tracking_number status]</span><span class="p">))</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">format_results</span><span class="p">(</span><span class="n">orders</span><span class="p">)</span>
    <span class="c1"># making this private would also resolve the ambiguity</span>
    <span class="n">orders</span><span class="p">.</span><span class="nf">as_json</span><span class="p">(</span><span class="ss">only: </span><span class="sx">%i[id tracking_number status]</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p><strong>Dynamic module building.</strong> When you write <code class="highlight ruby"><span class="kp">include</span> <span class="no">ActionFigure</span><span class="p">[</span><span class="ss">:jsonapi</span><span class="p">]</span></code>, the <code class="highlight ruby"><span class="p">[]</span></code> method dynamically builds a module that mixes in two things: the validation pipeline (<code class="highlight ruby"><span class="no">Core</span></code>) and the chosen formatter. Here’s the actual code:</p>

<figure class="code-example">
  <figcaption class="title">lib/action_figure.rb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="source ruby"><pre>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">[]</span><span class="p">(</span><span class="nb">format</span> <span class="o">=</span> <span class="n">configuration</span><span class="p">.</span><span class="nf">format</span><span class="p">)</span>
  <span class="n">format_modules</span><span class="p">.</span><span class="nf">compute_if_absent</span><span class="p">(</span><span class="nb">format</span><span class="p">)</span> <span class="p">{</span> <span class="n">build_format_module</span><span class="p">(</span><span class="nb">format</span><span class="p">,</span> <span class="n">fetch</span><span class="p">(</span><span class="nb">format</span><span class="p">))</span> <span class="p">}</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">new_format_module</span><span class="p">(</span><span class="n">formatter</span><span class="p">)</span>
  <span class="no">Module</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
    <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">included</span><span class="p">(</span><span class="n">base</span><span class="p">)</span>
      <span class="n">base</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="no">ActionFigure</span><span class="o">::</span><span class="no">Core</span><span class="o">::</span><span class="no">ClassMethods</span><span class="p">)</span>
      <span class="k">return</span> <span class="k">unless</span> <span class="k">defined?</span><span class="p">(</span><span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Notifications</span><span class="p">)</span> <span class="o">&amp;&amp;</span>
                    <span class="no">ActionFigure</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">activesupport_notifications</span>

      <span class="n">base</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="no">ActionFigure</span><span class="o">::</span><span class="no">Core</span><span class="o">::</span><span class="no">Notifications</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="kp">include</span> <span class="no">ActionFigure</span><span class="o">::</span><span class="no">Core</span>
    <span class="kp">include</span> <span class="n">formatter</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>This is why each action class gets a clean <code class="highlight ruby"><span class="kp">include</span></code> — the dynamically built module brings in everything the action needs in one shot. Formatter selection happens at include time, so different actions in the same app can use different formatters.</p>

<p><strong>Thread-safe caching.</strong> Format modules are cached in a <code class="highlight ruby"><span class="no">Concurrent</span><span class="o">::</span><span class="no">Map</span></code>, so each one is built exactly once and reused across threads. The <code class="highlight ruby"><span class="n">compute_if_absent</span></code> call guarantees thread safety without explicit locks — important for high-throughput Rails apps running on Puma.</p>

<figure class="code-example">
  <figcaption class="title">thread-safe module cache</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="source ruby"><pre>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">format_modules</span>
  <span class="vi">@format_modules</span> <span class="o">||=</span> <span class="no">Concurrent</span><span class="o">::</span><span class="no">Map</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p><strong>No inheritance by design.</strong> Class-level state — <code class="highlight ruby"><span class="n">params_schema</span></code>, <code class="highlight ruby"><span class="n">rules</span></code>, and the detected entry point — is stored in class instance variables that are intentionally not inherited by subclasses. This is a deliberate choice. Each action is a flat, self-contained unit that you can read without wondering what a parent class set up. It prevents the kind of deep inheritance chains that made the original service object problem so hard to reason about.</p>

<p><strong>Contract exposure.</strong> The underlying <code class="highlight ruby"><span class="no">Dry</span><span class="o">::</span><span class="no">Validation</span><span class="o">::</span><span class="no">Contract</span></code> is accessible via <code class="highlight ruby"><span class="p">.</span><span class="nf">contract</span></code>:</p>

<figure class="code-example">
  <figcaption class="title">using the contract directly</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="source ruby"><pre>
<span class="n">contract</span> <span class="o">=</span> <span class="no">Users</span><span class="o">::</span><span class="no">CreateAction</span><span class="p">.</span><span class="nf">contract</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">contract</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="ss">user: </span><span class="p">{</span> <span class="ss">name: </span><span class="s2">"Tad"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"tad@example.com"</span> <span class="p">})</span>
<span class="n">result</span><span class="p">.</span><span class="nf">success?</span>  <span class="c1"># =&gt; true</span>
<span class="n">result</span><span class="p">.</span><span class="nf">to_h</span>      <span class="c1"># =&gt; { user: { name: "Tad", email: "tad@example.com" } }</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>This is useful for form validation endpoints where you want to check params without running the full action, or for testing validation rules in isolation.</p>

<h3 id="testing">Testing</h3>

<p>With ActionFigure, testing is straightforward. Actions are Ruby objects that receive named parameters and return hashes. You call the class method with the same arguments the controller would pass, and you assert on the result.</p>

<figure class="code-example">
  <figcaption class="title">Minitest</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
</pre></td><td class="source ruby"><pre>
<span class="nb">require</span> <span class="s2">"action_figure/testing/minitest"</span>

<span class="k">class</span> <span class="nc">Users::CreateActionTest</span> <span class="o">&lt;</span> <span class="no">Minitest</span><span class="o">::</span><span class="no">Test</span>
  <span class="kp">include</span> <span class="no">ActionFigure</span><span class="o">::</span><span class="no">Testing</span><span class="o">::</span><span class="no">Minitest</span>

  <span class="k">def</span> <span class="nf">test_creates_a_user</span>
    <span class="n">company</span> <span class="o">=</span> <span class="no">Company</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Acme"</span><span class="p">)</span>

    <span class="n">result</span> <span class="o">=</span> <span class="no">Users</span><span class="o">::</span><span class="no">CreateAction</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
      <span class="ss">params: </span><span class="p">{</span> <span class="ss">user: </span><span class="p">{</span> <span class="ss">name: </span><span class="s2">"Tad"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"tad@example.com"</span> <span class="p">}</span> <span class="p">},</span>
      <span class="ss">company: </span><span class="n">company</span>
    <span class="p">)</span>

    <span class="n">assert_Created</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
    <span class="n">assert_includes</span> <span class="n">result</span><span class="p">[</span><span class="ss">:json</span><span class="p">][</span><span class="ss">:data</span><span class="p">].</span><span class="nf">to_s</span><span class="p">,</span> <span class="s2">"Tad"</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">test_rejects_missing_name</span>
    <span class="n">company</span> <span class="o">=</span> <span class="no">Company</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Acme"</span><span class="p">)</span>

    <span class="n">result</span> <span class="o">=</span> <span class="no">Users</span><span class="o">::</span><span class="no">CreateAction</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
      <span class="ss">params: </span><span class="p">{</span> <span class="ss">user: </span><span class="p">{</span> <span class="ss">email: </span><span class="s2">"tad@example.com"</span> <span class="p">}</span> <span class="p">},</span>
      <span class="ss">company: </span><span class="n">company</span>
    <span class="p">)</span>

    <span class="n">assert_UnprocessableContent</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">RSpec</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="source ruby"><pre>
<span class="nb">require</span> <span class="s2">"action_figure/testing/rspec"</span>

<span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="no">Users</span><span class="o">::</span><span class="no">CreateAction</span> <span class="k">do</span>
  <span class="n">it</span> <span class="s2">"creates a user with valid parameters"</span> <span class="k">do</span>
    <span class="n">company</span> <span class="o">=</span> <span class="no">Company</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Acme"</span><span class="p">)</span>

    <span class="n">result</span> <span class="o">=</span> <span class="no">Users</span><span class="o">::</span><span class="no">CreateAction</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
      <span class="ss">params: </span><span class="p">{</span> <span class="ss">user: </span><span class="p">{</span> <span class="ss">name: </span><span class="s2">"Tad"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"tad@example.com"</span> <span class="p">}</span> <span class="p">},</span>
      <span class="ss">company: </span><span class="n">company</span>
    <span class="p">)</span>

    <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">).</span><span class="nf">to</span> <span class="n">be_Created</span>
    <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">[</span><span class="ss">:json</span><span class="p">][</span><span class="ss">:data</span><span class="p">]).</span><span class="nf">to</span> <span class="kp">include</span><span class="p">(</span><span class="s2">"name"</span> <span class="o">=&gt;</span> <span class="s2">"Tad"</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="n">it</span> <span class="s2">"rejects missing name"</span> <span class="k">do</span>
    <span class="n">company</span> <span class="o">=</span> <span class="no">Company</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Acme"</span><span class="p">)</span>

    <span class="n">result</span> <span class="o">=</span> <span class="no">Users</span><span class="o">::</span><span class="no">CreateAction</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
      <span class="ss">params: </span><span class="p">{</span> <span class="ss">user: </span><span class="p">{</span> <span class="ss">email: </span><span class="s2">"tad@example.com"</span> <span class="p">}</span> <span class="p">},</span>
      <span class="ss">company: </span><span class="n">company</span>
    <span class="p">)</span>

    <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">).</span><span class="nf">to</span> <span class="n">be_UnprocessableContent</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>The matchers — <code class="highlight ruby"><span class="n">assert_Created</span></code>, <code class="highlight ruby"><span class="n">be_Created</span></code>, <code class="highlight ruby"><span class="n">assert_UnprocessableContent</span></code>, <code class="highlight ruby"><span class="n">be_UnprocessableContent</span></code>, and so on — are provided for all nine response statuses. They check the <code class="highlight ruby"><span class="ss">:status</span></code> key in the returned hash.</p>

<p>The test calls the action the same way the controller does. There’s nothing to mock.</p>

<h3 id="activesupport-notifications">ActiveSupport Notifications</h3>

<p>ActionFigure has optional <code class="highlight ruby"><span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Notifications</span></code> integration. When enabled, every action call emits a <code class="highlight ruby"><span class="n">process</span><span class="p">.</span><span class="nf">action_figure</span></code> event with the action class name and response status:</p>

<figure class="code-example">
  <figcaption class="title">enabling notifications</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="source ruby"><pre>
<span class="no">ActionFigure</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">activesupport_notifications</span> <span class="o">=</span> <span class="kp">true</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">subscribing to events</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="source ruby"><pre>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Notifications</span><span class="p">.</span><span class="nf">subscribe</span><span class="p">(</span><span class="s2">"process.action_figure"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
  <span class="no">Rails</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">info</span> <span class="s2">"</span><span class="si">#{</span><span class="n">event</span><span class="p">.</span><span class="nf">payload</span><span class="p">[</span><span class="ss">:action</span><span class="p">]</span><span class="si">}</span><span class="s2"> → </span><span class="si">#{</span><span class="n">event</span><span class="p">.</span><span class="nf">payload</span><span class="p">[</span><span class="ss">:status</span><span class="p">]</span><span class="si">}</span><span class="s2"> (</span><span class="si">#{</span><span class="n">event</span><span class="p">.</span><span class="nf">duration</span><span class="si">}</span><span class="s2">ms)"</span>
<span class="k">end</span>

<span class="c1"># =&gt; "Users::CreateAction → created (12.3ms)"</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>You get logging, monitoring, and alerting hooks without coupling any of that into the action logic itself. The action doesn’t know it’s being observed.</p>

<h3 id="file-conventions">File conventions</h3>

<p>Actions aren’t generic service objects floating in <code class="highlight ruby"><span class="n">app</span><span class="o">/</span><span class="n">services</span><span class="o">/</span></code> with vague names like <code class="highlight ruby"><span class="no">UserManager</span></code> or <code class="highlight ruby"><span class="no">ProcessOrder</span></code>. They follow a consistent naming convention: <code class="highlight ruby"><span class="no">Resource</span><span class="o">::</span><span class="no">VerbAction</span></code>. The <code class="highlight ruby"><span class="o">-</span><span class="no">Action</span></code> suffix tells you exactly what you’re looking at — a class that handles a single controller action with declared inputs and a structured response.</p>

<p>I have two recommended directory structures (that work with Rails autoloading):</p>

<figure class="code-example">
  <figcaption class="title">Option 1: standalone directory</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="source text"><pre>
app/actions/
  users/
    create_action.rb
    destroy_action.rb
    index_action.rb
    show_action.rb
    update_action.rb
  orders/
    search_action.rb
    cancel_action.rb
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">Option 2: alongside controllers</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="source text"><pre>
app/controllers/
  users_controller.rb
  users/
    create_action.rb
    destroy_action.rb
    index_action.rb
    show_action.rb
    update_action.rb
  orders_controller.rb
  orders/
    search_action.rb
    cancel_action.rb
</pre></td></tr></tbody></table></div>
</figure>

<p>The second option keeps actions right next to the controllers that call them. Either way, when you see <code class="highlight ruby"><span class="no">Users</span><span class="o">::</span><span class="no">CreateAction</span></code> you know it’s an action class that handles user creation for a controller — not a service object that might be called from anywhere.</p>

<h3 id="playing-well-with-others">Playing well with others</h3>

<p>Because actions are plain Ruby objects that take keyword arguments and return hashes, they compose naturally with whatever gems you’re already using. No adapters, no special imports — just call the library and pass the result to a response helper.</p>

<p><strong>Serialization.</strong> Use Blueprinter, Alba, Oj Serializers, or anything else that gives you a hash:</p>

<figure class="code-example">
  <figcaption class="title">with Blueprinter</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="source ruby"><pre>
<span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="n">params</span><span class="p">:,</span> <span class="n">company</span><span class="p">:)</span>
  <span class="n">user</span> <span class="o">=</span> <span class="n">company</span><span class="p">.</span><span class="nf">users</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:user</span><span class="p">])</span>
  <span class="k">return</span> <span class="no">UnprocessableContent</span><span class="p">(</span><span class="ss">errors: </span><span class="n">user</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">messages</span><span class="p">)</span> <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">any?</span>

  <span class="no">Created</span><span class="p">(</span><span class="ss">resource: </span><span class="no">UserBlueprint</span><span class="p">.</span><span class="nf">render_as_hash</span><span class="p">(</span><span class="n">user</span><span class="p">))</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p><strong>Authorization.</strong> Pass <code class="highlight ruby"><span class="n">current_user</span></code> as context from the controller and check policies inside the action:</p>

<figure class="code-example">
  <figcaption class="title">with Pundit</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="source ruby"><pre>
<span class="k">def</span> <span class="nf">destroy</span><span class="p">(</span><span class="n">params</span><span class="p">:,</span> <span class="n">current_user</span><span class="p">:)</span>
  <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
  <span class="k">unless</span> <span class="no">UserPolicy</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">current_user</span><span class="p">,</span> <span class="n">user</span><span class="p">).</span><span class="nf">destroy?</span>
    <span class="k">return</span> <span class="no">Forbidden</span><span class="p">(</span><span class="ss">errors: </span><span class="p">{</span> <span class="ss">base: </span><span class="p">[</span><span class="s2">"not authorized to delete this user"</span><span class="p">]</span> <span class="p">})</span>
  <span class="k">end</span>

  <span class="n">user</span><span class="p">.</span><span class="nf">destroy!</span>
  <span class="no">NoContent</span><span class="p">()</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p><strong>Pagination.</strong> Paginate with Pagy, cursor pagination, or whatever you prefer — pass metadata through the <code class="highlight ruby"><span class="n">meta</span><span class="p">:</span></code> keyword:</p>

<figure class="code-example">
  <figcaption class="title">with Pagy</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="source ruby"><pre>
<span class="k">class</span> <span class="nc">Users::IndexAction</span>
  <span class="kp">include</span> <span class="no">ActionFigure</span><span class="p">[</span><span class="ss">:jsend</span><span class="p">]</span>
  <span class="kp">include</span> <span class="no">Pagy</span><span class="o">::</span><span class="no">Backend</span>

  <span class="k">def</span> <span class="nf">index</span><span class="p">(</span><span class="n">request</span><span class="p">:,</span> <span class="n">company</span><span class="p">:,</span> <span class="o">**</span><span class="p">)</span>
    <span class="n">pagy</span><span class="p">,</span> <span class="n">users</span> <span class="o">=</span> <span class="n">pagy</span><span class="p">(</span><span class="ss">:keyset</span><span class="p">,</span> <span class="n">company</span><span class="p">.</span><span class="nf">users</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="ss">:name</span><span class="p">),</span> <span class="n">request</span><span class="p">:)</span>
    <span class="n">resource</span> <span class="o">=</span> <span class="no">UserSerializer</span><span class="p">.</span><span class="nf">many</span><span class="p">(</span><span class="n">users</span><span class="p">)</span>
    <span class="no">Ok</span><span class="p">(</span><span class="n">resource</span><span class="p">:,</span> <span class="ss">meta: </span><span class="p">{</span> <span class="ss">next: </span><span class="n">pagy</span><span class="p">.</span><span class="nf">next</span> <span class="p">})</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>The pattern is always the same: context comes in as keyword arguments, the gem does its job, and the result goes out through a response helper.</p>

<h3 id="actions-without-a-schema">Actions without a schema</h3>

<p>Not every action needs parameter validation. Actions that omit <code class="highlight ruby"><span class="n">params_schema</span></code> skip the validation pipeline entirely — any <code class="highlight ruby"><span class="n">params</span><span class="p">:</span></code> passed through are delivered as-is. This is useful when validation is handled upstream (like OpenAPI middleware) or when the action simply doesn’t need params:</p>

<figure class="code-example">
  <figcaption class="title">app/actions/health_check_action.rb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="source ruby"><pre>
<span class="k">class</span> <span class="nc">HealthCheckAction</span>
  <span class="kp">include</span> <span class="no">ActionFigure</span><span class="p">[</span><span class="ss">:jsend</span><span class="p">]</span>

  <span class="k">def</span> <span class="nf">check</span>
    <span class="no">Ok</span><span class="p">(</span><span class="ss">resource: </span><span class="p">{</span> <span class="ss">status: </span><span class="s2">"healthy"</span><span class="p">,</span> <span class="ss">time: </span><span class="no">Time</span><span class="p">.</span><span class="nf">current</span> <span class="p">})</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">app/controllers/health_controller.rb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="source ruby"><pre>
<span class="k">class</span> <span class="nc">HealthController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">show</span>
    <span class="n">render</span> <span class="no">HealthCheckAction</span><span class="p">.</span><span class="nf">check</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<h3 id="fully-articulated">Fully articulated</h3>

<p>The 12-deep service object chains happened because there were no clear roles or boundaries. Nobody knew where they were in the chain, so the chains got longer.</p>

<p>ActionFigure is my answer to that. Each action owns its validation, its logic, and its response shape. One class, one job, fully self-contained. You open the file and you can see what it does and know that it is being used by a controller.</p>

<p><a href="https://github.com/phaedryx/action_figure">ActionFigure on GitHub →</a></p>]]></content><author><name>Tad Thorley</name></author><summary type="html"><![CDATA[I worked on a codebase that had taken the “skinny controllers” and “skinny models” idea to heart by putting everything into service objects. I think that is a great idea if you are purposeful with your service objects. The problem was we weren’t. The stack from controller receiving a request to models updating the database was a dozen or more service objects deep in some places. It was service objects calling service objects calling service objects.]]></summary></entry><entry><title type="html">Lettering Practice Page Generator</title><link href="https://tad.thorley.dev/2026/03/15/lettering-practice-page-generator.html" rel="alternate" type="text/html" title="Lettering Practice Page Generator" /><published>2026-03-15T00:00:00+00:00</published><updated>2026-03-15T00:00:00+00:00</updated><id>https://tad.thorley.dev/2026/03/15/lettering-practice-page-generator</id><content type="html" xml:base="https://tad.thorley.dev/2026/03/15/lettering-practice-page-generator.html"><![CDATA[<p>I’ve been getting into lettering lately (gotta chase that dopamine).
I soon realized that the first thing I needed was practice sheets to try out different letterforms and get my writing more consistent.</p>

<p>However, many of the lettering practice sheets are locked behind email signups and they are fixed at a particular style. I realized that they aren’t that complicated and would be easy to generate.</p>

<p>So I built a <a href="/tools/lettering-practice-page-generator/">Lettering Practice Page Generator</a>.</p>

<p>It’s an interactive tool that lets you configure every aspect of a practice page, upload a letterform to trace, and download it as a PDF. No signup. No email. Just sliders and a download button.</p>

<h3 id="letterform-upload">Letterform upload</h3>

<p>The headline feature: upload a PNG or SVG of any letterform and the generator will place it on your practice sheet. There are three modes:</p>

<ul>
  <li><strong>Reference</strong> — a full-opacity copy at the start of each row, so you have something to look at while you practice</li>
  <li><strong>Tracing</strong> — ghosted copies tiled across the first row, so you can trace directly over them</li>
  <li><strong>Both</strong> — reference on every row, plus tracing on the first</li>
</ul>

<p>Once uploaded, you can fine-tune the placement with three sliders:</p>

<ul>
  <li><strong>Offset</strong> — shifts the letterform up or down, useful for letters with descenders like <em>g</em> or <em>y</em> that need to sit lower relative to the baseline</li>
  <li><strong>Size</strong> — scales the letterform from 25% to 200% of the row height</li>
  <li><strong>Spacing</strong> — controls the gap between tracing copies across the row</li>
</ul>

<h3 id="guide-lines">Guide lines</h3>

<p>The generator has controls for the five horizontal guide lines that matter in lettering:</p>

<ul>
  <li><strong>Ascender</strong> — the top boundary for tall letters like <em>b</em>, <em>d</em>, <em>h</em></li>
  <li><strong>Cap height</strong> — where capital letters reach</li>
  <li><strong>X-height</strong> — the height of lowercase letters like <em>a</em>, <em>e</em>, <em>o</em></li>
  <li><strong>Baseline</strong> — where letters sit</li>
  <li><strong>Descender</strong> — how far below the baseline letters like <em>g</em>, <em>p</em>, <em>y</em> drop</li>
</ul>

<p>Each line has a distinct color and style so you can tell them apart at a glance. The baseline is a solid dark line. The cap height is solid orange. The x-height is dotted blue. Ascender and descender lines are dashed gray.</p>

<p>Beyond the lines themselves, you can adjust:</p>

<ul>
  <li><strong>Slant angle</strong> — anywhere from -45° to +45° for scripts that require angled guides</li>
  <li><strong>Slant spacing</strong> — how far apart the slant lines are</li>
  <li><strong>Rows</strong> — how many line sets fit on the page</li>
  <li><strong>Gap</strong> — vertical space between rows</li>
  <li><strong>Margin</strong> — page margins</li>
</ul>

<p>Everything updates in real time on a canvas preview, so you can see exactly what you’ll get before downloading.</p>

<h3 id="the-pdf">The PDF</h3>

<p>The download generates a letter-sized PDF with all of your settings preserved. The line weights, colors, dash patterns, and letterform placement match the preview exactly. Print it, grab a pen, and practice.</p>

<p>That’s it. No account, no email list, no watermarks. Just a tool that makes the practice sheets I wanted.</p>

<p><a href="/tools/lettering-practice-page-generator/">Try it out →</a></p>]]></content><author><name>Tad Thorley</name></author><summary type="html"><![CDATA[I’ve been getting into lettering lately (gotta chase that dopamine). I soon realized that the first thing I needed was practice sheets to try out different letterforms and get my writing more consistent.]]></summary></entry><entry><title type="html">Ruby Regular Expression Editor</title><link href="https://tad.thorley.dev/2026/03/14/ruby-regular-expression-editor.html" rel="alternate" type="text/html" title="Ruby Regular Expression Editor" /><published>2026-03-14T00:00:00+00:00</published><updated>2026-03-14T00:00:00+00:00</updated><id>https://tad.thorley.dev/2026/03/14/ruby-regular-expression-editor</id><content type="html" xml:base="https://tad.thorley.dev/2026/03/14/ruby-regular-expression-editor.html"><![CDATA[<p>I have always liked <a href="https://rubular.com/">Rubular</a>. It is simple, focused, and does exactly what it says. You paste in a regex, you paste in a string, and you see what matches. I have used it for years.</p>

<p>But Rubular, as of this post, is still using Ruby 2.5.9. I thought it would be interesting to have a more modern version. Also, since this is a statically generated blog I wanted to see if I could build something similar that runs entirely in the browser.</p>

<h3 id="rubywasm">ruby.wasm</h3>

<p><a href="https://github.com/ruby/ruby.wasm">ruby.wasm</a> is a build of CRuby compiled to WebAssembly. You can load it with a single script tag:</p>

<figure class="code-example">
  <figcaption class="title">loading ruby.wasm</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
</pre></td><td class="source html"><pre>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.8.1/dist/browser.script.iife.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Once loaded, you can write Ruby directly in the page:</p>

<figure class="code-example">
  <figcaption class="title">inline Ruby</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="source html"><pre>
<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/ruby"</span><span class="nt">&gt;</span>
  <span class="nx">require</span> <span class="dl">"</span><span class="s2">js</span><span class="dl">"</span>
  <span class="nx">JS</span><span class="p">.</span><span class="nb">global</span><span class="p">[:</span><span class="nb">document</span><span class="p">].</span><span class="nf">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#output</span><span class="dl">"</span><span class="p">)[:</span><span class="nx">textContent</span><span class="p">]</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Hello from Ruby</span><span class="dl">"</span>
<span class="nt">&lt;/script&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>The <code class="highlight ruby"><span class="n">js</span></code> gem bridges Ruby and JavaScript. You can read DOM elements, set properties, and attach event listeners. The Ruby code runs in the browser with no server involved.</p>

<h3 id="the-regex-engine">The regex engine</h3>

<p>The core of the editor is a Ruby function that takes a pattern, flags, and test string, then returns structured match data as JSON:</p>

<figure class="code-example">
  <figcaption class="title">regex matching</figcaption>
  <aside>This runs in the browser via ruby.wasm</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
</pre></td><td class="source ruby"><pre>
<span class="k">def</span> <span class="nf">run_regex</span><span class="p">(</span><span class="n">pattern_str</span><span class="p">,</span> <span class="n">flags_str</span><span class="p">,</span> <span class="n">test_str</span><span class="p">,</span> <span class="n">sub_str</span><span class="p">)</span>
  <span class="n">opts</span> <span class="o">=</span> <span class="mi">0</span>
  <span class="n">opts</span> <span class="o">|=</span> <span class="no">Regexp</span><span class="o">::</span><span class="no">IGNORECASE</span> <span class="k">if</span> <span class="n">flags_str</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="s2">"i"</span><span class="p">)</span>
  <span class="n">opts</span> <span class="o">|=</span> <span class="no">Regexp</span><span class="o">::</span><span class="no">MULTILINE</span> <span class="k">if</span> <span class="n">flags_str</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="s2">"m"</span><span class="p">)</span>
  <span class="n">opts</span> <span class="o">|=</span> <span class="no">Regexp</span><span class="o">::</span><span class="no">EXTENDED</span> <span class="k">if</span> <span class="n">flags_str</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="s2">"x"</span><span class="p">)</span>

  <span class="n">regex</span> <span class="o">=</span> <span class="no">Regexp</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">pattern_str</span><span class="p">,</span> <span class="n">opts</span><span class="p">)</span>

  <span class="n">matches</span> <span class="o">=</span> <span class="p">[]</span>
  <span class="n">offset</span> <span class="o">=</span> <span class="mi">0</span>

  <span class="k">while</span> <span class="p">(</span><span class="n">m</span> <span class="o">=</span> <span class="n">regex</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="n">test_str</span><span class="p">,</span> <span class="n">offset</span><span class="p">))</span>
    <span class="n">match_data</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">full: </span><span class="n">m</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
      <span class="ss">index: </span><span class="n">m</span><span class="p">.</span><span class="nf">begin</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span>
      <span class="ss">length: </span><span class="n">m</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">length</span><span class="p">,</span>
      <span class="ss">groups: </span><span class="p">[]</span>
    <span class="p">}</span>

    <span class="p">(</span><span class="mi">1</span><span class="o">...</span><span class="n">m</span><span class="p">.</span><span class="nf">size</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span>
      <span class="n">group</span> <span class="o">=</span> <span class="p">{</span><span class="ss">number: </span><span class="n">i</span><span class="p">,</span> <span class="ss">value: </span><span class="n">m</span><span class="p">[</span><span class="n">i</span><span class="p">]}</span>
      <span class="k">if</span> <span class="n">i</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">&lt;</span> <span class="n">m</span><span class="p">.</span><span class="nf">names</span><span class="p">.</span><span class="nf">length</span> <span class="o">&amp;&amp;</span> <span class="n">m</span><span class="p">.</span><span class="nf">names</span><span class="p">[</span><span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span>
        <span class="n">group</span><span class="p">[</span><span class="ss">:name</span><span class="p">]</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">names</span><span class="p">[</span><span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span>
      <span class="k">end</span>
      <span class="n">match_data</span><span class="p">[</span><span class="ss">:groups</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="n">group</span>
    <span class="k">end</span>

    <span class="n">matches</span> <span class="o">&lt;&lt;</span> <span class="n">match_data</span>

    <span class="n">new_offset</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">end</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">new_offset</span> <span class="o">+=</span> <span class="mi">1</span> <span class="k">if</span> <span class="n">new_offset</span> <span class="o">==</span> <span class="n">offset</span>
    <span class="k">break</span> <span class="k">if</span> <span class="n">new_offset</span> <span class="o">&gt;</span> <span class="n">test_str</span><span class="p">.</span><span class="nf">length</span>
    <span class="n">offset</span> <span class="o">=</span> <span class="n">new_offset</span>
  <span class="k">end</span>

  <span class="p">{</span><span class="ss">error: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">matches: </span><span class="n">matches</span><span class="p">}.</span><span class="nf">to_json</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>This is real <code class="highlight ruby"><span class="no">Regexp</span></code>. Not a JavaScript approximation. Ruby-specific features like <code class="highlight ruby"><span class="p">\</span><span class="no">A</span></code>, <code class="highlight ruby"><span class="p">\</span><span class="n">z</span></code>, <code class="highlight ruby"><span class="p">\</span><span class="n">h</span></code>, <code class="highlight ruby"><span class="p">\</span><span class="no">K</span></code>, <code class="highlight ruby"><span class="p">\</span><span class="no">R</span></code>, and <code class="highlight ruby"><span class="p">\</span><span class="nb">p</span><span class="p">{</span><span class="no">L</span><span class="p">}</span></code> all work because it is the actual Ruby regex engine running in the browser.</p>

<h3 id="connecting-ruby-to-the-dom">Connecting Ruby to the DOM</h3>

<p>The function is exposed to JavaScript through a <code class="highlight ruby"><span class="no">Proc</span></code>:</p>

<figure class="code-example">
  <figcaption class="title">exposing to JavaScript</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="source ruby"><pre>
<span class="no">JS</span><span class="p">.</span><span class="nf">global</span><span class="p">[</span><span class="ss">:rubyRegex</span><span class="p">]</span> <span class="o">=</span> <span class="no">Proc</span><span class="p">.</span><span class="nf">new</span> <span class="p">{</span> <span class="o">|</span><span class="n">pattern</span><span class="p">,</span> <span class="n">flags</span><span class="p">,</span> <span class="n">text</span><span class="p">,</span> <span class="nb">sub</span><span class="o">|</span>
  <span class="n">run_regex</span><span class="p">(</span><span class="n">pattern</span><span class="p">.</span><span class="nf">to_s</span><span class="p">,</span> <span class="n">flags</span><span class="p">.</span><span class="nf">to_s</span><span class="p">,</span> <span class="n">text</span><span class="p">.</span><span class="nf">to_s</span><span class="p">,</span> <span class="nb">sub</span><span class="p">.</span><span class="nf">to_s</span><span class="p">)</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>On the JavaScript side, a thin layer reads the inputs, calls <code class="highlight javascript"><span class="nb">window</span><span class="p">.</span><span class="nf">rubyRegex</span><span class="p">()</span></code>, and renders the results. Match positions come back as indices, so building highlighted HTML is straightforward:</p>

<figure class="code-example">
  <figcaption class="title">match highlighting</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="source javascript"><pre>
<span class="kd">function</span> <span class="nf">buildHighlightedHTML</span><span class="p">(</span><span class="nx">text</span><span class="p">,</span> <span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">result</span> <span class="o">=</span> <span class="dl">''</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">lastIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

  <span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">match</span> <span class="k">of</span> <span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">start</span> <span class="o">=</span> <span class="nx">match</span><span class="p">.</span><span class="nx">index</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">end</span> <span class="o">=</span> <span class="nx">start</span> <span class="o">+</span> <span class="nx">match</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
    <span class="nx">result</span> <span class="o">+=</span> <span class="nf">escapeHTML</span><span class="p">(</span><span class="nx">text</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="nx">lastIndex</span><span class="p">,</span> <span class="nx">start</span><span class="p">));</span>
    <span class="nx">result</span> <span class="o">+=</span> <span class="dl">'</span><span class="s1">&lt;mark&gt;</span><span class="dl">'</span> <span class="o">+</span> <span class="nf">escapeHTML</span><span class="p">(</span><span class="nx">match</span><span class="p">.</span><span class="nx">full</span><span class="p">)</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">&lt;/mark&gt;</span><span class="dl">'</span><span class="p">;</span>
    <span class="nx">lastIndex</span> <span class="o">=</span> <span class="nx">end</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">result</span> <span class="o">+=</span> <span class="nf">escapeHTML</span><span class="p">(</span><span class="nx">text</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="nx">lastIndex</span><span class="p">));</span>
  <span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Input events fire with a 100ms debounce, which keeps things responsive without overwhelming the WASM runtime.</p>

<h3 id="the-tool">The tool</h3>

<p>The editor has a few features worth mentioning:</p>

<ul>
  <li><strong>Flag toggles</strong> for <code class="highlight ruby"><span class="n">i</span></code> (case insensitive), <code class="highlight ruby"><span class="n">m</span></code> (multiline), and <code class="highlight ruby"><span class="n">x</span></code> (extended mode)</li>
  <li>A <strong>gsub mode</strong> that shows the substitution result when toggled on</li>
  <li><strong>Generated Ruby code</strong> with syntax highlighting, so you can copy the exact <code class="highlight ruby"><span class="p">.</span><span class="nf">scan</span></code> or <code class="highlight ruby"><span class="p">.</span><span class="nf">gsub</span></code> call into your project</li>
  <li><strong>Permalinks</strong> that encode the regex, test string, and flags in the URL for sharing</li>
</ul>

<p>The loading time for ruby.wasm is noticeable—a few seconds on first visit while the WASM binary downloads. After that, everything runs instantly.</p>

<p><a href="/tools/ruby-regex-editor/">Try it out →</a></p>]]></content><author><name>Tad Thorley</name></author><summary type="html"><![CDATA[I have always liked Rubular. It is simple, focused, and does exactly what it says. You paste in a regex, you paste in a string, and you see what matches. I have used it for years.]]></summary></entry><entry><title type="html">RoboWar</title><link href="https://tad.thorley.dev/2026/01/07/robowar.html" rel="alternate" type="text/html" title="RoboWar" /><published>2026-01-07T00:00:00+00:00</published><updated>2026-01-07T00:00:00+00:00</updated><id>https://tad.thorley.dev/2026/01/07/robowar</id><content type="html" xml:base="https://tad.thorley.dev/2026/01/07/robowar.html"><![CDATA[<p>I first encountered <a href="https://en.wikipedia.org/wiki/RoboWar">RoboWar</a> as a teenager in high school. It let you program circular “robots” that would battle each other in a small arena. The unpaid shareware version only allowed 100 instructions—barely enough to move and shoot—so my robots weren’t great, but it was a lot of fun anyway and the programming language was a completely new paradigm for me. I’ve been looking for an activity for my <a href="https://urug.org/">local programming club</a> and thought it would be fun to revisit. Since most of the original documentation has vanished from the web, I wrote this article to restore it.</p>

<h2 id="robotalk-programming-language">RoboTalk Programming Language</h2>
<aside>It helped me understand my first HP calculator when I was taking engineering classes</aside>
<p>RoboTalk is a stack-based language using postfix (Reverse Polish) notation.
Instead of writing <code class="language-plaintext highlighter-rouge">5 + 3</code>, you write <code class="language-plaintext highlighter-rouge">5 3 +</code>. Values are pushed onto a stack,
and operators consume values from the stack and push results back.</p>

<h3 id="basic-syntax">Basic Syntax</h3>

<ul>
  <li><strong>Labels</strong> are defined with a colon, e.g <code class="language-plaintext highlighter-rouge">Main:</code></li>
  <li><strong>Comments</strong> start with <code class="language-plaintext highlighter-rouge">#</code> or are enclosed in <code class="language-plaintext highlighter-rouge">{ }</code></li>
  <li><strong>Variables</strong> are read by name and written with a quote: <code class="language-plaintext highlighter-rouge">aim</code> reads the aim register,
<code class="language-plaintext highlighter-rouge">aim'</code> writes to it</li>
  <li><strong>Storing values</strong> uses the pattern: <code class="language-plaintext highlighter-rouge">90 aim' sto</code> (stores 90 in aim)</li>
</ul>

<table class="styled">
  <caption>Arithmetic Operators</caption>
  <thead>
    <tr>
      <th>Operator</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">+</code></td>
      <td>Addition</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-</code></td>
      <td>Subtraction</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">*</code></td>
      <td>Multiplication</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/</code></td>
      <td>Division</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MOD</code></td>
      <td>Modulo (remainder)</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Math Functions</caption>
  <thead>
    <tr>
      <th>Operator</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SIN</code> / <code class="language-plaintext highlighter-rouge">SINE</code></td>
      <td>Sine</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">COS</code> / <code class="language-plaintext highlighter-rouge">COSINE</code></td>
      <td>Cosine</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TAN</code> / <code class="language-plaintext highlighter-rouge">TANGENT</code></td>
      <td>Tangent</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ARCSIN</code></td>
      <td>Inverse sine</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ARCCOS</code></td>
      <td>Inverse cosine</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ARCTAN</code></td>
      <td>Inverse tangent</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SQRT</code></td>
      <td>Square root</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ABS</code></td>
      <td>Absolute value</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CHS</code></td>
      <td>Change sign (negate)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MAX</code></td>
      <td>Maximum of two values</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MIN</code></td>
      <td>Minimum of two values</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DIST</code></td>
      <td>Distance calculation</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Comparison Operators</caption>
  <thead>
    <tr>
      <th>Operator</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&gt;</code></td>
      <td>Greater than</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;</code></td>
      <td>Less than</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">=</code></td>
      <td>Equal to</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">!</code></td>
      <td>Not equal to</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Logical Operators</caption>
  <thead>
    <tr>
      <th>Operator</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AND</code></td>
      <td>Logical AND</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OR</code></td>
      <td>Logical OR</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">XOR</code> / <code class="language-plaintext highlighter-rouge">EOR</code></td>
      <td>Exclusive OR</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">NOT</code></td>
      <td>Logical NOT</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Stack Manipulation</caption>
  <thead>
    <tr>
      <th>Operator</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DROP</code></td>
      <td>Remove top value from stack</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DROPALL</code></td>
      <td>Clear entire stack</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DUP</code></td>
      <td>Duplicate top value</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SWAP</code></td>
      <td>Exchange top two values</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ROLL</code></td>
      <td>Rotate stack values</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">NOP</code></td>
      <td>No operation</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Control Flow</caption>
  <thead>
    <tr>
      <th>Operator</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">IF</code></td>
      <td>Conditional jump (saves return address)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">IFG</code></td>
      <td>Conditional goto (no return address)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">IFE</code></td>
      <td>Conditional jump if equal</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">IFEG</code></td>
      <td>Conditional goto if equal</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">JUMP</code></td>
      <td>Unconditional jump</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CALL</code></td>
      <td>Call subroutine</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">RETURN</code></td>
      <td>Return from subroutine</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">RTI</code></td>
      <td>Return from interrupt</td>
    </tr>
  </tbody>
</table>

<p>The <code class="language-plaintext highlighter-rouge">IF</code> statement works like this: <code class="language-plaintext highlighter-rouge">value1 value2 operator label IF</code>. For
example, <code class="language-plaintext highlighter-rouge">range 0 &gt; ShootEm if</code> means “if range &gt; 0, jump to ShootEm and
save the return address.”</p>

<h3 id="variables-a-z">Variables (A-Z)</h3>

<p>You have 26 general-purpose variables named A through Z. Read them by name,
write to them with a quote suffix:</p>

<figure class="code-example">
  <figcaption class="title">sample label code</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="source text"><pre>
10 a' sto    # Store 10 in variable A
a 5 + b' sto # Store A + 5 in variable B
</pre></td></tr></tbody></table></div>
</figure>

<p>Note: <code class="language-plaintext highlighter-rouge">X</code> and <code class="language-plaintext highlighter-rouge">Y</code> are special—they return the robot’s coordinates and are
read-only.</p>

<h3 id="robot-state-registers">Robot State Registers</h3>

<p>These registers let you read and control your robot:</p>

<table class="styled">
  <caption>Robot State Registers</caption>
  <thead>
    <tr>
      <th>Register</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">X</code></td>
      <td>Robot X coordinate (read-only)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Y</code></td>
      <td>Robot Y coordinate (read-only)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AIM</code></td>
      <td>Turret aim angle (0-359 degrees)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SPEEDX</code></td>
      <td>Horizontal velocity (-20 to 20)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SPEEDY</code></td>
      <td>Vertical velocity (-20 to 20)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ENERGY</code></td>
      <td>Current energy level</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DAMAGE</code></td>
      <td>Current health</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SHIELD</code></td>
      <td>Current shield level</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Sensor Registers</caption>
  <thead>
    <tr>
      <th>Register</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">RANGE</code></td>
      <td>Distance to robot in line of sight (0 if none)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">RADAR</code></td>
      <td>Distance to nearest bullet (0 if none)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">LOOK</code></td>
      <td>Direction being scanned</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SCAN</code></td>
      <td>Scan for targets</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">WALL</code></td>
      <td>Distance to wall</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ROBOTS</code></td>
      <td>Number of robots remaining in arena</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">RANDOM</code></td>
      <td>Random number</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">COLLISION</code></td>
      <td>Collision detection</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DOPPLER</code></td>
      <td>Doppler shift (for leading shots)</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Communication Registers</caption>
  <thead>
    <tr>
      <th>Register</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CHANNEL</code></td>
      <td>Communication channel</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SIGNAL</code></td>
      <td>Signal value</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FRIEND</code></td>
      <td>Friendly robot detection</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TEAMMATES</code></td>
      <td>Teammate count</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ID</code></td>
      <td>Robot ID (0-5)</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Other Registers</caption>
  <thead>
    <tr>
      <th>Register</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PROBE</code></td>
      <td>Probe sensor data</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HISTORY</code></td>
      <td>Battle history</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">KILLS</code></td>
      <td>Kill count</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CHRONON</code></td>
      <td>Current game tick</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">NEAREST</code></td>
      <td>Nearest object</td>
    </tr>
  </tbody>
</table>

<h3 id="weapon-commands">Weapon Commands</h3>

<p>Write to these registers to fire weapons:</p>

<table class="styled">
  <caption>Weapon Commands</caption>
  <thead>
    <tr>
      <th>Command</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FIRE'</code></td>
      <td>Fire current weapon</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">BULLET'</code></td>
      <td>Fire bullet</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MISSILE'</code></td>
      <td>Fire missile</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">LASER'</code></td>
      <td>Fire laser</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HELLBORE'</code></td>
      <td>Fire hellbore</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">STUNNER'</code></td>
      <td>Fire stunner</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MINE'</code></td>
      <td>Deploy mine</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DRONE'</code></td>
      <td>Deploy drone</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">NUKE'</code></td>
      <td>Fire tactical nuke</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MEGANUKE'</code></td>
      <td>Fire mega nuke</td>
    </tr>
  </tbody>
</table>

<p>The value you write determines the energy spent (and thus damage dealt):
<code class="language-plaintext highlighter-rouge">15 fire' sto</code> fires with 15 energy.</p>

<table class="styled">
  <caption>Movement Commands</caption>
  <thead>
    <tr>
      <th>Command</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MOVEX'</code></td>
      <td>Set horizontal movement</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MOVEY'</code></td>
      <td>Set vertical movement</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SPEEDX'</code></td>
      <td>Set horizontal speed</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SPEEDY'</code></td>
      <td>Set vertical speed</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Interrupt Control</caption>
  <thead>
    <tr>
      <th>Command</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">INTON</code></td>
      <td>Enable interrupts</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">INTOFF</code></td>
      <td>Disable interrupts</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FLUSHINT</code></td>
      <td>Flush interrupt queue</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SETINT</code></td>
      <td>Set interrupt handler</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Display and Debug</caption>
  <thead>
    <tr>
      <th>Command</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PRINT</code></td>
      <td>Display value</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DEBUG</code> / <code class="language-plaintext highlighter-rouge">DEBUGGER</code></td>
      <td>Enter debug mode</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">BEEP</code></td>
      <td>Play sound</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ICON0</code> - <code class="language-plaintext highlighter-rouge">ICON9</code></td>
      <td>Display robot icons</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SND0</code> - <code class="language-plaintext highlighter-rouge">SND9</code></td>
      <td>Play sounds</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SYNC</code></td>
      <td>Synchronize</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">END</code></td>
      <td>End program</td>
    </tr>
  </tbody>
</table>

<h2 id="hardware-configuration">Hardware Configuration</h2>

<p>When building a robot, you spend Hardware Points on various attributes and
weapons. The total determines your robot’s class.</p>

<table class="styled">
  <caption>Robot Classes</caption>
  <thead>
    <tr>
      <th>Points</th>
      <th>Class</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0-2</td>
      <td>Little Leaguer</td>
    </tr>
    <tr>
      <td>3-9</td>
      <td>Mortal</td>
    </tr>
    <tr>
      <td>10-23</td>
      <td>Titan</td>
    </tr>
  </tbody>
</table>

<p>Most competitive play uses the 9-point “Mortal” class.</p>

<table class="styled">
  <caption>Energy Capacity</caption>
  <thead>
    <tr>
      <th>Max Energy</th>
      <th>Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>150</td>
      <td>3 points</td>
    </tr>
    <tr>
      <td>100</td>
      <td>2 points</td>
    </tr>
    <tr>
      <td>60</td>
      <td>1 point</td>
    </tr>
    <tr>
      <td>40</td>
      <td>0 points</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Damage Capacity (Health)</caption>
  <thead>
    <tr>
      <th>Max Damage</th>
      <th>Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>150</td>
      <td>3 points</td>
    </tr>
    <tr>
      <td>100</td>
      <td>2 points</td>
    </tr>
    <tr>
      <td>60</td>
      <td>1 point</td>
    </tr>
    <tr>
      <td>30</td>
      <td>0 points</td>
    </tr>
  </tbody>
</table>

<table class="styled">
  <caption>Shield Capacity</caption>
  <thead>
    <tr>
      <th>Max Shield</th>
      <th>Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>100</td>
      <td>3 points</td>
    </tr>
    <tr>
      <td>50</td>
      <td>2 points</td>
    </tr>
    <tr>
      <td>25</td>
      <td>1 point</td>
    </tr>
    <tr>
      <td>0</td>
      <td>0 points</td>
    </tr>
  </tbody>
</table>

<h3 id="processor-speed">Processor Speed</h3>

<p>This determines how many instructions your robot executes per chronon (game tick):</p>

<table class="styled">
  <caption>Processor Speed</caption>
  <thead>
    <tr>
      <th>Cycles</th>
      <th>Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>50</td>
      <td>4 points</td>
    </tr>
    <tr>
      <td>30</td>
      <td>3 points</td>
    </tr>
    <tr>
      <td>15</td>
      <td>2 points</td>
    </tr>
    <tr>
      <td>10</td>
      <td>1 point</td>
    </tr>
    <tr>
      <td>5</td>
      <td>0 points</td>
    </tr>
  </tbody>
</table>

<h3 id="bullet-types">Bullet Types</h3>

<p>Choose one (mutually exclusive):</p>

<table class="styled">
  <caption>Bullet Types</caption>
  <thead>
    <tr>
      <th>Type</th>
      <th>Cost</th>
      <th>Damage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Explosive</td>
      <td>2 points</td>
      <td>Energy × 2</td>
    </tr>
    <tr>
      <td>Normal</td>
      <td>1 point</td>
      <td>Energy × 1</td>
    </tr>
    <tr>
      <td>Rubber</td>
      <td>0 points</td>
      <td>Energy ÷ 2</td>
    </tr>
  </tbody>
</table>

<h3 id="weapons">Weapons</h3>

<p>Each weapon costs 1 Hardware Point. Choosing the right weapons for your
strategy is crucial.</p>

<h4 id="bullets-fire--bullet">Bullets (FIRE / BULLET)</h4>

<p>Bullets are your primary weapon and don’t cost extra hardware points—they’re
included with every robot. They travel at speed 12 and deal damage based on
your bullet type (Explosive, Normal, or Rubber). Bullets are versatile and
energy-efficient, making them the workhorse weapon for most robots. Use the
<code class="language-plaintext highlighter-rouge">BULLET'</code> command when you have explosive bullets but want to fire a normal
shot to avoid splash damage at close range.</p>

<ul>
  <li><strong>Damage</strong>: Depends on bullet type (Explosive: Energy × 2, Normal: Energy × 1, Rubber: Energy ÷ 2)</li>
  <li><strong>Speed</strong>: 12</li>
  <li><strong>Best for</strong>: General combat, consistent damage output</li>
</ul>

<h4 id="missiles">Missiles</h4>

<p>Missiles are slow but devastating. They travel at only speed 5, making them
easy to dodge at long range, but they deal double damage (Energy × 2). Missiles
excel at close-range combat where the enemy has less time to react, or against
stationary/predictable targets. The slow speed makes leading shots more
difficult—you need to anticipate where the enemy will be much further in
advance.</p>

<ul>
  <li><strong>Damage</strong>: Energy × 2</li>
  <li><strong>Speed</strong>: 5</li>
  <li><strong>Cost</strong>: 1 hardware point</li>
  <li><strong>Best for</strong>: Close-range brawling, high-damage ambushes</li>
</ul>

<h4 id="hellbores">Hellbores</h4>

<p>Hellbores are unique because their speed equals the energy you put into them.
Fire with 4 energy and it moves at speed 4; fire with 20 energy and it moves
at speed 20. This makes them incredibly versatile—slow and powerful or fast
and light. The tradeoff is you must fire between 4-20 energy per shot (no
tiny plinks or massive blasts). Advanced robots use hellbores to vary their
projectile timing and confuse enemies that try to dodge predictable shots.</p>

<ul>
  <li><strong>Damage</strong>: Energy × 1</li>
  <li><strong>Speed</strong>: Equal to energy invested (4-20)</li>
  <li><strong>Cost</strong>: 1 hardware point</li>
  <li><strong>Best for</strong>: Unpredictable attacks, skilled players who can exploit variable timing</li>
</ul>

<h4 id="lasers">Lasers</h4>

<p>Lasers hit instantly—no travel time, no leading required. Point at an enemy,
fire, and they take damage immediately. The catch? They only deal 1/5 of the
energy invested as damage, making them very energy-inefficient. Lasers also
require line-of-sight to a living robot; you can’t fire into empty space.
They’re ideal for finishing off weak enemies or for robots that struggle with
leading shots.</p>

<ul>
  <li><strong>Damage</strong>: Energy ÷ 5</li>
  <li><strong>Speed</strong>: Instant</li>
  <li><strong>Cost</strong>: 1 hardware point</li>
  <li><strong>Best for</strong>: Guaranteed hits, finishing blows, simple aiming logic</li>
</ul>

<h4 id="stunners">Stunners</h4>

<p>Stunners don’t just damage enemies—they temporarily freeze them in place,
stopping their program execution. This gives you time to line up follow-up
shots or escape. The damage is low (Energy ÷ 4), so stunners work best as a
tactical tool rather than a primary weapon. Stun an enemy, then blast them
with missiles while they can’t dodge.</p>

<ul>
  <li><strong>Damage</strong>: Energy ÷ 4</li>
  <li><strong>Speed</strong>: 14</li>
  <li><strong>Cost</strong>: 1 hardware point</li>
  <li><strong>Best for</strong>: Crowd control, setting up kills, disabling dangerous enemies</li>
</ul>

<h4 id="mines">Mines</h4>

<p>Mines are deployed at your current location and sit there until an enemy
touches them. They’re defensive weapons—drop them while retreating or place
them in areas enemies are likely to pass through. The damage formula
(2 × (Energy - 5)) means you need at least 6 energy to deploy one, with 5
energy as overhead. Mines are great for area denial and punishing aggressive
pursuers.</p>

<ul>
  <li><strong>Damage</strong>: 2 × (Energy - 5)</li>
  <li><strong>Minimum energy</strong>: 6</li>
  <li><strong>Cost</strong>: 1 hardware point</li>
  <li><strong>Best for</strong>: Defense, area denial, punishing chasers</li>
</ul>

<h4 id="drones">Drones</h4>

<p>Drones are autonomous homing projectiles. Once launched, they track and chase
the target robot for 100 chronons before expiring. They deal half damage
(Energy ÷ 2) but don’t require you to aim—the drone does the work. Drones are
excellent against evasive enemies and let your robot focus on dodging while
the drone handles offense. The downside is their limited lifespan and reduced
damage.</p>

<ul>
  <li><strong>Damage</strong>: Energy ÷ 2</li>
  <li><strong>Lifespan</strong>: 100 chronons</li>
  <li><strong>Cost</strong>: 1 hardware point</li>
  <li><strong>Best for</strong>: Fire-and-forget attacks, fighting evasive enemies, multitasking</li>
</ul>

<h4 id="tacnukes-tactical-nukes">TacNukes (Tactical Nukes)</h4>

<p>TacNukes explode at your current position, dealing area damage to nearby
enemies. They’re suicide weapons in a sense—you need to get close to use them
effectively, and you might catch yourself in the blast. They’re best used as
a desperation move when you’re about to die anyway, or by robots designed
around hit-and-run tactics.</p>

<ul>
  <li><strong>Damage</strong>: Energy × 1 (area effect)</li>
  <li><strong>Cost</strong>: 1 hardware point</li>
  <li><strong>Best for</strong>: Desperation attacks, close-range specialists, mutual destruction</li>
</ul>

<h4 id="meganukes">MegaNukes</h4>

<p>MegaNukes are larger, more powerful versions of TacNukes with a 20-unit blast
radius and 1.5× damage multiplier. They’re the ultimate area denial weapon
but share the same risks—you need to be close, and you might hurt yourself.
Use them when surrounded or when you’ve cornered an enemy and want to ensure
the kill.</p>

<ul>
  <li><strong>Damage</strong>: Energy × 1.5 (area effect)</li>
  <li><strong>Blast radius</strong>: 20 units</li>
  <li><strong>Cost</strong>: 1 hardware point</li>
  <li><strong>Best for</strong>: Large area attacks, ensuring kills, last stands</li>
</ul>

<h4 id="probes">Probes</h4>

<p>Probes aren’t weapons—they’re sensors that reveal enemy information. When you
have probes equipped, you can query a target robot’s stats: their remaining
damage (health), current energy, shield level, ID, aim direction, and more.
This intelligence lets you make smarter decisions: focus fire on wounded
enemies, avoid well-shielded ones, or predict where they’re aiming.</p>

<ul>
  <li><strong>Damage</strong>: None (sensor only)</li>
  <li><strong>Cost</strong>: 1 hardware point</li>
  <li><strong>Best for</strong>: Intelligence gathering, target prioritization, advanced tactics</li>
</ul>

<h3 id="probe-modes">Probe Modes</h3>

<p>When you have probes equipped, write a mode value to query enemy stats:</p>

<table class="styled">
  <caption>Probe Modes</caption>
  <thead>
    <tr>
      <th>Mode</th>
      <th>Returns</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>Target’s Damage</td>
    </tr>
    <tr>
      <td>1</td>
      <td>Target’s Energy</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Target’s Shield</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Target’s ID</td>
    </tr>
    <tr>
      <td>4</td>
      <td>Teammate Count</td>
    </tr>
    <tr>
      <td>5</td>
      <td>Target’s Aim</td>
    </tr>
    <tr>
      <td>6</td>
      <td>Target’s Look</td>
    </tr>
    <tr>
      <td>7</td>
      <td>Target’s Scan</td>
    </tr>
  </tbody>
</table>

<h2 id="game-mechanics">Game Mechanics</h2>

<h3 id="movement">Movement</h3>

<ul>
  <li>Maximum speed: 20 in any direction</li>
  <li>Movement cost: 2 × speed in energy per chronon</li>
  <li>Reversing direction costs double (going from +4 to -4 costs 16 energy)</li>
  <li>Speed persists until changed</li>
  <li>Arena boundaries: 0-300 on both X and Y axes</li>
</ul>

<h3 id="weapon-speeds">Weapon Speeds</h3>

<p>For leading your shots, you need to know projectile speeds:</p>

<table class="styled">
  <caption>Weapon Speeds</caption>
  <thead>
    <tr>
      <th>Weapon</th>
      <th>Speed</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bullets</td>
      <td>12</td>
    </tr>
    <tr>
      <td>Stunners</td>
      <td>14</td>
    </tr>
    <tr>
      <td>Missiles</td>
      <td>5</td>
    </tr>
    <tr>
      <td>Hellbores</td>
      <td>Energy invested</td>
    </tr>
    <tr>
      <td>Lasers</td>
      <td>Instant</td>
    </tr>
  </tbody>
</table>

<h3 id="leading-shots">Leading Shots</h3>

<p>To hit a moving target, use the doppler register with arctan:</p>

<figure class="code-example">
  <figcaption class="title">leading calculation code</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="source text"><pre>
doppler -12 arctan aim + aim' sto  # Lead for bullets
10 fire' sto
</pre></td></tr></tbody></table></div>
</figure>

<p>The formula compensates for target movement based on the weapon’s speed.</p>

<h3 id="example-robot">Example Robot</h3>

<p>Here’s a simple robot that demonstrates the basics:</p>

<figure class="code-example">
  <figcaption class="title">Simple Robot</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="source text"><pre>
# Simple robot that rotates and shoots

Main:
  range 0 &gt; ShootEm if    # If we see a robot, shoot it
  aim 5 + aim' sto        # Otherwise, rotate turret 5 degrees
  Main jump               # Loop forever

ShootEm:
  10 fire' sto            # Fire with 10 energy
  return                  # Return to scanning
</pre></td></tr></tbody></table></div>
</figure>

<p>This robot continuously rotates its turret. When <code class="language-plaintext highlighter-rouge">range</code> returns a non-zero
value (meaning there’s a robot in the line of fire), it jumps to <code class="language-plaintext highlighter-rouge">ShootEm</code>
and fires.</p>

<h3 id="a-more-advanced-robot">A More Advanced Robot</h3>

<figure class="code-example">
  <figcaption class="title">Advanced Robot</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="source text"><pre>
# Robot with movement and leading shots

Main:
  5 speedx' sto           # Start moving right

Scan:
  range 0 &gt; Attack if     # Found a target?
  aim 7 + aim' sto        # Rotate turret
  Scan jump

Attack:
  doppler -12 arctan      # Calculate lead angle
  aim + aim' sto          # Adjust aim
  15 fire' sto            # Fire!

  # Dodge after shooting
  speedy chs speedy' sto  # Reverse vertical direction
  Scan jump
</pre></td></tr></tbody></table></div>
</figure>

<p>This robot moves while scanning, leads its shots using doppler compensation,
and reverses direction after each shot to make itself harder to hit.</p>

<h3 id="tips-for-writing-robots">Tips for Writing Robots</h3>

<ol>
  <li><strong>Manage your energy</strong>: Don’t fire with more energy than you can spare.
Running out of energy means you can’t move or shoot.</li>
  <li><strong>Keep moving</strong>: A stationary robot is an easy target. Even simple
oscillating movement helps.</li>
  <li><strong>Lead your shots</strong>: Moving targets require aim compensation. The doppler
register tells you how much to adjust.</li>
  <li><strong>Use interrupts</strong>: Set up interrupt handlers for collisions and low
damage to react quickly to threats.</li>
  <li><strong>Know your class</strong>: A 9-point Mortal robot needs careful tradeoffs
between firepower, speed, and survivability.</li>
  <li><strong>Test against variety</strong>: A robot that beats one opponent might lose to
another. Test against different strategies.</li>
</ol>

<p>I compiled this documentation with help from Claude reviewing the RoboWar source code.</p>]]></content><author><name>Tad Thorley</name></author><summary type="html"><![CDATA[I first encountered RoboWar as a teenager—a game where you program circular "robots" to battle in a small arena. I've been looking for an activity for my local programming club and thought it would be fun to revisit. Since most of the original documentation has vanished from the web, I wrote this article to restore it.]]></summary></entry><entry><title type="html">Better JavaScript for the HTML You Already Have</title><link href="https://tad.thorley.dev/2026/01/03/better-javascript-for-the-html-you-already-have.html" rel="alternate" type="text/html" title="Better JavaScript for the HTML You Already Have" /><published>2026-01-03T00:00:00+00:00</published><updated>2026-01-03T00:00:00+00:00</updated><id>https://tad.thorley.dev/2026/01/03/better-javascript-for-the-html-you-already-have</id><content type="html" xml:base="https://tad.thorley.dev/2026/01/03/better-javascript-for-the-html-you-already-have.html"><![CDATA[<p>A few years ago I wrote about <a href="/2021/01/24/writing-stimulus-sold-me-on-vue.html">why Stimulus irritates me</a>
and how <a href="/2021/01/31/vue-for-the-html-you-already-have.html">Vue can do the same thing, but better</a>. It got me thinking about other tools that do a better job at being “JavaScript for the HTML you already have.” Two of them are <a href="https://alpinejs.dev/">Alpine.js</a> and <a href="https://htmx.org/">htmx</a>.</p>

<p><code class="language-plaintext highlighter-rouge">Alpine</code> and <code class="language-plaintext highlighter-rouge">htmx</code> take different approaches:</p>
<ul>
  <li><strong>Alpine</strong> is for client-side reactivity—state that lives in the browser</li>
  <li><strong>htmx</strong> is for server interactions—fetching HTML from your backend</li>
</ul>

<p>Both are better than <code class="language-plaintext highlighter-rouge">Stimulus</code> at what they do. And when you need both approaches? They work together to do a better job than <code class="language-plaintext highlighter-rouge">Stimulus</code>.</p>

<p>Let me show you with examples taken from the <a href="https://stimulus.hotwired.dev/">Stimulus documentation</a> itself.</p>

<p>This is the example from the <code class="language-plaintext highlighter-rouge">Stimulus</code> homepage. You type a name, click a button, and see a greeting.</p>

<figure class="code-example">
  <figcaption class="title">Stimulus HTML</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="source html"><pre>
<span class="nt">&lt;div</span> <span class="na">data-controller=</span><span class="s">"hello"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;input</span> <span class="na">data-hello-target=</span><span class="s">"name"</span> <span class="na">type=</span><span class="s">"text"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;button</span> <span class="na">data-action=</span><span class="s">"click-&gt;hello#greet"</span><span class="nt">&gt;</span>Greet<span class="nt">&lt;/button&gt;</span>
  <span class="nt">&lt;span</span> <span class="na">data-hello-target=</span><span class="s">"output"</span><span class="nt">&gt;&lt;/span&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">hello_controller.js</figcaption>
  <aside>notice that this is in a separate file that you need to include</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="source javascript"><pre>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/stimulus</span><span class="dl">"</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="nc">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
  <span class="kd">static</span> <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span> <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">output</span><span class="dl">"</span> <span class="p">]</span>

  <span class="nf">greet</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">outputTarget</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s2">`Hello, </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">nameTarget</span><span class="p">.</span><span class="nx">value</span><span class="p">}</span><span class="s2">!`</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>To understand what happens when you click the button, you have to:</p>
<ol>
  <li>Find the controller name in the HTML</li>
  <li>Find the corresponding JavaScript file</li>
  <li>Map the action syntax to a method</li>
  <li>Understand the all of the naming conventions</li>
</ol>

<p>Compare that to the <code class="language-plaintext highlighter-rouge">Alpine</code> version:</p>

<figure class="code-example">
  <figcaption class="title">Alpine HTML</figcaption>
  <aside>This version updates as you type, you don't need the button</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="source html"><pre>
<span class="nt">&lt;div</span> <span class="na">x-data=</span><span class="s">"{ name: '' }"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;input</span> <span class="na">x-model=</span><span class="s">"name"</span> <span class="na">type=</span><span class="s">"text"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;button&gt;</span>Greet<span class="nt">&lt;/button&gt;</span>
  <span class="nt">&lt;span</span> <span class="na">x-text=</span><span class="s">"name ? `Hello, ${name}!` : ''"</span><span class="nt">&gt;&lt;/span&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>No separate file. No imports. No target lookups. The behavior is right there in the HTML. I can easily put it directly here in this article:</p>

<script defer="" src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

<div x-data="{ name: '' }" class="demo">
  <input x-model="name" type="text" placeholder="Try typing a name here" />
  <span x-text="name ? `Hello, ${name}!` : ''"></span>
</div>
<hr />

<p>This is the second example from the <code class="language-plaintext highlighter-rouge">Stimulus</code> handbook. It loads HTML from the server and optionally refreshes it on an interval.</p>

<figure class="code-example">
  <figcaption class="title">Stimulus HTML</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="source html"><pre>
<span class="nt">&lt;div</span> <span class="na">data-controller=</span><span class="s">"content-loader"</span>
     <span class="na">data-content-loader-url-value=</span><span class="s">"/messages.html"</span>
     <span class="na">data-content-loader-refresh-interval-value=</span><span class="s">"5000"</span><span class="nt">&gt;&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">content_loader_controller.js</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
</pre></td><td class="source javascript"><pre>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hotwired/stimulus</span><span class="dl">"</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="nc">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
  <span class="kd">static</span> <span class="nx">values</span> <span class="o">=</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="na">refreshInterval</span><span class="p">:</span> <span class="nb">Number</span> <span class="p">}</span>

  <span class="nf">connect</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nf">load</span><span class="p">()</span>
    <span class="k">if </span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">hasRefreshIntervalValue</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="nf">startRefreshing</span><span class="p">()</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="nf">disconnect</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nf">stopRefreshing</span><span class="p">()</span>
  <span class="p">}</span>

  <span class="nf">load</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">fetch</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">urlValue</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nf">text</span><span class="p">())</span>
      <span class="p">.</span><span class="nf">then</span><span class="p">(</span><span class="nx">html</span> <span class="o">=&gt;</span> <span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">html</span><span class="p">)</span>
  <span class="p">}</span>

  <span class="nf">startRefreshing</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">refreshTimer</span> <span class="o">=</span> <span class="nf">setInterval</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="nf">load</span><span class="p">()</span>
    <span class="p">},</span> <span class="k">this</span><span class="p">.</span><span class="nx">refreshIntervalValue</span><span class="p">)</span>
  <span class="p">}</span>

  <span class="nf">stopRefreshing</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">if </span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">refreshTimer</span><span class="p">)</span> <span class="p">{</span>
      <span class="nf">clearInterval</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">refreshTimer</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>That’s 30+ lines of JavaScript to periodically fetch some HTML. You have to manage lifecycle hooks, timers, and cleanup.</p>

<p>Compare it to the <code class="language-plaintext highlighter-rouge">htmx</code> version:</p>

<figure class="code-example">
  <figcaption class="title">htmx HTML</figcaption>
  <aside>one line!</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
</pre></td><td class="source html"><pre>
<span class="nt">&lt;div</span> <span class="na">hx-get=</span><span class="s">"/messages.html"</span> <span class="na">hx-trigger=</span><span class="s">"load, every 5s"</span><span class="nt">&gt;&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p><code class="language-plaintext highlighter-rouge">htmx</code> handles the fetch, the interval, and the cleanup automatically.</p>

<p>Want navigation between different content sources?</p>

<figure class="code-example">
  <figcaption class="title">htmx navigation</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="source html"><pre>
<span class="nt">&lt;nav&gt;</span>
  <span class="nt">&lt;button</span> <span class="na">hx-get=</span><span class="s">"/demos/messages.html"</span> <span class="na">hx-target=</span><span class="s">"#content"</span><span class="nt">&gt;</span>Messages<span class="nt">&lt;/button&gt;</span>
  <span class="nt">&lt;button</span> <span class="na">hx-get=</span><span class="s">"/demos/tasks.html"</span> <span class="na">hx-target=</span><span class="s">"#content"</span><span class="nt">&gt;</span>Comments<span class="nt">&lt;/button&gt;</span>
<span class="nt">&lt;/nav&gt;</span>
<span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"content"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;p&gt;</span>Click a button to load content<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I can put the above code example directly in this article:</p>

<script src="https://unpkg.com/htmx.org@2.0.4"></script>

<div class="demo">
  <nav style="margin-bottom: 1rem;">
    <button class="button" hx-get="/demos/messages.html" hx-target="#content">Messages</button>
    <button class="button" hx-get="/demos/tasks.html" hx-target="#content">Tasks</button>
  </nav>
  <div id="content">
    <p>Click a button to load content</p>
  </div>
</div>

<hr />

<p>Sometimes you need both client-side state and server interaction. A modal dialog with a form is a classic example: showing/hiding the modal is client-side state, but the form submission goes to the server.</p>

<p>With <code class="language-plaintext highlighter-rouge">Stimulus</code> you’d need either one complex controller or two controllers that somehow communicate with each other (which <code class="language-plaintext highlighter-rouge">Stimulus</code> isn’t good at). You’d wire up the modal toggle, handle the form submission with fetch, parse the response, update the DOM, and close the modal on success.</p>

<p>I’m not going to code it up because it would be 80+ lines across multiple files and <code class="language-plaintext highlighter-rouge">Stimulus</code> sucks.</p>

<p>The <code class="language-plaintext highlighter-rouge">Alpine</code> with <code class="language-plaintext highlighter-rouge">htmx</code> example is around 15 lines of HTML:</p>

<figure class="code-example">
  <figcaption class="title">Alpine &amp; htmx HTML</figcaption>
  <aside>When the form submits, htmx POSTs to /subscribe. The server returns HTML, maybe something like <br />&lt;p&gt;You're subscribed!&lt;/p&gt;<br /> and htmx swaps it into the div#result. No JSON parsing, no DOM manipulation.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="source html"><pre>
<span class="nt">&lt;div</span> <span class="na">x-data=</span><span class="s">"{ open: false }"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;button</span> <span class="err">@</span><span class="na">click=</span><span class="s">"open = true"</span><span class="nt">&gt;</span>Subscribe<span class="nt">&lt;/button&gt;</span>

  <span class="nt">&lt;div</span> <span class="na">x-show=</span><span class="s">"open"</span> <span class="na">x-cloak</span> <span class="na">class=</span><span class="s">"modal-backdrop"</span> <span class="err">@</span><span class="na">click.self=</span><span class="s">"open = false"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"modal"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;h2&gt;</span>Subscribe to Updates<span class="nt">&lt;/h2&gt;</span>
      <span class="nt">&lt;form</span> <span class="na">hx-post=</span><span class="s">"/subscribe"</span> <span class="na">hx-target=</span><span class="s">"#result"</span> <span class="err">@</span><span class="na">htmx:after-request=</span><span class="s">"open = false"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"email"</span> <span class="na">name=</span><span class="s">"email"</span> <span class="na">placeholder=</span><span class="s">"you@example.com"</span> <span class="na">required</span><span class="nt">&gt;</span>
        <span class="nt">&lt;button</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">&gt;</span>Subscribe<span class="nt">&lt;/button&gt;</span>
      <span class="nt">&lt;/form&gt;</span>
      <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"result"</span><span class="nt">&gt;&lt;/div&gt;</span>
      <span class="nt">&lt;button</span> <span class="err">@</span><span class="na">click=</span><span class="s">"open = false"</span><span class="nt">&gt;</span>Cancel<span class="nt">&lt;/button&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p><code class="language-plaintext highlighter-rouge">Alpine</code> handles the modal state. <code class="language-plaintext highlighter-rouge">htmx</code> handles the form submission. They communicate through the <code class="highlight javascript"><span class="na">@htmx:after-request</span></code> event—when <code class="language-plaintext highlighter-rouge">htmx</code> finishes, <code class="language-plaintext highlighter-rouge">Alpine</code> closes the modal.</p>

<p>No JavaScript files. No lifecycle management. No manual DOM manipulation.</p>

<p>Check it out:</p>

<div x-data="{ open: false }" class="demo">
  <button class="button" @click="open = true">Subscribe</button>
  <div x-show="open" x-cloak class="modal-backdrop" @click.self="open = false" style="position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;">
    <div class="modal" style="background-color: white; margin: 8rem; padding: 2rem; border-radius: 8px; min-width: 300px;" @click.stop>
      <h3 style="margin-top: 0;">Subscribe to Updates</h3>
      <form hx-post="/subscribe" hx-target="#modal-result" hx-swap="innerHTML" @htmx:after-request="open = false">
        <input type="email" name="email" placeholder="you@example.com" required style="width: 100%; padding: 0.5rem; margin-bottom: 1rem;">
        <button class="button" type="submit">Subscribe</button>
        <button class="button secondary" type="button" @click="open = false">Cancel</button>
      </form>
      <div id="modal-result"></div>
    </div>
  </div>
</div>

<p><small>(The form won't actually submit because this is a static blog, but the modal interaction works.)</small></p>

<p><code class="language-plaintext highlighter-rouge">Stimulus</code> claims to be “a modest JavaScript framework for the HTML you already have.” But look at the examples:</p>

<ol>
  <li>The <code class="language-plaintext highlighter-rouge">Stimulus</code> HTML is littered with framework-specific attributes that point elsewhere</li>
  <li>The behavior lives in separate files with their own conventions</li>
  <li>Simple tasks require a lot of ceremony: controllers, targets, actions, values, and lifecycle hooks</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">Alpine</code> and <code class="language-plaintext highlighter-rouge">htmx</code> actually deliver on the promise:</p>

<ol>
  <li>The behavior is visible in the HTML itself</li>
  <li>You can understand what happens without opening another file</li>
  <li>Simple tasks are simple</li>
</ol>

<p>The best part? You don’t have to choose. <code class="language-plaintext highlighter-rouge">Alpine</code> handles client-side state. <code class="language-plaintext highlighter-rouge">htmx</code> handles server interactions. Use whichever one fits the problem or use them together.</p>]]></content><author><name>Tad Thorley</name></author><summary type="html"><![CDATA[A few years ago I wrote about why Stimulus irritates me and how Vue can do the same thing, but better. It got me thinking about other tools that do a better job at being “JavaScript for the HTML you already have.” Two of them are Alpine.js and htmx.]]></summary></entry><entry><title type="html">So I Still Want to Put Code in My Blog</title><link href="https://tad.thorley.dev/2026/01/01/so-i-still-want-to-put-code-in-my-blog.html" rel="alternate" type="text/html" title="So I Still Want to Put Code in My Blog" /><published>2026-01-01T00:00:00+00:00</published><updated>2026-01-01T00:00:00+00:00</updated><id>https://tad.thorley.dev/2026/01/01/so-i-still-want-to-put-code-in-my-blog</id><content type="html" xml:base="https://tad.thorley.dev/2026/01/01/so-i-still-want-to-put-code-in-my-blog.html"><![CDATA[<p>A few years ago I wrote about creating a <a href="/2019/08/29/syntax-highlighting-blog-code.html">custom Jekyll plugin for code blocks</a>.
It served me well, but recently I dusted off this blog and decided to revisit the code.
I found a few issues worth fixing.</p>

<p>The original plugin used the <a href="https://github.com/judofyr/tubby">tubby</a> gem to build HTML.
Tubby is a nice little library, but it hasn’t been updated since January 2021 and I didn’t
really need it. A heredoc works just as well and removes a dependency.</p>

<p>The original Tubby code:</p>
<figure class="code-example">
  <figcaption class="title">original HTML generation</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="source ruby"><pre>
<span class="no">Tubby</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">tag</span><span class="o">|</span>
  <span class="n">tag</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="ss">class: </span><span class="s1">'code-example'</span><span class="p">)</span> <span class="k">do</span>
    <span class="n">tag</span><span class="p">.</span><span class="nf">figcaption</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="ss">class: </span><span class="s1">'title'</span><span class="p">)</span>
    <span class="n">tag</span><span class="p">.</span><span class="nf">aside</span><span class="p">(</span><span class="vi">@params</span><span class="p">[</span><span class="ss">:comment</span><span class="p">])</span> <span class="k">if</span> <span class="vi">@params</span><span class="p">[</span><span class="ss">:comment</span><span class="p">]</span>
    <span class="n">tag</span><span class="p">.</span><span class="nf">div</span><span class="p">(</span><span class="ss">class: </span><span class="s1">'overflow'</span><span class="p">)</span> <span class="p">{</span> <span class="n">tag</span><span class="p">.</span><span class="nf">raw!</span> <span class="n">source</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span><span class="p">.</span><span class="nf">to_s</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Replaced with a simple heredoc:</p>
<figure class="code-example">
  <figcaption class="title">new HTML generation</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="source ruby"><pre>
<span class="o">&lt;&lt;~</span><span class="no">HTML</span><span class="sh">
  &lt;figure class="code-example"&gt;
    </span><span class="si">#{</span><span class="n">title_html</span><span class="si">}</span><span class="sh">
    </span><span class="si">#{</span><span class="n">aside_html</span><span class="si">}</span><span class="sh">
    &lt;div class="overflow"&gt;</span><span class="si">#{</span><span class="n">source</span><span class="si">}</span><span class="sh">&lt;/div&gt;
  &lt;/figure&gt;
</span><span class="no">HTML</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I also discovered a bug in my lexer lookup. The original code used <code class="highlight ruby"><span class="nf">const_get</span></code> with manual capitalization:</p>

<figure class="code-example">
  <figcaption class="title">original lexer lookup</figcaption>
  <aside>Note the language is used in code_class before being capitalized, but the capitalized version was used for the lexer lookup; this meant the CSS class and the lexer could be inconsistent.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="source ruby"><pre>
<span class="n">formatter</span> <span class="o">=</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Formatters</span><span class="o">::</span><span class="no">HTMLTable</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
  <span class="no">Rouge</span><span class="o">::</span><span class="no">Formatters</span><span class="o">::</span><span class="no">HTML</span><span class="p">.</span><span class="nf">new</span><span class="p">,</span>
  <span class="ss">table_class: </span><span class="s1">'code'</span><span class="p">,</span>
  <span class="ss">gutter_class: </span><span class="s1">'gutter'</span><span class="p">,</span>
  <span class="ss">code_class: </span><span class="s1">'source '</span> <span class="o">+</span> <span class="n">language</span>
<span class="p">)</span>
<span class="n">language</span> <span class="o">=</span> <span class="n">language</span><span class="p">.</span><span class="nf">capitalize</span> <span class="k">if</span> <span class="n">language</span> <span class="o">==</span> <span class="n">language</span><span class="p">.</span><span class="nf">downcase</span>
<span class="n">lexer</span> <span class="o">=</span>
  <span class="k">begin</span>
    <span class="no">Rouge</span><span class="o">::</span><span class="no">Lexers</span><span class="p">.</span><span class="nf">const_get</span><span class="p">(</span><span class="n">language</span><span class="p">).</span><span class="nf">new</span>
  <span class="k">rescue</span> <span class="no">NameError</span>
    <span class="no">Rouge</span><span class="o">::</span><span class="no">Lexers</span><span class="o">::</span><span class="no">Markdown</span><span class="p">.</span><span class="nf">new</span>
  <span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I was using <code class="highlight ruby"><span class="nf">const_get</span></code> with a fallback which was fine, but <code class="highlight ruby"><span class="no">Rouge</span><span class="o">::</span><span class="no">Lexer</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">language</span><span class="p">)</span></code>
is a much better approach.</p>

<figure class="code-example">
  <figcaption class="title">new lexer lookup</figcaption>
  <aside>Rouge::Lexer.find is case-sensitive, so downcase handles inputs like “ERB”</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
</pre></td><td class="source ruby"><pre>
<span class="n">lexer</span> <span class="o">=</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Lexer</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">language</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">downcase</span><span class="p">)</span> <span class="o">||</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Lexers</span><span class="o">::</span><span class="no">PlainText</span><span class="p">.</span><span class="nf">new</span>
</pre></td></tr></tbody></table></div>
</figure>

<p><code class="highlight ruby"><span class="no">Rouge</span><span class="o">::</span><span class="no">Lexer</span><span class="p">.</span><span class="nf">find</span></code> handles aliases and returns <code class="highlight ruby"><span class="kp">nil</span></code> if
the language isn’t found, which is cleaner than rescuing a <code class="highlight ruby"><span class="no">NameError</span></code>.</p>

<p>The original code always rendered a <code class="language-plaintext highlighter-rouge">&lt;figcaption&gt;</code> even when no title was provided.
Now, title and aside elements only render when they have content:</p>

<figure class="code-example">
  <figcaption class="title">conditional rendering</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="source ruby"><pre>
<span class="k">def</span> <span class="nf">title_html</span>
  <span class="k">return</span> <span class="s1">''</span> <span class="k">if</span> <span class="vi">@params</span><span class="p">[</span><span class="ss">:title</span><span class="p">].</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">empty?</span>
  <span class="sx">%(&lt;figcaption class="title"&gt;</span><span class="si">#{</span><span class="vi">@params</span><span class="p">[</span><span class="ss">:title</span><span class="p">]</span><span class="si">}</span><span class="sx">&lt;/figcaption&gt;)</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">aside_html</span>
  <span class="k">return</span> <span class="s1">''</span> <span class="k">if</span> <span class="vi">@params</span><span class="p">[</span><span class="ss">:comment</span><span class="p">].</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">empty?</span>
  <span class="sx">%(&lt;aside&gt;</span><span class="si">#{</span><span class="vi">@params</span><span class="p">[</span><span class="ss">:comment</span><span class="p">]</span><span class="si">}</span><span class="sx">&lt;/aside&gt;)</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Here’s the refactored plugin:</p>

<figure class="code-example">
  <figcaption class="title">code.rb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
</pre></td><td class="source ruby"><pre>
<span class="c1"># frozen_string_literal: true</span>

<span class="nb">require</span> <span class="s1">'rouge'</span>

<span class="k">class</span> <span class="nc">CodeBlock</span> <span class="o">&lt;</span> <span class="no">Liquid</span><span class="o">::</span><span class="no">Block</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">_tag_name</span><span class="p">,</span> <span class="n">params</span><span class="p">,</span> <span class="n">_tokens</span><span class="p">)</span>
    <span class="k">super</span>
    <span class="vi">@params</span> <span class="o">=</span> <span class="n">parse_params</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">render</span><span class="p">(</span><span class="n">_context</span><span class="p">)</span>
    <span class="n">code</span> <span class="o">=</span> <span class="k">super</span>
    <span class="n">language</span> <span class="o">=</span> <span class="vi">@params</span><span class="p">[</span><span class="ss">:language</span><span class="p">]</span>

    <span class="o">&lt;&lt;~</span><span class="no">HTML</span><span class="sh">
      &lt;figure class="code-example"&gt;
        </span><span class="si">#{</span><span class="n">title_html</span><span class="si">}</span><span class="sh">
        </span><span class="si">#{</span><span class="n">aside_html</span><span class="si">}</span><span class="sh">
        &lt;div class="overflow"&gt;</span><span class="si">#{</span><span class="n">highlight</span><span class="p">(</span><span class="n">code</span><span class="p">:,</span> <span class="n">language</span><span class="p">:)</span><span class="si">}</span><span class="sh">&lt;/div&gt;
      &lt;/figure&gt;
</span><span class="no">    HTML</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">highlight</span><span class="p">(</span><span class="n">code</span><span class="p">:,</span> <span class="n">language</span><span class="p">:)</span>
    <span class="n">lexer</span> <span class="o">=</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Lexer</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">language</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">downcase</span><span class="p">)</span> <span class="o">||</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Lexers</span><span class="o">::</span><span class="no">PlainText</span><span class="p">.</span><span class="nf">new</span>
    <span class="n">formatter</span> <span class="o">=</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Formatters</span><span class="o">::</span><span class="no">HTMLTable</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
      <span class="no">Rouge</span><span class="o">::</span><span class="no">Formatters</span><span class="o">::</span><span class="no">HTML</span><span class="p">.</span><span class="nf">new</span><span class="p">,</span>
      <span class="ss">table_class: </span><span class="s1">'code'</span><span class="p">,</span>
      <span class="ss">gutter_class: </span><span class="s1">'gutter'</span><span class="p">,</span>
      <span class="ss">code_class: </span><span class="s2">"source </span><span class="si">#{</span><span class="n">language</span><span class="si">}</span><span class="s2">"</span>
    <span class="p">)</span>
    <span class="n">formatter</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span><span class="n">lexer</span><span class="p">.</span><span class="nf">lex</span><span class="p">(</span><span class="n">code</span><span class="p">))</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">title_html</span>
    <span class="k">return</span> <span class="s1">''</span> <span class="k">if</span> <span class="vi">@params</span><span class="p">[</span><span class="ss">:title</span><span class="p">].</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">empty?</span>
    <span class="sx">%(&lt;figcaption class="title"&gt;</span><span class="si">#{</span><span class="vi">@params</span><span class="p">[</span><span class="ss">:title</span><span class="p">]</span><span class="si">}</span><span class="sx">&lt;/figcaption&gt;)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">aside_html</span>
    <span class="k">return</span> <span class="s1">''</span> <span class="k">if</span> <span class="vi">@params</span><span class="p">[</span><span class="ss">:comment</span><span class="p">].</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">empty?</span>
    <span class="sx">%(&lt;aside&gt;</span><span class="si">#{</span><span class="vi">@params</span><span class="p">[</span><span class="ss">:comment</span><span class="p">]</span><span class="si">}</span><span class="sx">&lt;/aside&gt;)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">parse_params</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
    <span class="n">params</span><span class="p">.</span><span class="nf">scan</span><span class="p">(</span><span class="no">Liquid</span><span class="o">::</span><span class="no">TagAttributes</span><span class="p">).</span><span class="nf">to_h</span> <span class="p">{</span> <span class="o">|</span><span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="o">|</span> <span class="p">[</span><span class="n">k</span><span class="p">.</span><span class="nf">to_sym</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="s1">'"'</span><span class="p">)]</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">Liquid</span><span class="o">::</span><span class="no">Template</span><span class="p">.</span><span class="nf">register_tag</span><span class="p">(</span><span class="s1">'code'</span><span class="p">,</span> <span class="no">CodeBlock</span><span class="p">)</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>The changes:</p>
<ol>
  <li>Removed the <code class="language-plaintext highlighter-rouge">tubby</code> dependency—one less gem to worry about</li>
  <li>Used <code class="highlight ruby"><span class="no">Rouge</span><span class="o">::</span><span class="no">Lexer</span><span class="p">.</span><span class="nf">find</span></code> for better language lookup</li>
  <li>Extracted <code class="highlight ruby"><span class="nf">highlight</span></code>, <code class="highlight ruby"><span class="nf">title_html</span></code>, and <code class="highlight ruby"><span class="nf">aside_html</span></code> methods for readability</li>
  <li>Empty elements no longer render</li>
  <li>Simplified <code class="highlight ruby"><span class="nf">parse_params</span></code> using <code class="highlight ruby"><span class="no">Liquid</span><span class="o">::</span><span class="no">TagAttributes</span></code> and <code class="highlight ruby"><span class="nf">.to_h</span></code></li>
  <li>Changed the default language from <code class="highlight ruby"><span class="s1">'markdown'</span></code> to <code class="highlight ruby"><span class="s1">'plaintext'</span></code></li>
</ol>

<p>While I was at it, I noticed that inline code references were inconsistent with the code
blocks in my blocks. When I mention <code class="highlight ruby"><span class="no">Rouge</span><span class="o">::</span><span class="no">Lexer</span><span class="p">.</span><span class="nf">find</span></code> in a sentence, it should have the
same syntax highlighting as it does in a code block—<code class="highlight ruby"><span class="no">Rouge</span></code> and <code class="highlight ruby"><span class="no">Lexer</span></code> as
constants, <code class="highlight ruby"><span class="nf">find</span></code> as a method name.</p>

<p>So I created a companion plugin for inline code:</p>

<figure class="code-example">
  <figcaption class="title">inline_code.rb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
</pre></td><td class="source ruby"><pre>
<span class="c1"># frozen_string_literal: true</span>

<span class="nb">require</span> <span class="s1">'rouge'</span>

<span class="k">class</span> <span class="nc">InlineCode</span> <span class="o">&lt;</span> <span class="no">Liquid</span><span class="o">::</span><span class="no">Block</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">_tag_name</span><span class="p">,</span> <span class="n">params</span><span class="p">,</span> <span class="n">_tokens</span><span class="p">)</span>
    <span class="k">super</span>
    <span class="vi">@params</span> <span class="o">=</span> <span class="n">parse_params</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">render</span><span class="p">(</span><span class="n">_context</span><span class="p">)</span>
    <span class="n">code</span> <span class="o">=</span> <span class="k">super</span><span class="p">.</span><span class="nf">strip</span>
    <span class="n">language</span> <span class="o">=</span> <span class="vi">@params</span><span class="p">[</span><span class="ss">:language</span><span class="p">]</span>
    <span class="n">token</span> <span class="o">=</span> <span class="vi">@params</span><span class="p">[</span><span class="ss">:token</span><span class="p">]</span>
    <span class="k">if</span> <span class="n">token</span>
      <span class="n">manual</span><span class="p">(</span><span class="n">code</span><span class="p">:,</span> <span class="n">language</span><span class="p">:,</span> <span class="n">token</span><span class="p">:)</span>
    <span class="k">else</span>
      <span class="n">auto</span><span class="p">(</span><span class="n">code</span><span class="p">:,</span> <span class="n">language</span><span class="p">:)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">parse_params</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
    <span class="n">params</span><span class="p">.</span><span class="nf">scan</span><span class="p">(</span><span class="no">Liquid</span><span class="o">::</span><span class="no">TagAttributes</span><span class="p">).</span><span class="nf">to_h</span> <span class="p">{</span> <span class="o">|</span><span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="o">|</span> <span class="p">[</span><span class="n">k</span><span class="p">.</span><span class="nf">to_sym</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="s1">'"'</span><span class="p">)]</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">manual</span><span class="p">(</span><span class="n">code</span><span class="p">:,</span> <span class="n">language</span><span class="p">:,</span> <span class="n">token</span><span class="p">:)</span>
    <span class="sx">%(&lt;code class="highlight </span><span class="si">#{</span><span class="n">language</span><span class="si">}</span><span class="sx">"&gt;&lt;span class="</span><span class="si">#{</span><span class="n">token</span><span class="si">}</span><span class="sx">"&gt;</span><span class="si">#{</span><span class="n">code</span><span class="si">}</span><span class="sx">&lt;/span&gt;&lt;/code&gt;)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">auto</span><span class="p">(</span><span class="n">code</span><span class="p">:,</span> <span class="n">language</span><span class="p">:)</span>
    <span class="n">lexer</span> <span class="o">=</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Lexer</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">language</span><span class="p">)</span> <span class="o">||</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Lexers</span><span class="o">::</span><span class="no">PlainText</span><span class="p">.</span><span class="nf">new</span>
    <span class="n">formatter</span> <span class="o">=</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Formatters</span><span class="o">::</span><span class="no">HTML</span><span class="p">.</span><span class="nf">new</span>
    <span class="n">highlighted</span> <span class="o">=</span> <span class="n">formatter</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span><span class="n">lexer</span><span class="p">.</span><span class="nf">lex</span><span class="p">(</span><span class="n">code</span><span class="p">))</span>
    <span class="sx">%(&lt;code class="highlight </span><span class="si">#{</span><span class="n">language</span><span class="si">}</span><span class="sx">"&gt;</span><span class="si">#{</span><span class="n">highlighted</span><span class="si">}</span><span class="sx">&lt;/code&gt;)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">Liquid</span><span class="o">::</span><span class="no">Template</span><span class="p">.</span><span class="nf">register_tag</span><span class="p">(</span><span class="s1">'ic'</span><span class="p">,</span> <span class="no">InlineCode</span><span class="p">)</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>The usage is similar to the code block plugin:</p>

<figure class="code-example">
  <figcaption class="title">usage</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
</pre></td><td class="source liquid"><pre>
{％ ic language: "ruby" ％}Rouge::Lexer.find{％ endic ％}
</pre></td></tr></tbody></table></div>
</figure>

<p>Sometimes Rouge doesn’t have enough context to identify a token correctly. For example,
<code class="highlight ruby"><span class="n">find</span></code> on its own is identified as a local variable, not a method.
The optional <code class="highlight ruby"><span class="n">token</span></code> parameter lets you override the styling:</p>

<figure class="code-example">
  <figcaption class="title">token override</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
</pre></td><td class="source liquid"><pre>
{％ ic language: "ruby", token: "nf" ％}find{％ endic ％}
</pre></td></tr></tbody></table></div>
</figure>

<p>The syntax highlighting classes are shared between the block and inline plugins via a
Sass mixin, so they stay in sync. Now when I reference a method like
<code class="highlight ruby"><span class="nf">parse_params</span></code> in the copy, it looks the same as it does in a code example.</p>]]></content><author><name>Tad Thorley</name></author><summary type="html"><![CDATA[A few years ago I wrote about creating a custom Jekyll plugin for code blocks. It served me well, but recently I dusted off this blog and decided to revisit the code. I found a few issues worth fixing.]]></summary></entry><entry><title type="html">Modest Vue For the HTML You Already Have</title><link href="https://tad.thorley.dev/2021/01/31/vue-for-the-html-you-already-have.html" rel="alternate" type="text/html" title="Modest Vue For the HTML You Already Have" /><published>2021-01-31T00:00:00+00:00</published><updated>2021-01-31T00:00:00+00:00</updated><id>https://tad.thorley.dev/2021/01/31/vue-for-the-html-you-already-have</id><content type="html" xml:base="https://tad.thorley.dev/2021/01/31/vue-for-the-html-you-already-have.html"><![CDATA[<p>Stimulus has a description top and center of its home page: “A modest JavaScript
framework for the HTML you already have.” By “modest” I think it is claiming that: 
it won’t take over your routing, it won’t bury its view code deep in JavaScript
code, it can be applied to the pages you choose (and won’t affect the pages that
you don’t), etc. By “HTML you already have” I think it is claiming that you can
take an HTML-first approach; you can write your HTML and then “sprinkle” in some
JavaScript functionality later.</p>

<p>I like Simulus’s proposition, but <a href="/2021/01/24/writing-stimulus-sold-me-on-vue.html">I don’t like the implementation</a>. So let’s do it in a Ruby on Rails
application with Vue instead (<a href="https://github.com/phaedryx/modest-vue-for-your-html">source code</a>).</p>

<p>Even though <a href="https://v3.vuejs.org/">Vue 3</a> was released in September 2020, Rails
still doesn’t support it as an option for creating new applications, so we’ll
have to create the app first and then configure it for Vue.</p>

<div class="terminal">
$ rails new modest-vue-for-your-html<br />
$ cd modest-vue-for-your-html
</div>

<p>Next add vue, the vue loader (for webpack).</p>

<div class="terminal">
<aside>
Vue 3 is using the @next tag so that the Vue 2 folks on "current" don't get upgraded.
</aside>
$ yarn add vue@next<br />
$ yarn add vue-loader@next<br />
</div>

<p>Now that Vue is installed, it needs some configuration. Edit the webpack configuration
file:</p>

<figure class="code-example">
  <figcaption class="title">config/webpack/environment.js</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
</pre></td><td class="source javascript"><pre>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">environment</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@rails/webpacker</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">DefinePlugin</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">webpack</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">VueLoaderPlugin</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">vue-loader</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">customConfig</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">resolve</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">alias</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">vue</span><span class="p">:</span> <span class="dl">'</span><span class="s1">vue/dist/vue.esm-bundler.js</span><span class="dl">'</span> <span class="c1">// Use the variation that allows Vue to use existing HTML (e.g. Rails views)</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">environment</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nf">merge</span><span class="p">(</span><span class="nx">customConfig</span><span class="p">)</span>

<span class="nx">environment</span><span class="p">.</span><span class="nx">plugins</span><span class="p">.</span><span class="nf">prepend</span><span class="p">(</span>
  <span class="dl">'</span><span class="s1">Define</span><span class="dl">'</span><span class="p">,</span>
  <span class="k">new</span> <span class="nc">DefinePlugin</span><span class="p">({</span>
    <span class="na">__VUE_OPTIONS_API__</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="c1">// To use the Composition API exclusively (I like it, but this is optional)</span>
    <span class="na">__VUE_PROD_DEVTOOLS__</span><span class="p">:</span> <span class="kc">false</span> <span class="c1">// Ensure development code tools stay out of production code</span>
  <span class="p">})</span>
<span class="p">)</span>

<span class="nx">environment</span><span class="p">.</span><span class="nx">plugins</span><span class="p">.</span><span class="nf">prepend</span><span class="p">(</span>
  <span class="dl">'</span><span class="s1">VueLoaderPlugin</span><span class="dl">'</span><span class="p">,</span>
  <span class="k">new</span> <span class="nc">VueLoaderPlugin</span><span class="p">()</span>
<span class="p">)</span>

<span class="nx">environment</span><span class="p">.</span><span class="nx">loaders</span><span class="p">.</span><span class="nf">prepend</span><span class="p">(</span><span class="dl">'</span><span class="s1">vue</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">test</span><span class="p">:</span> <span class="sr">/</span><span class="se">\.</span><span class="sr">vue$/</span><span class="p">,</span>
  <span class="na">use</span><span class="p">:</span> <span class="p">[{</span> <span class="na">loader</span><span class="p">:</span> <span class="dl">'</span><span class="s1">vue-loader</span><span class="dl">'</span> <span class="p">}]</span>
<span class="p">})</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">environment</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Now that the Vue is configured let’s give it some HTML to work with</p>

<div class="terminal">
$ rails generate scaffold Widget style:string color:string runcible:boolean<br />
$ rails db:migrate
</div>

<p>and edit the routes file to use it.</p>

<figure class="code-example">
  <figcaption class="title">config/routes.rb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="source ruby"><pre>
<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
  <span class="n">root</span> <span class="s1">'widgets#index'</span>

  <span class="n">resources</span> <span class="ss">:widgets</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Suppose I want to ensure that people fill out the style name correctly. I could 
add a Rails validation that the name matches a certain regular expression (and I
will later) but that means filling out the form, submitting it, and <em>then</em> discovering
a problem. It would be nicer if I got a warning as I was filling the form out. A
bit of JavaScript could be nice.</p>

<p>There are a lot of ways to set this up, but since it is a comparison to Stimulus
let’s set it up in a Stimulus style. In Stimulus you give an element a specific
<code class="language-plaintext highlighter-rouge">data-controller</code> attribute and it hooks code to the element. Let’s add a 
<code class="language-plaintext highlighter-rouge">data-component</code> (because Vue uses components) attribute to our HTML and then
hook some Vue code to it.</p>

<p>Our Widget views share a common form, so let’s add it to the form partial:</p>

<figure class="code-example">
  <figcaption class="title">app/views/widgets/_form.html.erb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="source erb"><pre>
<span class="nt">&lt;div</span> <span class="na">data-component=</span><span class="s">"Widget"</span><span class="nt">&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">form_with</span><span class="p">(</span><span class="ss">model: </span><span class="n">widget</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%&gt;</span>
    ...
  <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Create the component:</p>

<figure class="code-example">
  <figcaption class="title">app/javascript/components/Widget.js</figcaption>
  <aside>I really love how simple components in Vue can be. I don't have to worry about `this` (with the Composition style), I don't have to worry about `bind`ing anything (looking at you React), and I don't have to worry about any class inheritance. I just have a plain, ordinary JavaScript object.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="source javascript"><pre>
<span class="kd">const</span> <span class="nx">Widget</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Widget</span><span class="dl">'</span><span class="p">,</span>
  <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">Widget</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>And add code to hook them together:</p>

<figure class="code-example">
  <figcaption class="title">app/javascript/packs/application.js</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="source javascript"><pre>
<span class="c1">// standard Rails stuff</span>
<span class="k">import</span> <span class="nx">Rails</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@rails/ujs</span><span class="dl">"</span>
<span class="k">import</span> <span class="nx">Turbolinks</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">turbolinks</span><span class="dl">"</span>
<span class="k">import</span> <span class="o">*</span> <span class="nx">as</span> <span class="nx">ActiveStorage</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@rails/activestorage</span><span class="dl">"</span>
<span class="k">import</span> <span class="dl">"</span><span class="s2">channels</span><span class="dl">"</span>

<span class="nx">Rails</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span>
<span class="nx">Turbolinks</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span>
<span class="nx">ActiveStorage</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span>

<span class="c1">// Vue stuff here</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">createApp</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vue</span><span class="dl">'</span>

<span class="k">import</span> <span class="nx">Widget</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../components/Widget</span><span class="dl">'</span>
<span class="c1">// I could import more components here</span>

<span class="kd">const</span> <span class="nx">components</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">Widget</span> <span class="p">}</span> <span class="c1">// An object to hold components</span>

<span class="c1">// Normally this would be done with 'DOMContentLoaded', but Turbolinks breaks that</span>
<span class="nb">document</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">turbolinks:load</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// Find everything with a data-component</span>
  <span class="nb">document</span><span class="p">.</span><span class="nf">querySelectorAll</span><span class="p">(</span><span class="dl">'</span><span class="s1">[data-component]</span><span class="dl">'</span><span class="p">).</span><span class="nf">forEach</span><span class="p">((</span><span class="nx">node</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// mount the respective Vue component</span>
    <span class="nf">createApp</span><span class="p">(</span><span class="nx">components</span><span class="p">[</span><span class="nx">node</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">component</span><span class="p">]).</span><span class="nf">mount</span><span class="p">(</span><span class="nx">node</span><span class="p">)</span>
  <span class="p">})</span>
<span class="p">})</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>The plan is to run a check on a form field when it loses focus (<code class="language-plaintext highlighter-rouge">onBlur</code>), so let’s
start with that. In our component let’s write a function that takes a parameter and
logs it out with <code class="language-plaintext highlighter-rouge">console.log</code>.</p>

<figure class="code-example">
  <figcaption class="title">app/javascript/components/Widget.js</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="source javascript"><pre>
<span class="kd">const</span> <span class="nx">Widget</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Widget</span><span class="dl">'</span><span class="p">,</span>
  <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">log</span> <span class="o">=</span> <span class="p">(</span><span class="nx">message</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span> <span class="p">}</span>

    <span class="k">return</span> <span class="p">{</span> <span class="nx">log</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">Widget</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Update your HTML to use the new function:</p>
<figure class="code-example">
  <figcaption class="title">app/views/widgets/_form.html.erb</figcaption>
  <aside>@blur is Vue shorthand for a blur event. The 'log' function is the one we just defined.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="source erb"><pre>
<span class="nt">&lt;div</span> <span class="na">data-component=</span><span class="s">"Widget"</span><span class="nt">&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">form_with</span><span class="p">(</span><span class="ss">model: </span><span class="n">widget</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%&gt;</span>
    ...
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"field"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:style</span> <span class="cp">%&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:style</span><span class="p">,</span> <span class="s2">"@blur"</span> <span class="o">=&gt;</span> <span class="s2">"log('it got blurry')"</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
    ...
  <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>If you start the server and start up the page, you should be able to click in the first text field,
click out of the text field, and see the message in the browser’s console.</p>

<p>This is interesting, but not terribly useful. Let’s assume that widget styles start with “WX-“ and
we want to check that in the component:</p>
<figure class="code-example">
  <figcaption class="title">app/javascript/components/Widget.js</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="source javascript"><pre>
<span class="kd">const</span> <span class="nx">Widget</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Widget</span><span class="dl">'</span><span class="p">,</span>
  <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">check</span> <span class="o">=</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if </span><span class="p">(</span><span class="sr">/^WX-.+/</span><span class="p">.</span><span class="nf">test</span><span class="p">(</span><span class="nx">name</span><span class="p">))</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">valid</span><span class="dl">'</span><span class="p">)</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">invalid</span><span class="dl">'</span><span class="p">)</span>
      <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="p">{</span> <span class="nx">check</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">Widget</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>And hook the new code in:</p>
<figure class="code-example">
  <figcaption class="title">app/views/widgets/_form.html.erb</figcaption>
  <aside>The $event is a special variable that refers to the blur event in this case.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="source erb"><pre>
<span class="nt">&lt;div</span> <span class="na">data-component=</span><span class="s">"Widget"</span><span class="nt">&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">form_with</span><span class="p">(</span><span class="ss">model: </span><span class="n">widget</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%&gt;</span>
    ...
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"field"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:style</span> <span class="cp">%&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:style</span><span class="p">,</span> <span class="s2">"@blur"</span> <span class="o">=&gt;</span> <span class="s2">"check($event.target.value)"</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
    ...
  <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>When you try out the modified page you should see “valid” and “invalid” when you type different
strings into the text field and remove focus.</p>

<p>This is nice, but users aren’t going to look in their web console for messages. Let’s output a
message into the HTML. First we’ll create a Vue <code class="language-plaintext highlighter-rouge">ref</code> in our Widget:</p>

<figure class="code-example">
  <figcaption class="title">app/javascript/components/Widget.js</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="source javascript"><pre>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ref</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vue</span><span class="dl">'</span>

<span class="kd">const</span> <span class="nx">Widget</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Widget</span><span class="dl">'</span><span class="p">,</span>
  <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">errorMessage</span> <span class="o">=</span> <span class="nf">ref</span><span class="p">(</span><span class="kc">null</span><span class="p">)</span>

    <span class="c1">// ...</span>

    <span class="k">return</span> <span class="p">{</span> <span class="nx">errorMessage</span><span class="p">,</span> <span class="nx">check</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">Widget</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>And add a corresponding div to the HTML:</p>
<figure class="code-example">
  <figcaption class="title">app/views/widgets/_form.html.erb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="source erb"><pre>
<span class="nt">&lt;div</span> <span class="na">data-component=</span><span class="s">"Widget"</span><span class="nt">&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">form_with</span><span class="p">(</span><span class="ss">model: </span><span class="n">widget</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%&gt;</span>
    ...
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"field"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:style</span> <span class="cp">%&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:style</span><span class="p">,</span> <span class="s2">"@blur"</span> <span class="o">=&gt;</span> <span class="s2">"check($event.target.value)"</span> <span class="cp">%&gt;</span>
      <span class="nt">&lt;div</span> <span class="na">ref=</span><span class="s">"errorMessage"</span> <span class="na">style=</span><span class="s">"color: #C00"</span><span class="nt">&gt;&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
    ...
  <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Which leads to the final version of the component:</p>
<figure class="code-example">
  <figcaption class="title">app/javascript/components/Widget.js</figcaption>
  <aside>The .value call accesses the underlying DOM node in this case.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="source javascript"><pre>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ref</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vue</span><span class="dl">'</span>

<span class="kd">const</span> <span class="nx">Widget</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Widget</span><span class="dl">'</span><span class="p">,</span>
  <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">errorMessage</span> <span class="o">=</span> <span class="nf">ref</span><span class="p">(</span><span class="kc">null</span><span class="p">)</span>

    <span class="kd">const</span> <span class="nx">check</span> <span class="o">=</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if </span><span class="p">(</span><span class="sr">/^WX-.+/</span><span class="p">.</span><span class="nf">test</span><span class="p">(</span><span class="nx">name</span><span class="p">))</span> <span class="p">{</span>
        <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="dl">''</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">This style name is incorrect</span><span class="dl">'</span>
      <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="p">{</span> <span class="nx">errorMessage</span><span class="p">,</span> <span class="nx">check</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">Widget</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>And here is the complete, final version of the form partial:</p>
<figure class="code-example">
  <figcaption class="title">app/views/widgets/_form.html.erb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
</pre></td><td class="source erb"><pre>
<span class="nt">&lt;div</span> <span class="na">data-component=</span><span class="s">"Widget"</span><span class="nt">&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">form_with</span><span class="p">(</span><span class="ss">model: </span><span class="n">widget</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%</span> <span class="k">if</span> <span class="n">widget</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">any?</span> <span class="cp">%&gt;</span>
      <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"error_explanation"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;h2&gt;</span><span class="cp">&lt;%=</span> <span class="n">pluralize</span><span class="p">(</span><span class="n">widget</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">count</span><span class="p">,</span> <span class="s2">"error"</span><span class="p">)</span> <span class="cp">%&gt;</span> prohibited this widget from being saved:<span class="nt">&lt;/h2&gt;</span>

        <span class="nt">&lt;ul&gt;</span>
          <span class="cp">&lt;%</span> <span class="n">widget</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">error</span><span class="o">|</span> <span class="cp">%&gt;</span>
            <span class="nt">&lt;li&gt;</span><span class="cp">&lt;%=</span> <span class="n">error</span><span class="p">.</span><span class="nf">full_message</span> <span class="cp">%&gt;</span><span class="nt">&lt;/li&gt;</span>
          <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
        <span class="nt">&lt;/ul&gt;</span>
      <span class="nt">&lt;/div&gt;</span>
    <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>

    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"field"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:style</span> <span class="cp">%&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:style</span><span class="p">,</span> <span class="s2">"@blur"</span> <span class="o">=&gt;</span> <span class="s2">"check($event.target.value)"</span> <span class="cp">%&gt;</span>
      <span class="nt">&lt;div</span> <span class="na">ref=</span><span class="s">"errorMessage"</span> <span class="na">style=</span><span class="s">"color: #C00"</span><span class="nt">&gt;&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>

    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"field"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:color</span> <span class="cp">%&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:color</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>

    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"field"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:runcible</span> <span class="cp">%&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">check_box</span> <span class="ss">:runcible</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>

    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"actions"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">submit</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
  <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Final observations:</p>

<ul>
  <li>I think this code is quite readable. You can look at the HTML and get a good
  sense of what the JavaScript will do. You can look at the JavaScript and get
  a good sense of what will happen in the HTML.</li>
  <li>I don’t have to reason about JavaScript to know what the resulting HTML is
  going to be.</li>
  <li>This is easy to backtrace. If you “view source” on this in a browser it is
  finding the corresponding partial in your Rails views and component in your
  JavaScript should be pretty easy.</li>
  <li>It is easy to see what is being passed to the JavaScript functions.</li>
  <li>I chose a naming scheme and setup that made sense to me, but there are a lot
  of different approaches that would work.</li>
  <li>Vue is a “progressive” framework. That means that you can add more techniques
  as needed. If you want to evolve this into a single page application you can.
  If you want to add child components to this component you can. If you want to
  start using <a href="https://v3.vuejs.org/guide/single-file-component.html#introduction">single file components</a> you can (I like this tutorial: <a href="https://dev.to/vannsl/vue3-on-rails-l9d">https://dev.to/vannsl/vue3-on-rails-l9d</a>).</li>
  <li>Because the components are just objects with functions, it would be easy to
  extract this into something reusable. Imagine turning the <code class="language-plaintext highlighter-rouge">check</code> function into
  a host of functions (e.g. <code class="language-plaintext highlighter-rouge">matches</code>, <code class="language-plaintext highlighter-rouge">presence</code>, <code class="language-plaintext highlighter-rouge">numericality</code>) similar to Rails
  validators that you can import from a <code class="language-plaintext highlighter-rouge">useValidation</code> composable.</li>
  <li>I didn’t touch on testing (maybe my next blog post), but Vue has <a href="https://v3.vuejs.org/guide/testing.html#frameworks">a lot of documentation</a>
  on testing, and utilities for the major testing frameworks. This example could be
  tested with <a href="https://jestjs.io/">Jest</a> and mounting the component with Vue testing
  utilities. As I stated before, you could extract the code into a <code class="language-plaintext highlighter-rouge">useValidation</code>
  composable and test the functions, which is even more straightforward.</li>
</ul>]]></content><author><name>Tad Thorley</name></author><summary type="html"><![CDATA[Stimulus has a description top and center of its home page: “A modest JavaScript framework for the HTML you already have.” By “modest” I think it is claiming that: it won’t take over your routing, it won’t bury its view code deep in JavaScript code, it can be applied to the pages you choose (and won’t affect the pages that you don’t), etc. By “HTML you already have” I think it is claiming that you can take an HTML-first approach; you can write your HTML and then “sprinkle” in some JavaScript functionality later.]]></summary></entry><entry><title type="html">Writing Stimulus Sold Me On Vue</title><link href="https://tad.thorley.dev/2021/01/24/writing-stimulus-sold-me-on-vue.html" rel="alternate" type="text/html" title="Writing Stimulus Sold Me On Vue" /><published>2021-01-24T00:00:00+00:00</published><updated>2021-01-24T00:00:00+00:00</updated><id>https://tad.thorley.dev/2021/01/24/writing-stimulus-sold-me-on-vue</id><content type="html" xml:base="https://tad.thorley.dev/2021/01/24/writing-stimulus-sold-me-on-vue.html"><![CDATA[<p>For the past 3 years I had been working on a Rails app with an Angularjs 1.5
front-end (SPA); I didn’t love it. It was much more complex than it needed to
be. Our front-end permission management was CanCan implementd for the front-end 
that would interpret our backend’s CanCan permission object passed to it as JSON.
The routing rules used a lot of regular expressions. The pages had deeply nested
Angular controllers. The Angular templates were dozens and dozens of Rails view
partials buried deep inside various directories. I would often fantasize about
deleting the whole thing and replacing it with straightforward HTML. Most of the
functionality would have been the same.</p>

<p>I switched jobs a few months ago. The team I joined is replacing an application
that is a mixture of jQuery, Angular, React, Bootstrap, and Foundation. It uses
the Rails asset pipeline together with Webpack. It is a mess. The JavaScript
feels like a cancer that has spread everywhere.</p>

<p>For the new application we decided to use Stimulus. It is endorsed by the Rails
team and its promise to be “a modest JavaScript framework for the HTML you already
have” felt like a breath of fresh air.</p>

<p>However, after using Stimulus for a couple of months I realized that it wasn’t
for me for a variety of reasons, including:</p>

<ol>
  <li>There is nothing in the Stimulus documentation about how to test it. <a href="https://github.com/phaedryx/stimulus-testing-exploration">I tried
  to work out some strategies on my own</a>,
  but nothing was particularly satisfying.</li>
  <li>There is not a lot of documentation in general. For example, it took me a fair
  amount of searching to discover that they use “<code class="language-plaintext highlighter-rouge">--</code>” in their naming for subdirectories.
  I don’t think it is in the documentation still.</li>
  <li>Putting all of your state in the DOM is tricky. If you have anything that modifies
  the DOM (for example, we had something that toggled between an “edit” and “view” mode)
  you could clobber your state; taking state out of the DOM is one of early arguments
  for Backbone.</li>
  <li>There is no easy way to coordinate or message between controllers, especially
  nested ones. One of React’s key features is that components can easily pass information
  to their child components.</li>
  <li>Stimulus disconnects functions from their parameters. You call a function and then it 
  has to search for the applicable <code class="language-plaintext highlighter-rouge">data-</code> attribute in the DOM.</li>
  <li>Their naming scheme is cumbersome, e.g. <code class="language-plaintext highlighter-rouge">data-controller="using-a--sub-directory" 
  data-target="some--nested--target-has-a.function"</code>. Everything is location-in-your-code-file-structure based.</li>
  <li>It was hard for me to get reusable pieces of code. Because nesting controllers seemed
  to make things complex, I stopped trying to make them work together and I just started
  making a controller per page.</li>
  <li>It doesn’t try to work with the larger JavaScript ecosystem</li>
</ol>

<p>A lot of my frustrations with Stimulus stem, I think, from my differences from how David
Heinemeier Hansson works. I’ve listened to several of his interviews, including recent
ones about his <a href="https://hotwire.dev/">Hotwire</a> approach. For example:</p>

<ol>
  <li>He doesn’t like JavaScript and tries to avoid it when he can.</li>
  <li>He has a front-end team who can write custom code for him.</li>
  <li>He is focused on Basecamp and Hey</li>
</ol>

<p>In contrast:</p>

<ol>
  <li>I don’t love JavaScript, but I can appreciate its usefulness and I try to respect it for
  what it is.</li>
  <li>I am a full-stack developer, which means I have to live with the JavaScript I write
  and I often need to use JavaScript libraries.</li>
  <li>I work on products that don’t necessarily have the same needs as Basecamp or Hey</li>
</ol>

<p>About the time I was hitting peak frustration with Stimulus, Vue released version 3. I’ve
found that it addresses my frustrations well:</p>

<ol>
  <li>There are <a href="https://vue-test-utils.vuejs.org/v2/guide/introduction.html">official testing libraries</a>
  for Vue and with a lot of documentation.</li>
  <li>Vue has the most and the most thorough documentation of any JavaScript framework I’ve seen.</li>
  <li>Vue has several strategies for state management and you don’t have to put state in the DOM.</li>
  <li>Vue has support for passing data between components as well as support for emitting and
  listening to events.</li>
  <li>Vue makes it clear what parameters are being passed to functions.</li>
  <li>Vue has a lot of naming guidelings, but what you name things is much more flexible.</li>
  <li>Vue encourages code reuse through “composables” (very much like React’s hooks) and
  components.</li>
  <li>Vue tries work with the JavaScript ecosystem.</li>
</ol>

<p>The part of Vue that I like the best is that it is a “progressive” framework. That is, I can
use it for “sprinkles” at one end or a full single page application at the other. I can write
HTML first and then use it for “the HTML I already have.”</p>]]></content><author><name>Tad Thorley</name></author><summary type="html"><![CDATA[For the past 3 years I had been working on a Rails app with an Angularjs 1.5 front-end (SPA); I didn’t love it. It was much more complex than it needed to be. Our front-end permission management was CanCan implementd for the front-end that would interpret our backend’s CanCan permission object passed to it as JSON. The routing rules used a lot of regular expressions. The pages had deeply nested Angular controllers. The Angular templates were dozens and dozens of Rails view partials buried deep inside various directories. I would often fantasize about deleting the whole thing and replacing it with straightforward HTML. Most of the functionality would have been the same.]]></summary></entry><entry><title type="html">Using and Customizing Ant Design with Rails 6</title><link href="https://tad.thorley.dev/2020/08/08/custom-ant-design-with-rails.html" rel="alternate" type="text/html" title="Using and Customizing Ant Design with Rails 6" /><published>2020-08-08T00:00:00+00:00</published><updated>2020-08-08T00:00:00+00:00</updated><id>https://tad.thorley.dev/2020/08/08/custom-ant-design-with-rails</id><content type="html" xml:base="https://tad.thorley.dev/2020/08/08/custom-ant-design-with-rails.html"><![CDATA[<p><a href="https://ant.design/">Ant Design</a> is an incredibly thorough design system with a lot of
useful <a href="https://reactjs.org/">React</a> components. The trick is that all of the CSS is
generated with <a href="http://lesscss.org/">Less</a> and there are <a href="https://ant.design/docs/react/faq#Will-you-provide-Sass/Stylus(etc.)-style-files-in-addition-to-the-Less-style-files-currently-included">no plans to support Sass or anything else</a>. Less support is
really poor in the Rails community right now; the gems are largely abandoned. If you want
to customize Ant Design with a Rails project you need to do it via <a href="https://webpack.js.org/">Webpack</a>.</p>

<p>Here’s how I was able to get it working after a lot of trial and error and painful web
searching (seriously, “Less” is a terrible name for a project).</p>

<p>First I started a new rails project.</p>
<div class="terminal">
<aside>The postgresql option is so that I can run it on Heroku.</aside>
$ rails new nessman --database=postgresql<br />
$ cd nessman<br />
$ rails db:migrate<br />
</div>

<p>Next I added the <code class="language-plaintext highlighter-rouge">react-rails</code> gem. This could have been done with a <code class="language-plaintext highlighter-rouge">--webpack=react</code>
on creation, but I like to do things manually when I’m learning and it allowed me to switch
to the latest webpacker at the same time.</p>
<figure class="code-example">
  <figcaption class="title">Gemfile</figcaption>
  <aside>The react-rails gem is the official React gem. The react_on_rails gem is a popular alternative.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="source ruby"><pre>
<span class="o">...</span>
<span class="n">gem</span> <span class="s1">'webpacker'</span><span class="p">,</span> <span class="s1">'~&gt; 5.1'</span><span class="p">,</span> <span class="s1">'&gt;= 5.1.1'</span> <span class="c1"># &lt;-- updated to latest (as of this writing)</span>
<span class="n">gem</span> <span class="s1">'react-rails'</span><span class="p">,</span> <span class="s1">'~&gt; 2.6'</span><span class="p">,</span> <span class="s1">'&gt;= 2.6.1'</span> <span class="c1"># &lt;-- added</span>
<span class="o">...</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I used the setup scripts to configure it for my Rails app.</p>
<div class="terminal">
<aside>You can delete the hello_world.jsx file this generates.</aside>
$ bundle install<br />
$ rails webpacker:install<br />
$ rails webpacker:install:react<br />
$ rails generate react:install<br />
</div>

<p>I added the JavaScript modules that I would need. I didn’t technically need to add
<code class="language-plaintext highlighter-rouge">webpack</code>, but it was nice to stop some warnings.</p>
<div class="terminal">
<aside>The antd-dayjs-webpack-plugin module replaces the dependency on moment.js</aside>
$ yarn add webpack<br />
$ yarn add antd<br />
$ yarn add antd-dayjs-webpack-plugin<br />
$ yarn add less<br />
$ yarn add less-loader<br />
$ yarn add babel-plugin-import<br />
</div>

<p>Now that everything was installed, I needed to configure everything. I started by writing
a configuration file for <code class="language-plaintext highlighter-rouge">less-loader</code>.</p>
<figure class="code-example">
  <figcaption class="title">config/webpack/loaders/less.js</figcaption>
  <aside>I add customizations to this file later. Note that lessOptions is part of options.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="source javascript"><pre>
<span class="kd">const</span> <span class="nx">getStyleRule</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@rails/webpacker/package/utils/get_style_rule</span><span class="dl">'</span><span class="p">)</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nf">getStyleRule</span><span class="p">(</span><span class="sr">/</span><span class="se">\.</span><span class="sr">less$/i</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="p">[</span>
  <span class="p">{</span>
    <span class="na">loader</span><span class="p">:</span> <span class="dl">'</span><span class="s1">less-loader</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">sourceMap</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">lessOptions</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">javascriptEnabled</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">])</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I updated the webpack environment to use my loader configuration file.</p>
<figure class="code-example">
  <figcaption class="title">config/webpack/environment.js</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="source javascript"><pre>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">environment</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@rails/webpacker</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">less</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./loaders/less</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">environment</span><span class="p">.</span><span class="nx">loaders</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="dl">'</span><span class="s1">style</span><span class="dl">'</span><span class="p">,</span> <span class="nx">less</span><span class="p">)</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">environment</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I edited webpacker’s configuration file to reflect these changes.</p>
<figure class="code-example">
  <figcaption class="title">config/webpacker.yml</figcaption>
  <aside>I still think that Less is a terrible name for a project; it is also the name of a command line utility.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="source yaml"><pre>
<span class="nn">...</span>
  <span class="na">extensions</span><span class="pi">:</span>
    <span class="s">...</span>
    <span class="s">- .less</span>
    <span class="s">- .module.less</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I edited Babel’s configuration to import the styles from Ant Design.</p>
<figure class="code-example">
  <figcaption class="title">babel.config.js</figcaption>
  <aside>The libraryDirectory option is important so that you don't accidentally import a bunch of files that you don't need. The style option could be 'css' instead if you don't want to customize the Less variables.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="source javascript"><pre>
<span class="p">...</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="p">...</span>
    <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">...</span>
      <span class="p">],</span>
      <span class="p">[</span>
        <span class="dl">"</span><span class="s2">import</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">libraryName</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">antd</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">libraryDirectory</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">es</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">style</span><span class="dl">"</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span>
      <span class="p">]</span>
    <span class="p">].</span><span class="nf">filter</span><span class="p">(</span><span class="nb">Boolean</span><span class="p">)</span>
  <span class="p">...</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I decided to create a component to ensure that everything was working properly.</p>
<div class="terminal">
$ rails generate react:component ComponentTest
</div>
<p>I based it on the first examples from the Ant Design documents.</p>
<figure class="code-example">
  <figcaption class="title">app/javascript/components/ComponentTest.js</figcaption>
  <aside>I will pass the props in as a hash in the .erb file that calls it.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="source JSX"><pre>
<span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Button</span><span class="p">,</span> <span class="nx">DatePicker</span><span class="p">,</span> <span class="nx">Switch</span><span class="p">,</span> <span class="nx">TimePicker</span><span class="p">,</span> <span class="nx">version</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">antd</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">ComponentTest</span> <span class="o">=</span> <span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return </span><span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">React</span><span class="p">.</span><span class="nc">Fragment</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Ant Design version: <span class="si">{</span><span class="nx">version</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">DatePicker</span> <span class="p">/&gt;</span>
        <span class="p">&lt;</span><span class="nc">Button</span> <span class="na">type</span><span class="p">=</span><span class="s">"primary"</span> <span class="na">style</span><span class="p">=</span><span class="si">{</span><span class="p">{</span> <span class="na">marginLeft</span><span class="p">:</span> <span class="mi">8</span> <span class="p">}</span><span class="si">}</span><span class="p">&gt;</span>
          Primary Button
        <span class="p">&lt;/</span><span class="nc">Button</span><span class="p">&gt;</span>
      <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">div</span> <span class="na">style</span><span class="p">=</span><span class="si">{</span><span class="p">{</span> <span class="na">marginTop</span><span class="p">:</span> <span class="mi">16</span> <span class="p">}</span><span class="si">}</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">TimePicker</span> <span class="na">size</span><span class="p">=</span><span class="s">"large"</span> <span class="p">/&gt;</span>
        <span class="p">&lt;</span><span class="nc">Switch</span> <span class="na">checkedChildren</span><span class="p">=</span><span class="si">{</span> <span class="nx">props</span><span class="p">.</span><span class="nx">checked</span> <span class="si">}</span> <span class="na">unCheckedChildren</span><span class="p">=</span><span class="si">{</span> <span class="nx">props</span><span class="p">.</span><span class="nx">unchecked</span> <span class="si">}</span> <span class="na">style</span><span class="p">=</span><span class="si">{</span><span class="p">{</span> <span class="na">marginLeft</span><span class="p">:</span> <span class="mi">8</span> <span class="p">}</span><span class="si">}</span> <span class="p">/&gt;</span>
      <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nc">React</span><span class="p">.</span><span class="nc">Fragment</span><span class="p">&gt;</span>
  <span class="p">)</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">ComponentTest</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I created a view to try it out.</p>
<div class="terminal">
$ rails g controller Welcome index
</div>

<p>I made that view the default.</p>
<figure class="code-example">
  <figcaption class="title">config/routes.rb</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="source ruby"><pre>
<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
  <span class="n">root</span> <span class="s1">'welcome#index'</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I edited the application layout to generate a stylesheet with <code class="language-plaintext highlighter-rouge">stylesheet_pack_tag</code>. This
is for the styles from my JavaScript.</p>
<figure class="code-example">
  <figcaption class="title">app/views/layouts/application.html.erb</figcaption>
  <aside>The stylesheet_pack_tag helper is painfully undocumented. It is used when extract_css is set true (see webpacker.yml), like in production environments.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="source ERB"><pre>
<span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="nt">&lt;html&gt;</span>
  <span class="nt">&lt;head&gt;</span>
    <span class="nt">&lt;title&gt;</span>Nessman<span class="nt">&lt;/title&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">csrf_meta_tags</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">csp_meta_tag</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">stylesheet_pack_tag</span> <span class="s1">'application'</span><span class="p">,</span> <span class="ss">media: </span><span class="s1">'all'</span><span class="p">,</span> <span class="s1">'data-turbolinks-track'</span><span class="p">:</span> <span class="s1">'reload'</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">javascript_pack_tag</span> <span class="s1">'application'</span><span class="p">,</span> <span class="s1">'data-turbolinks-track'</span><span class="p">:</span> <span class="s1">'reload'</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">stylesheet_link_tag</span> <span class="s1">'application'</span><span class="p">,</span> <span class="ss">media: </span><span class="s1">'all'</span><span class="p">,</span> <span class="s1">'data-turbolinks-track'</span><span class="p">:</span> <span class="s1">'reload'</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;/head&gt;</span>

  <span class="nt">&lt;body&gt;</span>
    <span class="cp">&lt;%=</span> <span class="k">yield</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I edited the view to render my new component.</p>
<figure class="code-example">
  <figcaption class="title">app/views/welcome/index.erb</figcaption>
  <aside>The hash is escaped and serialized into the HTML.</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="source ERB"><pre>
<span class="nt">&lt;div</span> <span class="na">style=</span><span class="s">"width: 480px; margin: 10px auto;"</span><span class="nt">&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">react_component</span><span class="p">(</span><span class="s1">'ComponentTest'</span><span class="p">,</span> <span class="p">{</span> <span class="ss">checked: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">unchecked: </span><span class="mi">0</span> <span class="p">})</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>It worked well, but I wanted to test one last thing. I edited my Less loader to modify
a variable in the design, changing the primary color to a bright red. Any of the 
<a href="https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less">default variables</a>
can be changed.</p>

<figure class="code-example">
  <figcaption class="title">config/webpack/loaders/less.js</figcaption>
  <aside>If I had a lot of changes I would put the overides in their own file</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="source javascript"><pre>
<span class="kd">const</span> <span class="nx">getStyleRule</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@rails/webpacker/package/utils/get_style_rule</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">overrides</span> <span class="o">=</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">@primary-color</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">#F00</span><span class="dl">'</span> <span class="p">}</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nf">getStyleRule</span><span class="p">(</span><span class="sr">/</span><span class="se">\.</span><span class="sr">less$/i</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="p">[</span>
  <span class="p">{</span>
    <span class="na">loader</span><span class="p">:</span> <span class="dl">'</span><span class="s1">less-loader</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">sourceMap</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">lessOptions</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">javascriptEnabled</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
        <span class="na">modifyVars</span><span class="p">:</span> <span class="nx">overrides</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">])</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>I put a <a href="http://nessman.herokuapp.com/">working demo</a> up on Heroku.</p>]]></content><author><name>Tad Thorley</name></author><summary type="html"><![CDATA[Ant Design is an incredibly thorough design system with a lot of useful React components. The trick is that all of the CSS is generated with Less and there are no plans to support Sass or anything else. Less support is really poor in the Rails community right now; the gems are largely abandoned. If you want to customize Ant Design with a Rails project you need to do it via Webpack.]]></summary></entry><entry><title type="html">Converting SQL Queries to Arel</title><link href="https://tad.thorley.dev/2020/01/07/converting-queries-to-arel.html" rel="alternate" type="text/html" title="Converting SQL Queries to Arel" /><published>2020-01-07T00:00:00+00:00</published><updated>2020-01-07T00:00:00+00:00</updated><id>https://tad.thorley.dev/2020/01/07/converting-queries-to-arel</id><content type="html" xml:base="https://tad.thorley.dev/2020/01/07/converting-queries-to-arel.html"><![CDATA[<p>My big project at work this year is to upgrade our codebase from Rails 4 to Rails 5
(and hopefully Rails 6 if we have time). The biggest obstacle is that we rely heavily on
the <a href="https://github.com/activerecord-hackery/squeel">Squeel</a> gem; a gem that was
abandoned 5 years ago and isn’t compatible with Rails 5. There is a newer alternative
called <a href="https://github.com/rzane/baby_squeel">Baby Squeel</a> that doesn’t tie into the
internals of <code class="language-plaintext highlighter-rouge">ActiveRecord</code>, but it is also undermaintained. My team concluded that our
best option is to rewrite everything using only <code class="language-plaintext highlighter-rouge">ActiveRecord</code> and the <code class="language-plaintext highlighter-rouge">Arel</code> that
underlies it. Unfortunately, I’ve found that <code class="language-plaintext highlighter-rouge">Arel</code> is underdocumented, so I’m sharing
what I’ve learned here.</p>

<p>First, I want to thank <a href="https://github.com/camertron">Cameron Dutro</a> for the
<a href="http://www.scuttle.io/">scuttle.io</a> website and the
<a href="https://github.com/camertron/arel-helpers">Arel Helpers</a> gem. We decided to just write
our own helpers rather than add a gem dependency to our codebase, but the gem was still
helpful. I’m using this method a lot.</p>

<figure class="code-example">
  <figcaption class="title">application_record.rb</figcaption>
  <aside>Practically stolen from the gem's arel_table.rb file</aside>
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="source ruby"><pre>
<span class="k">class</span> <span class="nc">ApplicationRecord</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">primary_abstract_class</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">[]</span><span class="p">(</span><span class="n">column</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">arel_table</span> <span class="k">if</span> <span class="n">column</span><span class="p">.</span><span class="nf">nil?</span>

    <span class="n">arel_table</span><span class="p">[</span><span class="n">column</span><span class="p">]</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>We have an extensive GraphQL API and these are the query parts I had to figure out.</p>

<figure class="code-example">
  <figcaption class="title">EXISTS</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="n">subquery</span><span class="p">.</span><span class="nf">arel</span><span class="p">.</span><span class="nf">exists</span><span class="p">)</span>
  <span class="c1"># example:</span>
  <span class="no">Supplier</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span>
    <span class="no">Product</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="ss">:id</span><span class="p">).</span><span class="nf">where</span><span class="p">(</span>
      <span class="no">Product</span><span class="p">[</span><span class="ss">:supplier_id</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="no">Supplier</span><span class="p">[</span><span class="ss">:id</span><span class="p">]).</span><span class="nf">and</span><span class="p">(</span><span class="no">Product</span><span class="p">[</span><span class="ss">:price</span><span class="p">].</span><span class="nf">lt</span><span class="p">(</span><span class="mi">20</span><span class="p">))</span>
    <span class="p">).</span><span class="nf">arel</span><span class="p">.</span><span class="nf">exists</span>
  <span class="p">)</span>
  <span class="c1"># SELECT * FROM suppliers WHERE EXISTS(SELECT id FROM products WHERE products.supplier_id = suppliers.id AND products.price &lt; 20)</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">NOT EXISTS</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="n">subquery</span><span class="p">.</span><span class="nf">arel</span><span class="p">.</span><span class="nf">exists</span><span class="p">.</span><span class="nf">not</span><span class="p">)</span>
  <span class="c1"># example:</span>
  <span class="no">Supplier</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span>
    <span class="no">Product</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="ss">:id</span><span class="p">).</span><span class="nf">where</span><span class="p">(</span>
      <span class="no">Product</span><span class="p">[</span><span class="ss">:supplier_id</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="no">Supplier</span><span class="p">[</span><span class="ss">:id</span><span class="p">]).</span><span class="nf">and</span><span class="p">(</span><span class="no">Product</span><span class="p">[</span><span class="ss">:price</span><span class="p">].</span><span class="nf">lt</span><span class="p">(</span><span class="mi">20</span><span class="p">))</span>
    <span class="p">).</span><span class="nf">arel</span><span class="p">.</span><span class="nf">exists</span><span class="p">.</span><span class="nf">not</span>
  <span class="p">)</span>
  <span class="c1"># SELECT * FROM suppliers WHERE NOT EXISTS(SELECT id FROM products WHERE products.supplier_id = suppliers.id AND products.price &lt; 20)</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">IN</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">bar: </span><span class="p">[</span><span class="s1">'baz'</span><span class="p">,</span> <span class="s1">'qux'</span><span class="p">])</span>
  <span class="c1"># example:</span>
  <span class="no">Client</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">zipcode: </span><span class="p">[</span><span class="s1">'84720'</span><span class="p">,</span> <span class="s1">'84113'</span><span class="p">])</span>
  <span class="c1"># SELECT * FROM clients WHERE zipcode IN ('84720', '84113')</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">NOT IN</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">.</span><span class="nf">not</span><span class="p">(</span><span class="ss">bar: </span><span class="p">[</span><span class="s1">'baz'</span><span class="p">,</span> <span class="s1">'qux'</span><span class="p">])</span>
  <span class="c1"># example:</span>
  <span class="no">Client</span><span class="p">.</span><span class="nf">where</span><span class="p">.</span><span class="nf">not</span><span class="p">(</span><span class="ss">zipcode: </span><span class="p">[</span><span class="s1">'84720'</span><span class="p">,</span> <span class="s1">'84113'</span><span class="p">])</span>
  <span class="c1"># SELECT * FROM clients WHERE zipcode NOT IN ('84720', '84113')</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">LIKE</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">matches</span><span class="p">(</span><span class="n">baz</span><span class="p">))</span>
  <span class="c1"># example:</span>
  <span class="n">search_string</span> <span class="o">=</span> <span class="s1">'Jo'</span>
  <span class="no">Client</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Client</span><span class="p">[</span><span class="ss">:first_name</span><span class="p">].</span><span class="nf">matches</span><span class="p">(</span><span class="n">search_string</span> <span class="o">+</span> <span class="s1">'%'</span><span class="p">))</span>
  <span class="c1"># SELECT * FROM clients WHERE first_name LIKE 'Jo%'</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">NOT LIKE</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">does_not_match</span><span class="p">(</span><span class="n">pattern</span><span class="p">))</span>
  <span class="c1"># example:</span>
  <span class="no">Client</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Client</span><span class="p">[</span><span class="ss">:email</span><span class="p">].</span><span class="nf">does_not_match</span><span class="p">(</span><span class="s1">'%@example.com'</span><span class="p">))</span>
  <span class="c1"># SELECT * FROM clients WHERE email NOT LIKE '%@example.com'</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">ILIKE (case-insensitive)</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">matches</span><span class="p">(</span><span class="n">pattern</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="kp">true</span><span class="p">))</span>
  <span class="c1"># example:</span>
  <span class="no">Client</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Client</span><span class="p">[</span><span class="ss">:name</span><span class="p">].</span><span class="nf">matches</span><span class="p">(</span><span class="s1">'%smith%'</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="kp">true</span><span class="p">))</span>
  <span class="c1"># SELECT * FROM clients WHERE name ILIKE '%smith%' (PostgreSQL)</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">IS NULL</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="kp">nil</span><span class="p">))</span>
  <span class="c1"># example:</span>
  <span class="no">Client</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Client</span><span class="p">[</span><span class="ss">:address</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="kp">nil</span><span class="p">))</span>
  <span class="c1"># SELECT * FROM clients WHERE address IS NULL</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">IS NOT NULL</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">not_eq</span><span class="p">(</span><span class="kp">nil</span><span class="p">))</span>
  <span class="c1"># example:</span>
  <span class="no">Client</span><span class="p">.</span><span class="nf">where</span><span class="p">.</span><span class="nf">not</span><span class="p">(</span><span class="no">Client</span><span class="p">[</span><span class="ss">:address</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="kp">nil</span><span class="p">))</span>
  <span class="c1"># SELECT * FROM clients WHERE address IS NOT NULL</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">BETWEEN</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="source ruby"><pre>
  <span class="no">Client</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">created_at: </span><span class="no">Date</span><span class="p">.</span><span class="nf">yesterday</span><span class="o">..</span><span class="no">Date</span><span class="p">.</span><span class="nf">tomorrow</span><span class="p">)</span>
  <span class="c1"># SELECT * FROM clients WHERE created_at BETWEEN '2020-01-07' AND '2020-01-09'</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">OR</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="s1">'a'</span><span class="p">).</span><span class="nf">or</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:baz</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="s1">'b'</span><span class="p">)))</span>
  <span class="c1"># example:</span>
  <span class="no">Client</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Client</span><span class="p">[</span><span class="ss">:status</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="s1">'active'</span><span class="p">).</span><span class="nf">or</span><span class="p">(</span><span class="no">Client</span><span class="p">[</span><span class="ss">:vip</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="kp">true</span><span class="p">)))</span>
  <span class="c1"># SELECT * FROM clients WHERE status = 'active' OR vip = true</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">GREATER THAN / LESS THAN</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">gt</span><span class="p">(</span><span class="n">value</span><span class="p">))</span>   <span class="c1"># &gt;</span>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">gteq</span><span class="p">(</span><span class="n">value</span><span class="p">))</span> <span class="c1"># &gt;=</span>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">lt</span><span class="p">(</span><span class="n">value</span><span class="p">))</span>   <span class="c1"># &lt;</span>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">lteq</span><span class="p">(</span><span class="n">value</span><span class="p">))</span> <span class="c1"># &lt;=</span>
  <span class="c1"># example:</span>
  <span class="no">Product</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="no">Product</span><span class="p">[</span><span class="ss">:price</span><span class="p">].</span><span class="nf">gteq</span><span class="p">(</span><span class="mi">100</span><span class="p">).</span><span class="nf">and</span><span class="p">(</span><span class="no">Product</span><span class="p">[</span><span class="ss">:price</span><span class="p">].</span><span class="nf">lteq</span><span class="p">(</span><span class="mi">500</span><span class="p">)))</span>
  <span class="c1"># SELECT * FROM products WHERE price &gt;= 100 AND price &lt;= 500</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">ORDER BY</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">asc</span><span class="p">)</span>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="no">Foo</span><span class="p">[</span><span class="ss">:bar</span><span class="p">].</span><span class="nf">desc</span><span class="p">)</span>
  <span class="c1"># example:</span>
  <span class="no">Client</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="no">Client</span><span class="p">[</span><span class="ss">:created_at</span><span class="p">].</span><span class="nf">desc</span><span class="p">,</span> <span class="no">Client</span><span class="p">[</span><span class="ss">:name</span><span class="p">].</span><span class="nf">asc</span><span class="p">)</span>
  <span class="c1"># SELECT * FROM clients ORDER BY created_at DESC, name ASC</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">INNER JOIN</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">joins</span><span class="p">(</span><span class="ss">:bars</span><span class="p">)</span> <span class="c1"># simple association</span>
  <span class="c1"># custom join condition:</span>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">joins</span><span class="p">(</span><span class="no">Foo</span><span class="p">.</span><span class="nf">arel_table</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">Bar</span><span class="p">.</span><span class="nf">arel_table</span><span class="p">).</span><span class="nf">on</span><span class="p">(</span>
    <span class="no">Foo</span><span class="p">[</span><span class="ss">:bar_id</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="no">Bar</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
  <span class="p">).</span><span class="nf">join_sources</span><span class="p">)</span>
  <span class="c1"># example:</span>
  <span class="no">Client</span><span class="p">.</span><span class="nf">joins</span><span class="p">(</span><span class="no">Client</span><span class="p">.</span><span class="nf">arel_table</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">Order</span><span class="p">.</span><span class="nf">arel_table</span><span class="p">).</span><span class="nf">on</span><span class="p">(</span>
    <span class="no">Client</span><span class="p">[</span><span class="ss">:id</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="no">Order</span><span class="p">[</span><span class="ss">:client_id</span><span class="p">]).</span><span class="nf">and</span><span class="p">(</span><span class="no">Order</span><span class="p">[</span><span class="ss">:status</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="s1">'active'</span><span class="p">))</span>
  <span class="p">).</span><span class="nf">join_sources</span><span class="p">)</span>
  <span class="c1"># SELECT * FROM clients INNER JOIN orders ON clients.id = orders.client_id AND orders.status = 'active'</span>
</pre></td></tr></tbody></table></div>
</figure>

<figure class="code-example">
  <figcaption class="title">LEFT JOIN</figcaption>
  
  <div class="overflow"><table class="code"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="source ruby"><pre>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">left_joins</span><span class="p">(</span><span class="ss">:bars</span><span class="p">)</span> <span class="c1"># simple association</span>
  <span class="c1"># custom join condition:</span>
  <span class="no">Foo</span><span class="p">.</span><span class="nf">joins</span><span class="p">(</span><span class="no">Foo</span><span class="p">.</span><span class="nf">arel_table</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">Bar</span><span class="p">.</span><span class="nf">arel_table</span><span class="p">,</span> <span class="no">Arel</span><span class="o">::</span><span class="no">Nodes</span><span class="o">::</span><span class="no">OuterJoin</span><span class="p">).</span><span class="nf">on</span><span class="p">(</span>
    <span class="no">Foo</span><span class="p">[</span><span class="ss">:bar_id</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="no">Bar</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
  <span class="p">).</span><span class="nf">join_sources</span><span class="p">)</span>
  <span class="c1"># example:</span>
  <span class="no">Client</span><span class="p">.</span><span class="nf">joins</span><span class="p">(</span><span class="no">Client</span><span class="p">.</span><span class="nf">arel_table</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">Order</span><span class="p">.</span><span class="nf">arel_table</span><span class="p">,</span> <span class="no">Arel</span><span class="o">::</span><span class="no">Nodes</span><span class="o">::</span><span class="no">OuterJoin</span><span class="p">).</span><span class="nf">on</span><span class="p">(</span>
    <span class="no">Client</span><span class="p">[</span><span class="ss">:id</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="no">Order</span><span class="p">[</span><span class="ss">:client_id</span><span class="p">])</span>
  <span class="p">).</span><span class="nf">join_sources</span><span class="p">).</span><span class="nf">where</span><span class="p">(</span><span class="no">Order</span><span class="p">[</span><span class="ss">:id</span><span class="p">].</span><span class="nf">eq</span><span class="p">(</span><span class="kp">nil</span><span class="p">))</span>
  <span class="c1"># SELECT * FROM clients LEFT OUTER JOIN orders ON clients.id = orders.client_id WHERE orders.id IS NULL</span>
</pre></td></tr></tbody></table></div>
</figure>

<p>Thanks for reading!</p>]]></content><author><name>Tad Thorley</name></author><summary type="html"><![CDATA[My big project at work this year is to upgrade our codebase from Rails 4 to Rails 5 (and hopefully Rails 6 if we have time). The biggest obstacle is that we rely heavily on the Squeel gem; a gem that was abandoned 5 years ago and isn’t compatible with Rails 5. There is a newer alternative called Baby Squeel that doesn’t tie into the internals of ActiveRecord, but it is also undermaintained. My team concluded that our best option is to rewrite everything using only ActiveRecord and the Arel that underlies it. Unfortunately, I’ve found that Arel is underdocumented, so I’m sharing what I’ve learned here.]]></summary></entry></feed>