<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>mentalized.net</title>
    <description>The virtual home of Jakob Skjerning, an experienced web application developer, consultancy owner and grumpy geek</description>
    <link>https://mentalized.net</link>
    <atom:link href="https://mentalized.net/journal/entries.xml" rel="self" type="application/rss+xml" />
    
      <item>
        <title>Announcing Flowbite Components for Rails</title>
        <description>&lt;p&gt;&lt;a href=&quot;https://substancelab.dk&quot;&gt;We&lt;/a&gt;‘re open sourcing our Flowbite component library, a set of view components, we’re using in &lt;a href=&quot;https://www.frontlobby.dk&quot;&gt;Front Lobby&lt;/a&gt;, &lt;a href=&quot;https://www.skrift.eu&quot;&gt;Skrift&lt;/a&gt;, and a bunch of other projects, both internal and external.&lt;/p&gt;



&lt;p&gt;These days there’s no shortage of UI kits for Rails. It seems &lt;a href=&quot;https://railsui.com/&quot;&gt;everybody&lt;/a&gt; &lt;a href=&quot;https://railsdesigner.com/components/&quot;&gt;is building&lt;/a&gt; &lt;a href=&quot;https://railsblocks.com/&quot;&gt;their own&lt;/a&gt; &lt;a href=&quot;https://instrumental.dev/docs/ui-components&quot;&gt;design system&lt;/a&gt; and selling a &lt;a href=&quot;https://nitrokit.dev/&quot;&gt;component library&lt;/a&gt; on top of it. We aren’t that ambitious; we just want a gem we can include in all our projects and be off to the races, preferably open source so we don’t have to bother with private gems.&lt;/p&gt;

&lt;p&gt;Enter &lt;a href=&quot;https://flowbite-components.substancelab.com&quot;&gt;flowbite-components&lt;/a&gt;: UI components for your &lt;a href=&quot;https://rubyonrails.org&quot;&gt;Rails&lt;/a&gt; application, built on &lt;a href=&quot;https://viewcomponent.org/&quot;&gt;ViewComponent&lt;/a&gt;, &lt;a href=&quot;https://tailwindcss.com&quot;&gt;Tailwind&lt;/a&gt;, and &lt;a href=&quot;https://flowbite.com&quot;&gt;Flowbite&lt;/a&gt;, licensed under an MIT license, and ready to use today.&lt;/p&gt;

&lt;h2&gt;Forms&lt;/h2&gt;

&lt;p&gt;A major driver for building &lt;a href=&quot;https://flowbite-components.substancelab.com&quot;&gt;flowbite-components&lt;/a&gt; was input fields.&lt;/p&gt;

&lt;p&gt;A complete input field isn’t just the input itself, but also its label, its error messages, and helper text. They have a bunch of states they can be in, like are there errors or is the control disabled, and they come in a multitude of types - all of which benefit from all of the above.&lt;/p&gt;

&lt;p&gt;Rails’ &lt;a href=&quot;https://guides.rubyonrails.org/form_helpers.html&quot;&gt;form helpers&lt;/a&gt; provide some building blocks for this, but we wanted a fully fledged experience we could bring along across projects, and still be sure that both the user experience is great, and the developer experience isn’t restricted moving forward.&lt;/p&gt;

&lt;p&gt;I think we’ve ended up on something that’s both flexible and expressive:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;form_with&lt;/span&gt; &lt;span&gt;model: &lt;/span&gt;&lt;span&gt;@user&lt;/span&gt; &lt;span&gt;do&lt;/span&gt; &lt;span&gt;|&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;|&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt; &lt;span&gt;Flowbite&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;InputField&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Text&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;attribute: :name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;form: &lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt; &lt;span&gt;Flowbite&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;InputField&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Email&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;attribute: :email&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;form: &lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt; &lt;span&gt;Flowbite&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Button&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;type: :submit&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;content: &lt;/span&gt;&lt;span&gt;&quot;Save&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;end&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And of course, if you don’t want to just use the defaults for a field, you have options for customizing each element of the &lt;a href=&quot;https://flowbite-components.substancelab.com/docs/components/Flowbite::InputField&quot;&gt;input field&lt;/a&gt;:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;  &lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt; &lt;span&gt;Flowbite&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;InputField&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Text&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;attribute: :name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;form: &lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
    &lt;span&gt;label: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;content: &lt;/span&gt;&lt;span&gt;&quot;Full Name&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;class: &lt;/span&gt;&lt;span&gt;&quot;text-2xl&quot;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;
    &lt;span&gt;hint: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;content: &lt;/span&gt;&lt;span&gt;&quot;Enter your full name&quot;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;Getting started&lt;/h2&gt;

&lt;p&gt;Flowbite Components is available now on RubyGems and &lt;a href=&quot;https://github.com/substancelab/flowbite-components/&quot;&gt;GitHub&lt;/a&gt;, and the documentation is &lt;a href=&quot;https://flowbite-components.substancelab.com/docs/components/Flowbite&quot;&gt;online&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Do check out the &lt;a href=&quot;https://flowbite-components.substancelab.com/docs/getting_started&quot;&gt;installation instructions&lt;/a&gt;, which unfortunately aren’t as simple as I’d like them to, at this point.&lt;/p&gt;

&lt;h2&gt;Why Flowbite?&lt;/h2&gt;

&lt;p&gt;There are many UI/design kits out there, so why Flowbite?&lt;/p&gt;

&lt;p&gt;Flowbite has a bunch of things going for it.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;It’s open source, MIT licensed, which means we can actually release this gem and not have it be a private gem just for us.&lt;/li&gt;
  &lt;li&gt;It comes with both &lt;a href=&quot;https://flowbite.com/docs/customize/dark-mode/&quot;&gt;dark and light mode&lt;/a&gt;, which pretty much is a must these days.&lt;/li&gt;
  &lt;li&gt;It has a bunch of &lt;a href=&quot;https://flowbite.com/blocks/&quot;&gt;premium pages and components&lt;/a&gt;, some of which are paid for, which makes me hopeful for its extensibility and longevity.&lt;/li&gt;
  &lt;li&gt;Importantly, it has a comprehensive set of well designed &lt;a href=&quot;https://flowbite.com/docs/forms/input-field/&quot;&gt;input components&lt;/a&gt;, with all the elements you need for a user-friendly CRUD interface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Outside of that, I like the look and feel. Their &lt;a href=&quot;https://flowbite.com/docs/customize/variables/&quot;&gt;semantic classes&lt;/a&gt; and easy &lt;a href=&quot;https://flowbite.com/docs/customize/theming/&quot;&gt;themability&lt;/a&gt; just reinforces the choice.&lt;/p&gt;

&lt;h2&gt;It’s a work in progress&lt;/h2&gt;

&lt;p&gt;Today, we’re officially releasing v0.2. Why not v1? We’re still polishing and tweaking the API, so I am reluctant to commit to a v1 just yet.&lt;/p&gt;

&lt;p&gt;Also, we’re really far behind the &lt;a href=&quot;https://flowbite.com/docs/components/accordion/&quot;&gt;full set of components&lt;/a&gt;. I mean, &lt;a href=&quot;https://flowbite-components.substancelab.com/docs/components/Flowbite&quot;&gt;we only have 9 so far&lt;/a&gt;, which doesn’t really warrant a v1.&lt;/p&gt;

&lt;h2&gt;The future&lt;/h2&gt;

&lt;p&gt;Will we ever support all the components that Flowbite has designed? I honestly doubt it. For now we’re extracting components from our existing application, and implementing new ones as we need them. If we never need a, say, &lt;a href=&quot;https://flowbite.com/docs/components/kbd/&quot;&gt;KBD&lt;/a&gt; component, we’ll likely never see one.&lt;/p&gt;

&lt;p&gt;That said, not all components need to live in a component library. &lt;a href=&quot;https://flowbite.com/docs/components/kbd/&quot;&gt;KBD&lt;/a&gt; is a good example of this; it’s effectively a single &lt;code&gt;div&lt;/code&gt; with a list of CSS class names. I am not sure exactly how much value is added by creating a &lt;code&gt;Flowbite::KBD&lt;/code&gt; component for this.&lt;/p&gt;

&lt;p&gt;Another thing I’d love to investigate is a form builder, perhaps similar to &lt;a href=&quot;https://github.com/heartcombo/simple_form&quot;&gt;Simple Form&lt;/a&gt;. Being able to say&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;form_with&lt;/span&gt; &lt;span&gt;model: &lt;/span&gt;&lt;span&gt;@user&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;builder: &lt;/span&gt;&lt;span&gt;Flowbite&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Form&lt;/span&gt; &lt;span&gt;do&lt;/span&gt; &lt;span&gt;|&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;|&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;f&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;input&lt;/span&gt; &lt;span&gt;:name&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;f&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;input&lt;/span&gt; &lt;span&gt;:address&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;f&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;submit&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;end&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and still get the full Flowbite experience would be pretty cool.&lt;/p&gt;
</description>
        <atom:summary>We‘re open sourcing our Flowbite component library, a set of view components, we’re using in Front Lobby, Skrift, and...</atom:summary>
        <pubDate>Sat, 21 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://mentalized.net/journal/2026/02/21/flowbite-components/</link>
        <guid isPermaLink="true">https://mentalized.net/journal/2026/02/21/flowbite-components/</guid>
      </item>
    
      <item>
        <title>Announcing styr</title>
        <description>&lt;p&gt;Ever needed to run some maintenance task in your production environment? For me this always triggers a bunch of questions: What’s that hostname again? What user should I log in as? And where did we deploy the application?&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://substancelab.com&quot;&gt;We&lt;/a&gt;‘ve built &lt;a href=&quot;https://github.com/substancelab/styr&quot;&gt;styr&lt;/a&gt; to answer those questions and ease the pain.&lt;/p&gt;



&lt;p&gt;With styr launching a Rails console in your production environment is as simple as&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ styr --target=production run bundle exec rails console
Running rails console on ⬢ redacted-production... up, run.9530
Loading production environment (Rails 7.2.1.2)
redacted(prod)&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And the best part is, if your application is reachable via SSH instead of being hosted on Heroku as above, launching a Rails console in your production environment is as simple as&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ styr --target=production run bundle exec rails console
Loading production environment (Rails 8.0.2.1)
redacted(prod)&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;Getting started&lt;/h2&gt;

&lt;p&gt;Just follow the fairly simple installation and usage instructions at &lt;a href=&quot;https://github.com/substancelab/styr&quot;&gt;substancelab/styr&lt;/a&gt;. Basically:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Grab the code from GitHub.&lt;/li&gt;
  &lt;li&gt;Add the &lt;code&gt;styr&lt;/code&gt; binary to your &lt;code&gt;PATH&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Configure targets in your application.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You should now be able to see &lt;code&gt;styr&lt;/code&gt; in all its glory:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ styr
styr [options] [--target] task [task options]
        --help                       Show helpful information
        --target TARGET              Target to perform the task on

Available tasks:
  run       Run a command on a target
  targets   List configured targets
  tasks     List available tasks
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;How about one-off scripts?&lt;/h2&gt;

&lt;p&gt;In my last post here I talked about &lt;a href=&quot;https://mentalized.net/journal/2025/10/16/heroku-run-rails-runner/&quot;&gt;running one-off scripts on Heroku with Rails&lt;/a&gt;, and of course you can do this with styr:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ cat hi.rb | styr --target=production run &quot;rails runner -&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;Legacy&lt;/h2&gt;

&lt;p&gt;Looking back at my posts, this has actually been an ongoing thing of mine, and in many ways &lt;code&gt;styr&lt;/code&gt; is the spiritual successor to &lt;a href=&quot;https://mentalized.net/journal/2015/11/22/capistrano-remote/&quot;&gt;capistrano-remote&lt;/a&gt; - just simpler and better.&lt;/p&gt;

&lt;h2&gt;The future&lt;/h2&gt;

&lt;p&gt;While the current version provides the bare minimum, I’d love to be able to add application specific tasks. How about running &lt;code&gt;styr --target=production rake db:migrate&lt;/code&gt; for example, or perhaps just &lt;code&gt;styr --target=production console&lt;/code&gt; to launch a console regardless of your application stack.&lt;/p&gt;

&lt;p&gt;I am also seeing something interesting in running tasks on multiple servers at once. Perhaps something like &lt;code&gt;styr --target=worker,web,app-01,app-02 run df -h&lt;/code&gt; to see free disk space across multiple servers?&lt;/p&gt;
</description>
        <atom:summary>Ever needed to run some maintenance task in your production environment? For me this always triggers a bunch of quest...</atom:summary>
        <pubDate>Sat, 24 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://mentalized.net/journal/2026/01/24/styr/</link>
        <guid isPermaLink="true">https://mentalized.net/journal/2026/01/24/styr/</guid>
      </item>
    
      <item>
        <title>One-off scripts on Heroku with Rails</title>
        <description>&lt;p&gt;Every so often you have a small script that needs to be run on your staging or production server, but committing and deploying the script on its own is just too much of a hassle. Luckily &lt;a href=&quot;https://rubyonrails.org/&quot;&gt;Rails&lt;/a&gt; and &lt;a href=&quot;https://www.heroku.com/&quot;&gt;Heroku&lt;/a&gt; has a great solution for this.&lt;/p&gt;



&lt;h2&gt;rails runner&lt;/h2&gt;

&lt;p&gt;If you have a script in your application that needs to run in the context of your Rails application, you can run it via &lt;a href=&quot;https://guides.rubyonrails.org/command_line.html#bin-rails-runner&quot;&gt;&lt;code&gt;rails runner&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For example, let’s say we have this very important script:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;puts&lt;/span&gt; &lt;span&gt;Rails&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Running this with just &lt;code&gt;ruby&lt;/code&gt; gives an error:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;$&lt;/span&gt; &lt;span&gt;ruby&lt;/span&gt; &lt;span&gt;hi&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;rb&lt;/span&gt;
&lt;span&gt;hi&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;rb&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;:in&lt;/span&gt; &lt;span&gt;`&amp;lt;main&amp;gt;&apos;: uninitialized constant Rails (NameError)

puts Rails.env
     ^^^^^
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This makes sense, since &lt;code&gt;Rails&lt;/code&gt; isn’t loaded. That’s where &lt;code&gt;rails runner&lt;/code&gt; steps up and ensures the full Rails environment is loaded:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;$&lt;/span&gt; &lt;span&gt;rails&lt;/span&gt; &lt;span&gt;runner&lt;/span&gt; &lt;span&gt;hi&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;rb&lt;/span&gt;
&lt;span&gt;development&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Pretty helpful!&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rails runner&lt;/code&gt; doesn’t only run scripts from disk, it also has a few other tricks up its sleeve. For example, you could pass the code directly:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;$&lt;/span&gt; &lt;span&gt;rails&lt;/span&gt; &lt;span&gt;runner&lt;/span&gt; &lt;span&gt;&quot;puts Rails.env&quot;&lt;/span&gt;
&lt;span&gt;development&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;or you can pipe the Ruby code to &lt;code&gt;rails runner&lt;/code&gt; - note the little &lt;code&gt;-&lt;/code&gt; at the end of the command there, that means read the script from stdin.&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;$&lt;/span&gt; &lt;span&gt;cat&lt;/span&gt; &lt;span&gt;hi&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;rb&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;rails&lt;/span&gt; &lt;span&gt;runner&lt;/span&gt;
&lt;span&gt;development&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;heroku run&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://devcenter.heroku.com/articles/heroku-cli&quot;&gt;Heroku’s CLI&lt;/a&gt; has a function similar to &lt;code&gt;rails runner&lt;/code&gt;, namely &lt;code&gt;heroku run&lt;/code&gt;.&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;Usage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;heroku&lt;/span&gt; &lt;span&gt;run&lt;/span&gt; &lt;span&gt;COMMAND&lt;/span&gt;

&lt;span&gt;Example&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;heroku&lt;/span&gt; &lt;span&gt;run&lt;/span&gt; &lt;span&gt;bash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That allows you to run a one-off process inside a Heroku dyno. Say, if we needed to run migrations on Heroku we could do&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;$&lt;/span&gt; &lt;span&gt;heroku&lt;/span&gt; &lt;span&gt;run&lt;/span&gt; &lt;span&gt;rails&lt;/span&gt; &lt;span&gt;db&lt;/span&gt;&lt;span&gt;:migrate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Also pretty helpful!&lt;/p&gt;

&lt;h2&gt;heroku run rails runner&lt;/h2&gt;

&lt;p&gt;Let’s combine those! &lt;code&gt;heroku run&lt;/code&gt; runs any process, what if that process was &lt;code&gt;rails runner&lt;/code&gt;?&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;$&lt;/span&gt; &lt;span&gt;heroku&lt;/span&gt; &lt;span&gt;run&lt;/span&gt; &lt;span&gt;&apos;rails runner &quot;puts Rails.env&quot;&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We do have to be fairly vigilant about placing our quotation marks so the correct things are run by the correct processes. In the above everything in &lt;code&gt;&apos;&lt;/code&gt; is run by &lt;code&gt;heroku run&lt;/code&gt;, which in turn runs everything in &lt;code&gt;&quot;&lt;/code&gt; using &lt;code&gt;rails runner&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;heroku run rails runner with a pipe&lt;/h2&gt;

&lt;p&gt;But can we pipe a script to &lt;code&gt;rails runner&lt;/code&gt; and have &lt;code&gt;heroku run&lt;/code&gt; run it in a dyno? I am so glad you asked! Yes, we can. &lt;code&gt;heroku run&lt;/code&gt; forwards stdin to the process being run, so we can do&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;$&lt;/span&gt; &lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;puts Rails.env&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;heroku&lt;/span&gt; &lt;span&gt;run&lt;/span&gt; &lt;span&gt;&quot;rails runner -&quot;&lt;/span&gt;
&lt;span&gt;Running&lt;/span&gt; &lt;span&gt;rails&lt;/span&gt; &lt;span&gt;runner&lt;/span&gt; &lt;span&gt;-&lt;/span&gt; &lt;span&gt;on&lt;/span&gt; &lt;span&gt;⬢&lt;/span&gt; &lt;span&gt;some&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;app&lt;/span&gt;&lt;span&gt;...&lt;/span&gt; &lt;span&gt;up&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;run&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;7413&lt;/span&gt;
&lt;span&gt;puts&lt;/span&gt; &lt;span&gt;Rails&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;
&lt;span&gt;production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note how the script/stdin is echoed back to us, which gets annoying for longer scripts. To avoid this we could use the &lt;code&gt;--no-tty&lt;/code&gt; flag:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;$&lt;/span&gt; &lt;span&gt;echo&lt;/span&gt; &lt;span&gt;&quot;puts Rails.env&quot;&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;heroku&lt;/span&gt; &lt;span&gt;run&lt;/span&gt; &lt;span&gt;--&lt;/span&gt;&lt;span&gt;no&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;tty&lt;/span&gt; &lt;span&gt;&quot;rails runner -&quot;&lt;/span&gt;
&lt;span&gt;Running&lt;/span&gt; &lt;span&gt;rails&lt;/span&gt; &lt;span&gt;runner&lt;/span&gt; &lt;span&gt;-&lt;/span&gt; &lt;span&gt;on&lt;/span&gt; &lt;span&gt;⬢&lt;/span&gt; &lt;span&gt;some&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;app&lt;/span&gt;&lt;span&gt;...&lt;/span&gt; &lt;span&gt;up&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;run&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;9354&lt;/span&gt;
&lt;span&gt;production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;How to run one-off Rails application scripts on Heroku&lt;/h2&gt;

&lt;p&gt;This leads us to the natural conclusion of all the above. In order to run a one-off script in a Heroku dyno with your Rails application environment loaded without having to add it to git and deploy, you can run it like so:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;$&lt;/span&gt; &lt;span&gt;cat&lt;/span&gt; &lt;span&gt;hi&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;rb&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; &lt;span&gt;heroku&lt;/span&gt; &lt;span&gt;run&lt;/span&gt; &lt;span&gt;--&lt;/span&gt;&lt;span&gt;no&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;tty&lt;/span&gt; &lt;span&gt;&quot;rails runner -&quot;&lt;/span&gt;
&lt;span&gt;Running&lt;/span&gt; &lt;span&gt;rails&lt;/span&gt; &lt;span&gt;runner&lt;/span&gt; &lt;span&gt;-&lt;/span&gt; &lt;span&gt;on&lt;/span&gt; &lt;span&gt;⬢&lt;/span&gt; &lt;span&gt;some&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;app&lt;/span&gt;&lt;span&gt;...&lt;/span&gt; &lt;span&gt;up&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;run&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;1846&lt;/span&gt;
&lt;span&gt;production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Very helpful indeed!&lt;/p&gt;

&lt;h2&gt;Styr might help you&lt;/h2&gt;

&lt;p&gt;If you find the above relevant or helpful, you might be interested in checking out &lt;a href=&quot;https://mentalized.net/journal/2026/01/24/styr/&quot;&gt;styr&lt;/a&gt; instead, which makes the above even easier.&lt;/p&gt;
</description>
        <atom:summary>Every so often you have a small script that needs to be run on your staging or production server, but committing and ...</atom:summary>
        <pubDate>Thu, 16 Oct 2025 00:00:00 +0000</pubDate>
        <link>https://mentalized.net/journal/2025/10/16/heroku-run-rails-runner/</link>
        <guid isPermaLink="true">https://mentalized.net/journal/2025/10/16/heroku-run-rails-runner/</guid>
      </item>
    
      <item>
        <title>Don&apos;t use instance variables in partials</title>
        <description>&lt;p&gt;One of my favorite pet peeves is instance variables in partials. Unfortuntaly, I still encounter these in actual production code.&lt;/p&gt;



&lt;p&gt;I get it, it happens. You have a view template that needs cleaning up:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;# app/views/employees/show.html.erb&lt;/span&gt;
&lt;span&gt;...&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;% @meetings.each &lt;/span&gt;&lt;span&gt;do&lt;/span&gt; &lt;span&gt;|&lt;/span&gt;&lt;span&gt;meeting&lt;/span&gt;&lt;span&gt;|&lt;/span&gt; &lt;span&gt;%&amp;gt;
  A bunch of code to render a meeting
&amp;lt;% end %&amp;gt;&lt;/span&gt;
&lt;span&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Easy enough, just extract some parts of it into a &lt;a href=&quot;https://guides.rubyonrails.org/layouts_and_rendering.html#using-partials&quot;&gt;partial&lt;/a&gt;:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;# app/views/employees/show.html.erb&lt;/span&gt;
&lt;span&gt;...&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;%= render(&quot;meetings&quot;) %&amp;gt;
...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;# app/views/employees/_meetings.html.erb&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;% @meetings.each &lt;/span&gt;&lt;span&gt;do&lt;/span&gt; &lt;span&gt;|&lt;/span&gt;&lt;span&gt;meeting&lt;/span&gt;&lt;span&gt;|&lt;/span&gt; &lt;span&gt;%&amp;gt;
  A bunch of code to render a meeting
&amp;lt;% end %&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now you suddenly have an instance variable in the partial. No biggie, you think, it works perfectly fine and that instance variable is already defined.&lt;/p&gt;

&lt;h2&gt;Why is this bad?&lt;/h2&gt;

&lt;p&gt;You’ve just coupled the partial to - not just the view - but the controller action, which is where the instance variable is defined.&lt;/p&gt;

&lt;p&gt;If you ever need to use that partial in a different context it’ll break. If you remove the &lt;code&gt;@meetings&lt;/code&gt; variable from the action (which isn’t unlikely because it’s obviously not being used in the view, so why even create it?) it’ll break. Boom, &lt;code&gt;NoMethodError: undefined method &apos;each&apos; for nil&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead, use locals. Even better, use &lt;a href=&quot;https://masilotti.com/safer-rails-partials-with-strict-locals/&quot;&gt;strict locals&lt;/a&gt;. Or perhaps even better, &lt;a href=&quot;https://www.viewcomponent.org&quot;&gt;ViewComponents&lt;/a&gt;, but that’s out of scope for this article.&lt;/p&gt;

&lt;h2&gt;Locals&lt;/h2&gt;

&lt;p&gt;If we instead rely on a local variable in the partial, the error would’ve been &lt;code&gt;undefined method or variable, meetings&lt;/code&gt;, which is much more direct understandable:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;# app/views/employees/_meeting.html.erb&lt;/span&gt;
&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;% meetings.each &lt;/span&gt;&lt;span&gt;do&lt;/span&gt; &lt;span&gt;|&lt;/span&gt;&lt;span&gt;meeting&lt;/span&gt;&lt;span&gt;|&lt;/span&gt; &lt;span&gt;%&amp;gt;
  A bunch of code to render a meeting
&amp;lt;% end %&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can then use the partial like this:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;% render(&quot;meetings&quot;, &lt;/span&gt;&lt;span&gt;:meetings&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;@meetings&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;While this is a bit more typing up front, it makes it painfully obvious what data the partial needs.&lt;/p&gt;

&lt;h2&gt;Strict locals&lt;/h2&gt;

&lt;p&gt;To make it even more explicit what locals a partial needs, we can configure the partial to use strict locals. Effectively document the “method signature” for a partial:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;%# locals (meetings:) %&amp;gt;
&amp;lt;% meetings.each do |meeting| %&amp;gt;
  A bunch of code to render a meeting
&amp;lt;% end %&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If we then try to render the partial without passing the &lt;code&gt;:meetings&lt;/code&gt; local we’d get a &lt;code&gt;ArgumentError: missing local: :meetings&lt;/code&gt; - again, much more explicit, understandable and actionable.&lt;/p&gt;

&lt;h2&gt;Don’t take my word for it&lt;/h2&gt;

&lt;p&gt;If you need more arguments against using instance variables in partials, just listen to &lt;a href=&quot;https://andycroll.com/ruby/dont-use-instance-variables-in-partials/&quot;&gt;Andy Croll&lt;/a&gt;.&lt;/p&gt;
</description>
        <atom:summary>One of my favorite pet peeves is instance variables in partials. Unfortuntaly, I still encounter these in actual prod...</atom:summary>
        <pubDate>Wed, 17 Sep 2025 00:00:00 +0000</pubDate>
        <link>https://mentalized.net/journal/2025/09/17/no-instance-variables-in-partials/</link>
        <guid isPermaLink="true">https://mentalized.net/journal/2025/09/17/no-instance-variables-in-partials/</guid>
      </item>
    
      <item>
        <title>How to fix &lt;code&gt;ld: library not found for -lzstd&lt;/code&gt; installing mysql2</title>
        <description>&lt;p&gt;I have a history of problems with installing the mysql2 gem, and this is just another chapter in &lt;a href=&quot;https://mentalized.net/journal/2020/08/06/mysql2-gem-with-mysql-56/&quot;&gt;that tale&lt;/a&gt;. If you receive a &lt;code&gt;ld: library not found for -lzstd&lt;/code&gt; error when running bundler or otherwise attempt to install the mysql2 gem, this might be the solution.&lt;/p&gt;



&lt;h2&gt;What I experienced&lt;/h2&gt;

&lt;p&gt;I am using &lt;a href=&quot;https://www.macports.org/&quot;&gt;MacPorts&lt;/a&gt; and trying to install the &lt;a href=&quot;https://rubygems.org/gems/mysql2&quot;&gt;mysql2 gem&lt;/a&gt; gave me the following error&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ gem install mysql2 -v 0.5.6
...
ld: library not found for -lzstd
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [mysql2.bundle] Error 1

make failed, exit code 2
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;TLDR; the solution&lt;/h2&gt;

&lt;p&gt;What worked for me was telling &lt;code&gt;ld&lt;/code&gt; that it can find the libzstd files in the MacPorts lib directory at &lt;code&gt;/opt/local/lib&lt;/code&gt;:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ gem install mysql2 -v 0.5.6 -- --with-ldflags=&quot;-L/opt/local/lib&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;More details&lt;/h2&gt;

&lt;p&gt;The relevant part of the error message above is&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;ld: library not found for -lzstd&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This happens when &lt;code&gt;ld&lt;/code&gt; wants to include some library, given after &lt;code&gt;-l&lt;/code&gt; (in this case &lt;code&gt;libzstd&lt;/code&gt;), but can’t. However the library &lt;em&gt;is&lt;/em&gt; installed as part of my MacPort installation:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ ls -1 /opt/local/lib/libzstd*
/opt/local/lib/libzstd.1.5.7.dylib
/opt/local/lib/libzstd.1.dylib
/opt/local/lib/libzstd.a
/opt/local/lib/libzstd.dylib
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Seeing that &lt;code&gt;ld&lt;/code&gt; can’t an installed library probably means that &lt;code&gt;ld&lt;/code&gt; is looking the wrong place - or at the very least, not looking in the right place. So let’s tell it to.&lt;/p&gt;

&lt;p&gt;Investigating &lt;a href=&quot;https://www.man7.org/linux/man-pages/man1/ld.1.html&quot;&gt;&lt;code&gt;man ld&lt;/code&gt;&lt;/a&gt; leads us to the &lt;code&gt;-L&lt;/code&gt;/&lt;code&gt;--library-path=&lt;/code&gt; command line argument:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;-Ldir: Add dir to the list of directories in which to search for libraries. Directories specified with -L are searched in the order they appear on the command line and before the default search path. In Xcode4 and later, there can be a space between the -L and directory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So maybe telling &lt;code&gt;ld&lt;/code&gt; where to find the libraries can help. However, we are not calling &lt;code&gt;ld&lt;/code&gt; directly ourselves, but rather &lt;a href=&quot;https://rubygems.org/&quot;&gt;RubyGems&lt;/a&gt; is calling it for us as part of the installation process.&lt;/p&gt;

&lt;p&gt;Luckily, we can forward arguments to the installation process:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ gem install mysql2 -v 0.5.6 -- --with-ldflags=&quot;-L/opt/local/lib&quot;
Building native extensions with: &apos;--with-ldflags=-L/opt/local/lib&apos;
This could take a while...
Successfully installed mysql2-0.5.6
1 gem installed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So basically, we’re telling &lt;code&gt;gem&lt;/code&gt; to tell &lt;code&gt;make&lt;/code&gt; to tell &lt;code&gt;ld&lt;/code&gt; to use &lt;code&gt;/opt/local/lib&lt;/code&gt; as the &lt;code&gt;-L&lt;/code&gt; argument.&lt;/p&gt;

&lt;h2&gt;Extra credits&lt;/h2&gt;

&lt;p&gt;Unfortunately this is a one-time thing. The next time you have to install mysql2, it’ll fail again - there’s no persistence here.&lt;/p&gt;

&lt;p&gt;We can, however, take advantage of &lt;a href=&quot;https://bundler.io/&quot;&gt;bundler&lt;/a&gt; here. If you do&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ bundle config --local build.mysql2 &quot;--with-ldflags=-L/opt/local/lib&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;you tell &lt;code&gt;bundler&lt;/code&gt; to tell &lt;code&gt;gem&lt;/code&gt; to tell &lt;code&gt;make&lt;/code&gt; to tell &lt;code&gt;ld&lt;/code&gt; to use &lt;code&gt;/opt/local/lib&lt;/code&gt; in all future installations of the &lt;code&gt;mysql2&lt;/code&gt; gem.&lt;/p&gt;
</description>
        <atom:summary>I have a history of problems with installing the mysql2 gem, and this is just another chapter in that tale. If you re...</atom:summary>
        <pubDate>Mon, 04 Aug 2025 00:00:00 +0000</pubDate>
        <link>https://mentalized.net/journal/2025/08/04/mysql-gem-library-not-found/</link>
        <guid isPermaLink="true">https://mentalized.net/journal/2025/08/04/mysql-gem-library-not-found/</guid>
      </item>
    
      <item>
        <title>Designing the API for a ViewComponent Input Group</title>
        <description>&lt;p&gt;I’ve been working on a set of form input components based on the &lt;a href=&quot;https://viewcomponent.org/&quot;&gt;ViewComponent&lt;/a&gt; library. Unfortunately I’ve been having problems deciding on an API I like and this post is my attempt to think it through publicly.&lt;/p&gt;



&lt;h2&gt;The simple case&lt;/h2&gt;

&lt;p&gt;The simplest case for an input group is something like&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;InputGroup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;:name&lt;/span&gt;&lt;span&gt;))&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;which should render something like&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;label&lt;/span&gt; &lt;span&gt;for=&lt;/span&gt;&lt;span&gt;&quot;user_name&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;Name&lt;span&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;input&lt;/span&gt; &lt;span&gt;id=&lt;/span&gt;&lt;span&gt;&quot;user_name&quot;&lt;/span&gt; &lt;span&gt;name=&lt;/span&gt;&lt;span&gt;&quot;user[name]&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s easy and straightforward to implement:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;InputGroup&lt;/span&gt; &lt;span&gt;&amp;lt;&lt;/span&gt; &lt;span&gt;ViewComponent&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Base&lt;/span&gt;
  &lt;span&gt;erb_template&lt;/span&gt; &lt;span&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span&gt;ERB&lt;/span&gt;&lt;span&gt;
    &amp;lt;%= @form.label(@attribute) %&amp;gt;
    &amp;lt;%= @form.text_field(@attribute) %&amp;gt;
&lt;/span&gt;&lt;span&gt;  ERB&lt;/span&gt;

  &lt;span&gt;def&lt;/span&gt; &lt;span&gt;initialize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;attribute&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;@attribute&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;attribute&lt;/span&gt;
    &lt;span&gt;@form&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;form&lt;/span&gt;
  &lt;span&gt;end&lt;/span&gt;
&lt;span&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;Specify the label text&lt;/h2&gt;

&lt;p&gt;Now, it’s fairly common to change the text of the label to something other than the default, ie to get something like:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;label&lt;/span&gt; &lt;span&gt;for=&lt;/span&gt;&lt;span&gt;&quot;user_name&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;Please enter your full name&lt;span&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;input&lt;/span&gt; &lt;span&gt;id=&lt;/span&gt;&lt;span&gt;&quot;user_name&quot;&lt;/span&gt; &lt;span&gt;name=&lt;/span&gt;&lt;span&gt;&quot;user[name]&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;An API for this, which goes hand in hand with &lt;a href=&quot;https://viewcomponent.org/guide/slots.html#with_slot_name_content&quot;&gt;what View Component already provides&lt;/a&gt; is:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;InputGroup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;:name&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;with_label_content&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Please enter your full name)) &lt;/span&gt;&lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For this to work we need to implement a &lt;code&gt;LabelComponent&lt;/code&gt; that uses the &lt;a href=&quot;https://viewcomponent.org/api.html#content--string&quot;&gt;&lt;code&gt;#content&lt;/code&gt;&lt;/a&gt; accessor:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;LabelComponent&lt;/span&gt; &lt;span&gt;&amp;lt;&lt;/span&gt; &lt;span&gt;ViewComponent&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Base&lt;/span&gt;
  &lt;span&gt;erb_template&lt;/span&gt; &lt;span&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span&gt;ERB&lt;/span&gt;&lt;span&gt;
    &amp;lt;%= @form.label(@attribute, content, **options) %&amp;gt;
&lt;/span&gt;&lt;span&gt;  ERB&lt;/span&gt;

  &lt;span&gt;def&lt;/span&gt; &lt;span&gt;initialize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;attribute&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;**&lt;/span&gt;&lt;span&gt;options&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;@attribute&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;attribute&lt;/span&gt;
    &lt;span&gt;@form&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;form&lt;/span&gt;
    &lt;span&gt;@options&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;options&lt;/span&gt;
  &lt;span&gt;end&lt;/span&gt;
&lt;span&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and we need to use that component. Initially I thought I could use a &lt;a href=&quot;https://viewcomponent.org/guide/slots.html#component-slots&quot;&gt;component slot&lt;/a&gt; for this, but that doesn’t work since we have to pass extra options (ie &lt;code&gt;@attribute&lt;/code&gt; and &lt;code&gt;@form&lt;/code&gt;) to the label. Well, we could make it work, but that’d require the consumer to pass those arguments for us:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt; &lt;span&gt;InputGroup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;:name&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;do&lt;/span&gt; &lt;span&gt;|&lt;/span&gt;&lt;span&gt;component&lt;/span&gt;&lt;span&gt;|&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;component&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;with_label&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;:name&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;&quot;Please enter your full name&quot;&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;end&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and that’s too repetitive and error prone. A better solution is to use a &lt;a href=&quot;https://viewcomponent.org/guide/slots.html#lambda-slots&quot;&gt;lambda slot&lt;/a&gt;, which is intended to work as “wrappers for another ViewComponent with specific default values”:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;InputGroup&lt;/span&gt;
  &lt;span&gt;...&lt;/span&gt;
  &lt;span&gt;renders_one&lt;/span&gt; &lt;span&gt;:label&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;block&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;do&lt;/span&gt;
    &lt;span&gt;arguments&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;attribute: &lt;/span&gt;&lt;span&gt;@attribute&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;form: &lt;/span&gt;&lt;span&gt;@form&lt;/span&gt;
    &lt;span&gt;}.&lt;/span&gt;&lt;span&gt;merge&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

    &lt;span&gt;LabelComponent&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;arguments&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;block&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;end&lt;/span&gt;
  &lt;span&gt;...&lt;/span&gt;
&lt;span&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With that in place, we get the expected output:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;label&lt;/span&gt; &lt;span&gt;for=&lt;/span&gt;&lt;span&gt;&quot;user_name&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;Please enter your full name&lt;span&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;input&lt;/span&gt; &lt;span&gt;id=&lt;/span&gt;&lt;span&gt;&quot;user_name&quot;&lt;/span&gt; &lt;span&gt;name=&lt;/span&gt;&lt;span&gt;&quot;user[name]&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So far, so good. However, while changing just the label text is a common usage case, it is also a simple case. There might be cases where the consumer wants to do crazy and do something entirely different for the label.&lt;/p&gt;

&lt;h2&gt;Replacing the entire label&lt;/h2&gt;

&lt;p&gt;I want the consumers of my component to be able to do whatever they want for their labels. Perhaps they want to not have a label, or add more details to their labels, or otherwise go nuts, I’m not judging:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Your username&lt;span&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This will be used whenever you log in&lt;span&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;input&lt;/span&gt; &lt;span&gt;id=&lt;/span&gt;&lt;span&gt;&quot;user_name&quot;&lt;/span&gt; &lt;span&gt;name=&lt;/span&gt;&lt;span&gt;&quot;user[name]&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Using standard ViewComponent practices and slots that could look something like:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt; &lt;span&gt;InputGroup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt; &lt;span&gt;do&lt;/span&gt; &lt;span&gt;|&lt;/span&gt;&lt;span&gt;component&lt;/span&gt;&lt;span&gt;|&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;component&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;with_label&lt;/span&gt; &lt;span&gt;do&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
    &lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;LabelWithDescription&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;end&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;end&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Alas, this doesn’t work as hoped. The return value of the block passed to &lt;code&gt;with_label&lt;/code&gt; is used as the content for the &lt;code&gt;label&lt;/code&gt; slot, which means we’ll pass that content onto our &lt;code&gt;LabelComponent&lt;/code&gt;, effectively rendering a &lt;code&gt;LabelComponent&lt;/code&gt; with a &lt;code&gt;LabelWithDescription&lt;/code&gt; inside it. That’s not what we want.&lt;/p&gt;

&lt;p&gt;(Aside: This begs the question; what is the actual difference between &lt;code&gt;with_label_content&lt;/code&gt; and &lt;code&gt;with_label&lt;/code&gt;? 🤔)&lt;/p&gt;

&lt;h2&gt;Back to the drawing board…&lt;/h2&gt;

&lt;p&gt;The only way I know of where we can achieve that last part is by using a blank/anonymous slot definition:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;InputGroup&lt;/span&gt;
  &lt;span&gt;...&lt;/span&gt;
  &lt;span&gt;renders_one&lt;/span&gt; &lt;span&gt;:label&lt;/span&gt;
  &lt;span&gt;...&lt;/span&gt;
&lt;span&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;However, this means we need to find a new way to handle the simple case, since&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;InputGroup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;:name&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;with_label_content&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Please enter your full name)) &lt;/span&gt;&lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;is now also going to just render whatever we pass to &lt;code&gt;#with_label_content&lt;/code&gt;, ie without the wrapping &lt;code&gt;LabelComponent&lt;/code&gt;. Thankfully, ViewComponent 4.0 introduces &lt;a href=&quot;https://viewcomponent.org/guide/slots.html#with_slot_name_content&quot;&gt;&lt;code&gt;#default_SLOT_NAME&lt;/code&gt;&lt;/a&gt; methods, which is used to return whatever should be in a slot when content isn’t provided.&lt;/p&gt;

&lt;p&gt;This means we can do&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;InputGroup&lt;/span&gt;
  &lt;span&gt;def&lt;/span&gt; &lt;span&gt;default_label&lt;/span&gt;
    &lt;span&gt;LabelComponent&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;@form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;@attribute&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;end&lt;/span&gt;
&lt;span&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;to handle the simple case where no content is provided for the slot. We still need to find a way to specify custom text for the label, though. Instead of using &lt;code&gt;#with_label_content&lt;/code&gt; we should be able to pass it as an argument, ie something like:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;InputGroup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;:name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;label: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;content: &lt;/span&gt;&lt;span&gt;&quot;Please enter your full name&quot;&lt;/span&gt;&lt;span&gt;}))&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To handle that we need to change our initializer:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;initialize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;attribute&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;label: &lt;/span&gt;&lt;span&gt;{})&lt;/span&gt;
  &lt;span&gt;@attribute&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;attribute&lt;/span&gt;
  &lt;span&gt;@form&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;form&lt;/span&gt;
  &lt;span&gt;@label&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;label&lt;/span&gt;
&lt;span&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and our &lt;code&gt;default_label&lt;/code&gt; method:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;default_label&lt;/span&gt;
  &lt;span&gt;label_options&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;@label&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;dup&lt;/span&gt;

  &lt;span&gt;# Extract the content argument&lt;/span&gt;
  &lt;span&gt;label_content&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;label_options&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;delete&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;:content&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

  &lt;span&gt;# Build a component with the remaining arguments&lt;/span&gt;
  &lt;span&gt;component&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;LabelComponent&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;attribute&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;**&lt;/span&gt;&lt;span&gt;label_options&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;if&lt;/span&gt; &lt;span&gt;label_content&lt;/span&gt;
    &lt;span&gt;# Pass the content argument as the content of the component if it exists&lt;/span&gt;
    &lt;span&gt;component&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;with_content&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;label_content&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  &lt;span&gt;else&lt;/span&gt;
    &lt;span&gt;# ... otherwise just use the default&lt;/span&gt;
    &lt;span&gt;component&lt;/span&gt;
  &lt;span&gt;end&lt;/span&gt;
&lt;span&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With the above in place, we’re back in business. The following&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;InputGroup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;:name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;label: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;content: &lt;/span&gt;&lt;span&gt;&quot;Please enter your full name&quot;&lt;/span&gt;&lt;span&gt;}))&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;now renders&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;label&lt;/span&gt; &lt;span&gt;for=&lt;/span&gt;&lt;span&gt;&quot;user_name&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;Please enter your full name&lt;span&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;input&lt;/span&gt; &lt;span&gt;id=&lt;/span&gt;&lt;span&gt;&quot;user_name&quot;&lt;/span&gt; &lt;span&gt;name=&lt;/span&gt;&lt;span&gt;&quot;user[name]&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;just like we want it to &lt;strong&gt;and&lt;/strong&gt; we get the default behavior using&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;InputGroup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;:name&lt;/span&gt;&lt;span&gt;))&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;and&lt;/strong&gt; we can replace the entire component with any content we desire:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt; &lt;span&gt;InputGroup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;:name&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;do&lt;/span&gt; &lt;span&gt;|&lt;/span&gt;&lt;span&gt;component&lt;/span&gt;&lt;span&gt;|&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;component&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;with_label&lt;/span&gt; &lt;span&gt;do&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
    &lt;span&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Who are you&lt;span&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;label&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;:name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;&quot;What&apos;s your name?&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
  &lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;end&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;%&lt;/span&gt; &lt;span&gt;end&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;Bonus: Label attributes&lt;/h2&gt;

&lt;p&gt;The above way of doing things has a neat bonus; it makes it really easy for us to pass options to our label component. Say we want to add a CSS class to the label, this code&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt; &lt;span&gt;render&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;InputGroup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;:name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;label: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;class: &lt;/span&gt;&lt;span&gt;&quot;required&quot;&lt;/span&gt;&lt;span&gt;}))&lt;/span&gt; &lt;span&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;should end up rendering&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;label&lt;/span&gt; &lt;span&gt;for=&lt;/span&gt;&lt;span&gt;&quot;user_name&quot;&lt;/span&gt; &lt;span&gt;class=&lt;/span&gt;&lt;span&gt;&quot;required&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;User name&lt;span&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;input&lt;/span&gt; &lt;span&gt;id=&lt;/span&gt;&lt;span&gt;&quot;user_name&quot;&lt;/span&gt; &lt;span&gt;name=&lt;/span&gt;&lt;span&gt;&quot;user[name]&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Lo and behold, it already does. Since we pass everything in the &lt;code&gt;label&lt;/code&gt; argument onto &lt;code&gt;LabelComponent&lt;/code&gt; we’ve already implemented this. What a nice bonus.&lt;/p&gt;

&lt;h2&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;I think I am fairly happy with this API. It…&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Gives us sensible defaults.&lt;/li&gt;
  &lt;li&gt;Allows us to easily override the most common default (ie label text).&lt;/li&gt;
  &lt;li&gt;Allows us to pass arguments to the label component to customize it.&lt;/li&gt;
  &lt;li&gt;Allows us to replace the label component entirely with something more fancy.&lt;/li&gt;
  &lt;li&gt;Paves a path forward where other slots can be customized in the same way, ie &lt;code&gt;render(InputGroup.new(form, :name, input: {class: &quot;w-100&quot;}))&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
</description>
        <atom:summary>I’ve been working on a set of form input components based on the ViewComponent library. Unfortunately I’ve been havin...</atom:summary>
        <pubDate>Tue, 29 Jul 2025 00:00:00 +0000</pubDate>
        <link>https://mentalized.net/journal/2025/07/29/input-group-component-api/</link>
        <guid isPermaLink="true">https://mentalized.net/journal/2025/07/29/input-group-component-api/</guid>
      </item>
    
      <item>
        <title>Driving View Transitions with Hotwired/Turbo</title>
        <description>&lt;p&gt;View transitions between pages has been a reality on the web for quite a while now (at least in the browsers that support it). It has also been possible to use them with Turbo, albeit somewhat &lt;a href=&quot;https://dev.to/nejremeslnici/how-to-use-view-transitions-in-hotwire-turbo-1kdi&quot;&gt;cumbersome&lt;/a&gt;, but that’s changed. &lt;a href=&quot;https://turbo.hotwired.dev/&quot;&gt;Turbo&lt;/a&gt; now has built-in support for the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API&quot;&gt;View Transitions API&lt;/a&gt; as of version 8+.&lt;/p&gt;



&lt;h2&gt;Opt in to view transitions in Turbo Drive&lt;/h2&gt;

&lt;p&gt;First of all, ensure you’re using &lt;a href=&quot;https://caniuse.com/view-transitions&quot;&gt;a browser with View Transitions support&lt;/a&gt;, ie not Firefox, alas.&lt;/p&gt;

&lt;p&gt;All we have to do to enable Turbos built-in support for view transitions is &lt;a href=&quot;https://turbo.hotwired.dev/handbook/drive#view-transitions&quot;&gt;add a meta tag&lt;/a&gt; to our page(s):&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;meta&lt;/span&gt; &lt;span&gt;name=&lt;/span&gt;&lt;span&gt;&quot;view-transition&quot;&lt;/span&gt; &lt;span&gt;content=&lt;/span&gt;&lt;span&gt;&quot;same-origin&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;… and that’s it! When you navigate between pages you now get the default crossfading transition.&lt;/p&gt;

&lt;figure&gt;
  &lt;video controls=&quot;&quot; src=&quot;https://res.cloudinary.com/substancelab/video/upload/f_auto,q_auto,w_1024/v1745068281/mentalized/hotwired-turbo-view-transitions/default-transition.mp4&quot; title=&quot;Default view transition&quot;&gt;&lt;/video&gt;
&lt;/figure&gt;

&lt;h2&gt;Make it easier to see what’s going on&lt;/h2&gt;

&lt;p&gt;In the real world we don’t want the transitions to take more than a few hundred miliseconds, if that. Any longer and the app starts feeling slow and sluggish. Remember, the transition cannot start until the new page has been actually loaded, so the transition does indeed slow down the user.&lt;/p&gt;

&lt;p&gt;However, for debugging it can be beneficial to slow the transitions down so we can actually see what’s happening. We can do that by target the view-transition pseudo elements and set their animation duration:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;::view-transition-old&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;root&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;
&lt;span&gt;::view-transition-new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;root&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;animation-duration&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;5s&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;figure&gt;
  &lt;video controls=&quot;&quot; src=&quot;https://res.cloudinary.com/substancelab/video/upload/f_auto,q_auto,w_1024/v1745068281/mentalized/hotwired-turbo-view-transitions/slow-transition.mp4&quot; title=&quot;Default view transition slowed down&quot;&gt;&lt;/video&gt;
&lt;/figure&gt;

&lt;h2&gt;Animate the outgoing page&lt;/h2&gt;

&lt;p&gt;The default crossfade is boring, though, let’s spice it up.&lt;/p&gt;

&lt;p&gt;A great thing about the view transition animations is that they are just standard CSS animations. This means we can use pretty much all the tricks and timing functions, we’re used to. Let’s define an animation that moves an element left and fades it out:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;@keyframes&lt;/span&gt; &lt;span&gt;exit-left&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;0&lt;/span&gt;&lt;span&gt;%&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;transform&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;100%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;100&lt;/span&gt;&lt;span&gt;%&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;transform&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-100%&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;0%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;/* ... and apply it to the leaving page */&lt;/span&gt;
&lt;span&gt;::view-transition-old&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;root&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;animation-name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;exit-left&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;animation-timing-function&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ease-in&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;By applying this animation to the &lt;code&gt;::view-transition-old&lt;/code&gt; element, we’ve made the existing page look like it disappears stage left, while the new page still crossfades in.&lt;/p&gt;

&lt;figure&gt;
  &lt;video controls=&quot;&quot; src=&quot;https://res.cloudinary.com/substancelab/video/upload/f_auto,q_auto,w_1024/v1745068281/mentalized/hotwired-turbo-view-transitions/old-exit-left.mp4&quot; title=&quot;Animate the old page out to the left&quot;&gt;&lt;/video&gt;
&lt;/figure&gt;

&lt;h2&gt;Animate the incoming page&lt;/h2&gt;

&lt;p&gt;Let’s change how to new page appears by adding a new animation that makes it appear to come in from the right:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;@keyframes&lt;/span&gt; &lt;span&gt;enter-left&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;0&lt;/span&gt;&lt;span&gt;%&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;transform&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;100%&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;0%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;100&lt;/span&gt;&lt;span&gt;%&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;transform&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0%&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;100%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;::view-transition-new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;root&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;animation-name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;enter-left&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;animation-timing-function&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ease-out&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;figure&gt;
  &lt;video controls=&quot;&quot; src=&quot;https://res.cloudinary.com/substancelab/video/upload/f_auto,q_auto,w_1024/v1745068281/mentalized/hotwired-turbo-view-transitions/new-enter-left.mp4&quot; title=&quot;Animate the new page in from the right&quot;&gt;&lt;/video&gt;
&lt;/figure&gt;

&lt;p&gt;This looks pretty neat, huh? Now, you can obviously play with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function&quot;&gt;timing functions&lt;/a&gt; and animations to your hearts content and creative ability.&lt;/p&gt;

&lt;h2&gt;Going directions&lt;/h2&gt;

&lt;p&gt;View transitions appear on all page changes – not just the ones triggered by Turbo Drive. This also means, that when we click the Back button, we go back to the previous page with a pretty transition. However, the animation is the same as when we navigate by clicking.&lt;/p&gt;

&lt;figure&gt;
  &lt;video controls=&quot;&quot; src=&quot;https://res.cloudinary.com/substancelab/video/upload/f_auto,q_auto,w_1024/v1745068281/mentalized/hotwired-turbo-view-transitions/restored-page-enters-left.mp4&quot; title=&quot;Pages from history still arrive moving in from the right&quot;&gt;&lt;/video&gt;
&lt;/figure&gt;

&lt;p&gt;Since the page we navigate to by using the back page is seen by view transitions as a new page (even though it’s actually a page from the cache) it will be animated as a new transition and enter from the right, which looks unexpected.&lt;/p&gt;

&lt;p&gt;Luckily, &lt;a href=&quot;https://turbo.hotwired.dev/handbook/drive#view-transitions&quot;&gt;Turbo has our back&lt;/a&gt; here:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Turbo also adds a &lt;code&gt;data-turbo-visit-direction&lt;/code&gt; attribute to the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element to indicate the direction of the transition. The attribute can have one of the following values:&lt;/p&gt;

  &lt;ul&gt;
    &lt;li&gt;forward in advance visits.&lt;/li&gt;
    &lt;li&gt;back in restoration visits.&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means we can modify our CSS selectors and change the animations based on what direction we’re going; &lt;strong&gt;forward&lt;/strong&gt; when we’re moving forward in the history using links or the forward button, and &lt;strong&gt;back&lt;/strong&gt; when using the back button.&lt;/p&gt;

&lt;h2&gt;Only slide forward&lt;/h2&gt;

&lt;p&gt;So let’s start by ensuring we only use our left-moving animations when we navigate forward:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;data-turbo-visit-direction&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;forward&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&amp;amp;::view-transition-new(root)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;animation-name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;enter-left&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;animation-timing-function&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ease-out&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;::view-transition-old&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;root&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;animation-name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;exit-left&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;animation-timing-function&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ease-in&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Doing this and we get our fancy animation when we navigate forward, ie click on links, and the default crossfade when going back by using the back button.&lt;/p&gt;

&lt;figure&gt;
  &lt;video controls=&quot;&quot; src=&quot;https://res.cloudinary.com/substancelab/video/upload/f_auto,q_auto,w_1024/v1745068281/mentalized/hotwired-turbo-view-transitions/restored-page-fades-in.mp4&quot; title=&quot;Pages from history now crossfade in&quot;&gt;&lt;/video&gt;
&lt;/figure&gt;

&lt;h2&gt;Slide history pages in&lt;/h2&gt;

&lt;p&gt;We can improve the back animation, for example to have it go the other way of the forward animation. Let’s start by adding two animations going the other way, ie one that enters going right and one that exits going right:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;@keyframes&lt;/span&gt; &lt;span&gt;exit-right&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;0&lt;/span&gt;&lt;span&gt;%&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;transform&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;100%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;100&lt;/span&gt;&lt;span&gt;%&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;transform&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;100%&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;0%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;@keyframes&lt;/span&gt; &lt;span&gt;enter-right&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;0&lt;/span&gt;&lt;span&gt;%&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;transform&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;-100%&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;0%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;100&lt;/span&gt;&lt;span&gt;%&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;transform&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;translateX&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0%&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
    &lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;100%&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can then use those animations when the navigation direction is “back”:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;data-turbo-visit-direction&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;back&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;&amp;amp;::view-transition-new(root)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;animation-name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;enter-right&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;animation-timing-function&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ease-out&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;::view-transition-old&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;root&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;animation-name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;exit-right&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;animation-timing-function&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;ease-in&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;figure&gt;
  &lt;video controls=&quot;&quot; src=&quot;https://res.cloudinary.com/substancelab/video/upload/f_auto,q_auto,w_1024/v1745068281/mentalized/hotwired-turbo-view-transitions/restored-page-enters-right.mp4&quot; title=&quot;Pages from history arrive moving right&quot;&gt;&lt;/video&gt;
&lt;/figure&gt;

&lt;h2&gt;Animate just part of the page&lt;/h2&gt;

&lt;p&gt;Until now we’ve animated the entire page, which is a bit much. Let’s say we want our slide animations to apply only to the main content area of the page. In this case that area is wrapped in an &lt;code&gt;article&lt;/code&gt; element. We can target that to give it a specific &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/view-transition-name&quot;&gt;view transition identifier&lt;/a&gt;:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;article&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;view-transition-name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;article&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If we then change all our previous &lt;code&gt;::view-transition-new(root)&lt;/code&gt; selectors to &lt;code&gt;::view-transition-new(article)&lt;/code&gt; and the same for &lt;code&gt;::view-transition-old(root)&lt;/code&gt; , which becomes &lt;code&gt;::view-transition-old(article)&lt;/code&gt;, we get an entirely different experience:&lt;/p&gt;

&lt;figure&gt;
  &lt;video controls=&quot;&quot; src=&quot;https://res.cloudinary.com/substancelab/video/upload/f_auto,q_auto,w_1024/v1745068281/mentalized/hotwired-turbo-view-transitions/animate-article-element.mp4&quot; title=&quot;Only the content section of the pages are transitioned&quot;&gt;&lt;/video&gt;
&lt;/figure&gt;

&lt;p&gt;(I’ve also changed the animation duration here to more realistic values).&lt;/p&gt;

&lt;h2&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://code.avi.nyc/turbo-view-transitions-in-rails&quot;&gt;Avi Flombaums guide to View Transitions with Turbo&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API&quot;&gt;View Transition API on MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <atom:summary>View transitions between pages has been a reality on the web for quite a while now (at least in the browsers that sup...</atom:summary>
        <pubDate>Sat, 19 Apr 2025 00:00:00 +0000</pubDate>
        <link>https://mentalized.net/journal/2025/04/19/hotwired-turbo-view-transitions/</link>
        <guid isPermaLink="true">https://mentalized.net/journal/2025/04/19/hotwired-turbo-view-transitions/</guid>
      </item>
    
      <item>
        <title>Active Record STI and eager loading</title>
        <description>&lt;p&gt;This week has been … interesting. Not because of obvious geopolitical events, but because a &lt;a href=&quot;https://eventzonen.dk&quot;&gt;client application&lt;/a&gt; started throwing weird errors; background processes failed in ways that weren’t possible and otherwise reliable cronjobs just didn’t do what they were supposed to; no failures or anything, they just didn’t process the records they were meant to.&lt;/p&gt;



&lt;p&gt;This particular application uses &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html&quot;&gt;Single Table Inheritance (STI)&lt;/a&gt; for one of the tables and we introduced a new super class into the hierarchy at the beginning of the week. Everything worked great in development and in testing, but in production things started being off.&lt;/p&gt;

&lt;p&gt;Turns out, STI was to blame - or rather, &lt;a href=&quot;https://substancelab.dk&quot;&gt;us&lt;/a&gt; not reading the manual…&lt;/p&gt;

&lt;h2&gt;Let me tell you a tale…&lt;/h2&gt;

&lt;p&gt;The application has an &lt;code&gt;Event&lt;/code&gt; model, which has a few subclasses; &lt;code&gt;ArtistEvent&lt;/code&gt; inherits from &lt;code&gt;Event&lt;/code&gt;, while &lt;code&gt;BookingEvent&lt;/code&gt; and &lt;code&gt;OfferEvent&lt;/code&gt; inherits from &lt;code&gt;ArtistEvent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We need to send an email every so often for specific &lt;code&gt;BookingEvent&lt;/code&gt;s so we have a cron job set up for that, running a rake task that runs some method in a class. And for some reason, those mails didn’t go out.&lt;/p&gt;

&lt;h3&gt;Blame the code!&lt;/h3&gt;

&lt;p&gt;Looking at the code, everything appears fine. The relevant events are loaded from the database using something similar to &lt;code&gt;ArtistEvent.ready_for_reminders&lt;/code&gt; and each model is then processed.&lt;/p&gt;

&lt;h3&gt;Blame the data!&lt;/h3&gt;

&lt;p&gt;Running the code in a &lt;code&gt;rails console&lt;/code&gt; on the production server made it clear that there were indeed events ready for reminders, yet when the cron job ran it found exacly 0 events.&lt;/p&gt;

&lt;h3&gt;Blame the cron job!&lt;/h3&gt;

&lt;p&gt;The cron job in question uses a rake task to trigger the code, and after a bunch of debugging we concluded that things did not work when running the code via a rake task, but it did work when running the same code via &lt;code&gt;rails runner&lt;/code&gt;. Wut?!&lt;/p&gt;

&lt;h3&gt;Blame rake!&lt;/h3&gt;

&lt;p&gt;One sneaky difference between using &lt;code&gt;rake&lt;/code&gt; (even when loading the rails environment) and using &lt;code&gt;rails runner&lt;/code&gt; is how they load your application: &lt;a href=&quot;https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#eager-loading&quot;&gt;By default, in production environments Rake tasks do not eager load the application&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;Blame eager loading!&lt;/h3&gt;

&lt;p&gt;Armed with this knowledge (which I didn’t know until now, and I have a fair amount of Rails experience, I’d say 👴🏻) we started looking closer at how &lt;a href=&quot;https://rubyonrails.org/&quot;&gt;Rails&lt;/a&gt; eager loads.&lt;/p&gt;

&lt;p&gt;Lo and behold, we could reproduce the problem in development when setting &lt;code&gt;config.eager_load = true&lt;/code&gt;:&lt;/p&gt;

&lt;h3&gt;With &lt;code&gt;eager_load=false&lt;/code&gt;
&lt;/h3&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;ArtistEvent&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;limit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;to_sql&lt;/span&gt;
&lt;span&gt;#=&amp;gt; &quot;SELECT \&quot;events\&quot;.* FROM \&quot;events\&quot; WHERE \&quot;events\&quot;.\&quot;type\&quot; IN (&apos;ArtistEvent&apos;) LIMIT 1&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3&gt;With &lt;code&gt;eager_load=true&lt;/code&gt;
&lt;/h3&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;ArtistEvent&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;limit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;to_sql&lt;/span&gt;
&lt;span&gt;#=&amp;gt; &quot;SELECT \&quot;events\&quot;.* FROM \&quot;events\&quot; WHERE \&quot;events\&quot;.\&quot;type\&quot; IN (&apos;ArtistEvent&apos;, &apos;OfferEvent&apos;, &apos;BookingEvent&apos;) LIMIT 1&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In other words, when &lt;code&gt;eager_load&lt;/code&gt; is set to &lt;code&gt;false&lt;/code&gt;, our query exclusively loads models of type &lt;code&gt;ArtistEvent&lt;/code&gt;, ie none. We want the subclasses of &lt;code&gt;ArtistEvent&lt;/code&gt; to be loaded, which is what happens with &lt;code&gt;eager_load = true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;RTFM&lt;/h2&gt;

&lt;p&gt;Apparently, &lt;a href=&quot;https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#single-table-inheritance&quot;&gt;Single Table Inheritance doesn’t play well with lazy loading classes&lt;/a&gt;: Active Record has to be aware of STI hierarchies to work correctly, but when lazy loading, classes are precisely loaded only on demand.&lt;/p&gt;

&lt;p&gt;This means that when we do &lt;code&gt;ArtistEvent.subclasses&lt;/code&gt; with &lt;code&gt;eager_load = false&lt;/code&gt; the subclasses has yet to be loaded and therefore doesn’t exist yet!&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;ArtistEvent&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;subclasses&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;:name&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;# With eager_load=false&lt;/span&gt;
&lt;span&gt;#=&amp;gt; []&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;“Thankfully” 🙄 this is a known and documented problem and I guess we should have read the manual on &lt;a href=&quot;https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#option-2-preload-a-collapsed-directory&quot;&gt;Autoloading and reloading constants&lt;/a&gt;, but we didn’t and the application has been running just fine for years…&lt;/p&gt;

&lt;p&gt;Luckily a bunch of solutions are also well documented in the above guide, so if you ever run into problems with ActiveRecord STI not including all subclasses in &lt;code&gt;type&lt;/code&gt; queries there is a way forward.&lt;/p&gt;
</description>
        <atom:summary>This week has been … interesting. Not because of obvious geopolitical events, but because a client application starte...</atom:summary>
        <pubDate>Fri, 24 Jan 2025 00:00:00 +0000</pubDate>
        <link>https://mentalized.net/journal/2025/01/24/sti-eager-load/</link>
        <guid isPermaLink="true">https://mentalized.net/journal/2025/01/24/sti-eager-load/</guid>
      </item>
    
      <item>
        <title>How to fix (some) UnknownError and InvalidArgumentError in Capybara</title>
        <description>&lt;p&gt;The other day, &lt;a href=&quot;https://substancelab.dk&quot;&gt;our&lt;/a&gt; headless system specs in a Ruby on Rails project started failing with a bunch of errors we hadn’t seen before. Mostly &lt;code&gt;Selenium::WebDriver::Error::UnknownError: unknown error: failed to close window in 20 seconds&lt;/code&gt; but in some cases also &lt;code&gt;Selenium::WebDriver::Error::InvalidArgumentError: invalid argument: &apos;handle&apos; must be a string&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Usually random errors in browser tests can be fixed by upgrading &lt;a href=&quot;https://rubygems.org/gems/selenium-webdriver&quot;&gt;selenium-webdriver&lt;/a&gt;, &lt;a href=&quot;https://www.google.com/intl/da/chrome/&quot;&gt;Chrome&lt;/a&gt;, &lt;a href=&quot;https://developer.chrome.com/docs/chromedriver&quot;&gt;chromedriver&lt;/a&gt; or any combination thereof, but not this time.&lt;/p&gt;



&lt;h2&gt;TLDR&lt;/h2&gt;

&lt;p&gt;If you’re seeing errors like &lt;code&gt;unknown error: failed to close window in 20 seconds&lt;/code&gt; or &lt;code&gt;invalid argument: &apos;handle&apos; must be a string&lt;/code&gt; in your Selenium test suite running on Chrome you might need to add &lt;code&gt;--disable-search-engine-choice-screen&lt;/code&gt; to the driver options.&lt;/p&gt;

&lt;h2&gt;Root cause&lt;/h2&gt;

&lt;p&gt;The culprit revealed itself when we tried running the test suite with an actually visible, non-headless Chrome:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/substancelab/image/upload/v1723628945/google-chrome-search-engine-ballot.png&quot; alt=&quot;Chromes Search Engine Ballot&quot;&gt;&lt;/p&gt;

&lt;p&gt;Chrome, even when running headless, wants the user to pick their search engine preference. But since there is no user, or no way to see the dialog, it’ll just wait and time out after a while.&lt;/p&gt;

&lt;h2&gt;The solution&lt;/h2&gt;

&lt;p&gt;Luckily there is a commandline option to disable the dialog: &lt;code&gt;--disable-search-engine-choice-screen&lt;/code&gt;. So to fix the issue we can add that to our other Capybara driver options:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;Capybara&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Selenium&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Driver&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;app&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;:browser&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;:chrome&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;:options&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;Selenium&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;WebDriver&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Chrome&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;Options&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
    &lt;span&gt;:args&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;
      &lt;span&gt;&quot;--headless=new&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;&quot;--disable-search-engine-choice-screen&quot;&lt;/span&gt;
    &lt;span&gt;]&lt;/span&gt;
  &lt;span&gt;)&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <atom:summary>The other day, our headless system specs in a Ruby on Rails project started failing with a bunch of errors we hadn’t ...</atom:summary>
        <pubDate>Wed, 14 Aug 2024 00:00:00 +0000</pubDate>
        <link>https://mentalized.net/journal/2024/08/14/chrome-failed-to-close-window-in-20-seconds/</link>
        <guid isPermaLink="true">https://mentalized.net/journal/2024/08/14/chrome-failed-to-close-window-in-20-seconds/</guid>
      </item>
    
      <item>
        <title>SimpleLocalize on Rails</title>
        <description>&lt;p&gt;When your application spans across multiple countries and languages, managing your translations using Rails’ default locale files in git becomes too cumbersome. We recently moved a clients Rails app to &lt;a href=&quot;https://simplelocalize.io/?rid=MMsFDINJt5NS&quot;&gt;SimpleLocalize&lt;/a&gt; to give translators and developers a better workflow around translations. This is how we did it.&lt;/p&gt;



&lt;h2&gt;The setup&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;a href=&quot;https://rubyonrails.org&quot;&gt;Rails&lt;/a&gt; 6 using &lt;a href=&quot;https://guides.rubyonrails.org/i18n.html&quot;&gt;I18n&lt;/a&gt; with the default backend spanning 7 different languages.&lt;/li&gt;
  &lt;li&gt;Deployment to &lt;a href=&quot;https://www.linode.com/lp/refer/?r=10d4761839ce04859fb9d81decae7fb5c7a69818&quot;&gt;Linode&lt;/a&gt; servers via &lt;a href=&quot;https://www.hatchbox.io/&quot;&gt;Hatchbox&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;a href=&quot;https://circleci.com/&quot;&gt;Circle CI&lt;/a&gt; runs tests and prepares deployments.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Install the CLI&lt;/h2&gt;

&lt;p&gt;We’re going to make use of &lt;a href=&quot;https://simplelocalize.io/docs/cli/get-started/&quot;&gt;SimpleLocalizes CLI tool&lt;/a&gt; to move translations around, so we start by installing that.&lt;/p&gt;

&lt;p&gt;I am not a big fan of the &lt;code&gt;curl | bash&lt;/code&gt; process outlined &lt;a href=&quot;https://simplelocalize.io/docs/cli/get-started/&quot;&gt;in their docs&lt;/a&gt;, especially not when it then asks for root access 😬, so I’ve opted for downloading the CLI from &lt;a href=&quot;https://github.com/simplelocalize/simplelocalize-cli/releases&quot;&gt;their releases page&lt;/a&gt;. But you do whatever fits you and your setup here, the important thing is that we get a &lt;code&gt;simplelocalize&lt;/code&gt; command to run.&lt;/p&gt;

&lt;h2&gt;Configure CLI for Rails defaults&lt;/h2&gt;

&lt;p&gt;Having installed the CLI we can run&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;simplelocalize init
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;to generate a default config file in the directory, we’re in. Personally I prefer having config files stored in &lt;code&gt;/config&lt;/code&gt;, so let’s move it elsewhere there and add it to git.&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;mv simplelocalize.yml config/
git add config/simplelocalize.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Do note that this means we have to add a &lt;code&gt;-c&lt;/code&gt; option to all &lt;code&gt;simplelocalize&lt;/code&gt; commands for it to pick up our config file.&lt;/p&gt;

&lt;p&gt;Now feel free to customize your config file as you please. I’d recommend the following options, though, as they match what we’re used to in Rails:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;uploadPath&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;./config/locales/application.{lang}.yml&lt;/span&gt;
&lt;span&gt;uploadFormat&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;yaml&lt;/span&gt;
&lt;span&gt;uploadOptions&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
  &lt;span&gt;-&lt;/span&gt; &lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;MULTI_LANGUAGE&apos;&lt;/span&gt;
  &lt;span&gt;-&lt;/span&gt; &lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;REPLACE_TRANSLATION_IF_FOUND&apos;&lt;/span&gt;

&lt;span&gt;downloadPath&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;./config/locales/application.{lang}.yml&lt;/span&gt;
&lt;span&gt;downloadFormat&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;yaml&lt;/span&gt;
&lt;span&gt;downloadOptions&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
  &lt;span&gt;-&lt;/span&gt; &lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;MULTI_LANGUAGE&apos;&lt;/span&gt;
  &lt;span&gt;-&lt;/span&gt; &lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;WRITE_NESTED&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Most of the CLI commands need to be authorized with &lt;code&gt;--apiKey&lt;/code&gt;. The API key can be found in the ‘Integrations &amp;gt; Project credentials &amp;gt; API Key’.&lt;/p&gt;

&lt;p&gt;You can keep your API key in the config file or you can provide it on the CLI whenever you call the CLI, that’s up to you. Keeping secret keys out of git is a good practice, though. Ultimately, storing the API key in an environment variable is probably the best approach.&lt;/p&gt;

&lt;h2&gt;Import your existing translations&lt;/h2&gt;

&lt;p&gt;After signing up for &lt;a href=&quot;https://simplelocalize.io/?rid=MMsFDINJt5NS&quot;&gt;SimpleLocalize&lt;/a&gt; and creating a project for your application, you want to seed the project with all your existing translations. Head to the “Data” tab in SimpleLocalize and choose YAML file under Import translations.&lt;/p&gt;

&lt;p&gt;To configure the import for the format used by I18n’s YAML files, use the following settings:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Multi-language file: Even though each file only contains a single language, the file is formatted like a multi-language file with the locale name as a root-level key.&lt;/li&gt;
  &lt;li&gt;Overwrite translations: Not strictly necessary, but if you end up importing more than once, it does come in handy.&lt;/li&gt;
  &lt;li&gt;Leave Namespace empty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, upload each of your language files one by one (sidenote, being able to choose and upload multiple files at once here would be great).&lt;/p&gt;

&lt;p&gt;Alternatively, you can use the CLI to perform the above process, assuming you’ve configured everything as described:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;simplelocalize &lt;span&gt;-c&lt;/span&gt; config/simplelocalize.yml upload &lt;span&gt;--apiKey&lt;/span&gt; &lt;span&gt;$API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;Download translations to development&lt;/h2&gt;

&lt;p&gt;Now that SimpleLocalize is the ultimate source of truth for translations, we should remove them from our repository. You &lt;em&gt;could&lt;/em&gt; still have them in git, and it would ease some things (like deployment). We have found, though, that the overall cost in terms of merge conflicts and confusion as to what translations are the most recent makes that solution untenable.&lt;/p&gt;

&lt;p&gt;So…&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;git &lt;span&gt;rm &lt;/span&gt;config/locales/&lt;span&gt;*&lt;/span&gt;
&lt;span&gt;touch &lt;/span&gt;config/locales/.gitkeep
&lt;span&gt;echo &lt;/span&gt;config/locales &lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
git add config/locales/.gitkeep
git commit &lt;span&gt;-m&lt;/span&gt; &lt;span&gt;&quot;Remove translation files&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, your up-to-date translations is no more than a&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;simplelocalize &lt;span&gt;-c&lt;/span&gt; config/simplelocalize.yml download &lt;span&gt;--apiKey&lt;/span&gt; &lt;span&gt;$API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;away.&lt;/p&gt;

&lt;h2&gt;Download translations during deployment&lt;/h2&gt;

&lt;p&gt;Now that we have removed our translations from the project, they’ll no longer be included when we run a deployment. Instead we’ll have to download the most recent translations from SimpleLocalize during deployment.&lt;/p&gt;

&lt;p&gt;This application uses &lt;a href=&quot;https://www.hatchbox.io/&quot;&gt;Hatchbox&lt;/a&gt; for deployments, so the following applies directly to that service. However, you should be able to replicate this using whatever hosting provider you use.&lt;/p&gt;

&lt;p&gt;During the build phase (we use a Custom build script) we can download the translations from SimpleLocalize using the CLI - assuming it’s installed:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;simplelocalize &lt;span&gt;-c&lt;/span&gt; config/simplelocalize.yml download &lt;span&gt;--apiKey&lt;/span&gt; &lt;span&gt;$API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is just like we do in development and you might consider wrapping this in a script that you can run whenever/wherever needed (we’ve got a &lt;code&gt;script/translations/pull&lt;/code&gt;) that does exactly this.&lt;/p&gt;

&lt;p&gt;During the pre-build phase we’ll make sure the CLI is installed on the build server. Your milage may vary, but we found that downloading a specific release&lt;/p&gt;

&lt;p&gt;How you install the CLI on your build server is up to you, but you could do it via a script like the following:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;wget &lt;span&gt;--no-verbose&lt;/span&gt; https://github.com/simplelocalize/simplelocalize-cli/releases/download/2.6.0/simplelocalize-cli-linux
&lt;span&gt;mv &lt;/span&gt;simplelocalize-cli-linux ~/bin/simplelocalize
&lt;span&gt;chmod &lt;/span&gt;a+x ~/bin/simplelocalize
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2&gt;Download translations on CI&lt;/h2&gt;

&lt;p&gt;We use &lt;a href=&quot;https://circleci.com/&quot;&gt;Circle&lt;/a&gt; as our CI server and we need the translations downloaded there as well in order to run tests and build the project assets.&lt;/p&gt;

&lt;p&gt;Unfortunately we ran into issues with the most recent versions of the CLI. Because Circle is running older versions of Ubuntu we can’t run the newer CLI releases:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;simplelocalize: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33&apos; not found (required by simplelocalize)
simplelocalize: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34&apos; not found (required by simplelocalize)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We ended up just installing a legacy version of the CLI (2.1.1), which works, and subsequently using it to download translations:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;curl &lt;span&gt;-s&lt;/span&gt; https://get.simplelocalize.io/2.1.1/install | bash
simplelocalize &lt;span&gt;-c&lt;/span&gt; config/simplelocalize.yml download &lt;span&gt;--apiKey&lt;/span&gt; &lt;span&gt;$API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(on CI I am not too worried about the &lt;code&gt;curl | bash&lt;/code&gt; pattern - should I be?)&lt;/p&gt;

&lt;h2&gt;Closing notes&lt;/h2&gt;

&lt;p&gt;Now all that is left is inviting your developers and translators to SimpleLocalize and start localizing.&lt;/p&gt;
</description>
        <atom:summary>When your application spans across multiple countries and languages, managing your translations using Rails’ default ...</atom:summary>
        <pubDate>Wed, 24 Apr 2024 00:00:00 +0000</pubDate>
        <link>https://mentalized.net/journal/2024/04/24/simplelocalize-on-rails/</link>
        <guid isPermaLink="true">https://mentalized.net/journal/2024/04/24/simplelocalize-on-rails/</guid>
      </item>
    
  </channel>
</rss>
