<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: NejŘemeslníci</title>
    <description>The latest articles on DEV Community by NejŘemeslníci (@nejremeslnici).</description>
    <link>https://dev.to/nejremeslnici</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1044%2Fa752ea38-0725-43f0-8796-fe27fe8120e3.png</url>
      <title>DEV Community: NejŘemeslníci</title>
      <link>https://dev.to/nejremeslnici</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nejremeslnici"/>
    <language>en</language>
    <item>
      <title>How to detect classes contained in ruby gems in Tailwind 4</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Wed, 05 Mar 2025 21:40:06 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/how-to-detect-classes-contained-in-ruby-gems-in-tailwind-4-14n6</link>
      <guid>https://dev.to/nejremeslnici/how-to-detect-classes-contained-in-ruby-gems-in-tailwind-4-14n6</guid>
      <description>&lt;p&gt;Our main web app uses Tailwind CSS and we are happy to have recently  migrated it to &lt;a href="https://tailwindcss.com/docs/upgrade-guide" rel="noopener noreferrer"&gt;Tailwind version 4&lt;/a&gt;. Among the nice features that Tailwind 4 brings to the table is an automatic &lt;a href="https://tailwindcss.com/docs/detecting-classes-in-source-files" rel="noopener noreferrer"&gt;detection of CSS classes&lt;/a&gt; in the application source files.   This allowed us to completely remove the &lt;a href="https://v3.tailwindcss.com/docs/content-configuration" rel="noopener noreferrer"&gt;&lt;code&gt;content&lt;/code&gt; section&lt;/a&gt; from the former Tailwind v3 configuration and just forget about it.&lt;/p&gt;

&lt;p&gt;For a moment only, that is.&lt;/p&gt;

&lt;p&gt;As it quickly turned out, we forgot about the fact that not all CSS classes are used within our application itself. Our app includes an internal gem that implements several &lt;a href="https://flowbite.com/" rel="noopener noreferrer"&gt;Flowbite components&lt;/a&gt; and some of the Tailwind classes it contains are unique to that gem. As the gem sources reside outside the app root directory, the Tailwind program cannot see them and doesn’t scan them for potential classes.&lt;/p&gt;

&lt;h3&gt;
  
  
  How we did it in Tailwind 3
&lt;/h3&gt;

&lt;p&gt;In Tailwind v3 we actually had the following code in the &lt;code&gt;tailwind.config.js&lt;/code&gt; configuration file, handling this issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tailwind.config.js&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getGemPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`bundle show &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gem&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;  
    &lt;span class="p"&gt;...,&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getGemPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flowbite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/app/components/**/*.{slim,rb}`&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The config defined a function which called the &lt;code&gt;bundle show&lt;/code&gt; command to get the absolute path to the gem source code folder. It then combined it with glob patterns to the actual source code files. This solution was probably inspired from &lt;a href="https://stackoverflow.com/a/74737193/1544012" rel="noopener noreferrer"&gt;here&lt;/a&gt; and just worked for us for several years.&lt;/p&gt;

&lt;h3&gt;
  
  
  But how to do it in Tailwind 4?
&lt;/h3&gt;

&lt;p&gt;In Tailwind 4, things have changed dramatically. Using a JS configuration is &lt;a href="https://tailwindcss.com/docs/functions-and-directives#compatibility" rel="noopener noreferrer"&gt;still possible&lt;/a&gt; but not really recommended and we did not want to keep that tech debt in our project. &lt;/p&gt;

&lt;p&gt;The official way now is to configure Tailwind via a CSS file. In a CSS file though, there is no way to run code dynamically, everything has to be pre-defined and static. So how can we reference gem source files when each environment that our app runs on stores them under a different path?&lt;/p&gt;

&lt;p&gt;We came up with a solution that we are happy with using two small tricks: 1) we made the gem path static and 2) we made it accessible relative to the main app.&lt;/p&gt;

&lt;p&gt;As it turns out, &lt;a href="https://bundler.io/" rel="noopener noreferrer"&gt;Bundler&lt;/a&gt;, has the concept of &lt;a href="https://bundler.io/guides/plugins.html" rel="noopener noreferrer"&gt;plugins&lt;/a&gt; and one of them does precisely what we need: the &lt;a href="https://github.com/petekinnecom/bundler-symlink" rel="noopener noreferrer"&gt;&lt;code&gt;bundler-symlink&lt;/code&gt; plugin&lt;/a&gt; provides a post-install hook that adds &lt;a href="https://en.wikipedia.org/wiki/Symbolic_link" rel="noopener noreferrer"&gt;symlinks&lt;/a&gt; to all gems of the main application under its local directory. Specifically, under the &lt;code&gt;.bundle/gems/&lt;/code&gt; directory of your project. This is great because all the different absolute gem paths are now accessible from a known place within the main application.&lt;/p&gt;

&lt;p&gt;To ensure the gem is available in your setup, you can add it to the &lt;code&gt;Gemfile&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;

&lt;span class="n"&gt;plugin&lt;/span&gt; &lt;span class="s2"&gt;"bundler-symlink"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, whenever &lt;code&gt;bundle install&lt;/code&gt; is run, the plugin adds / updates a set of symlinks to the gems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bundle &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="go"&gt;
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Fetching bundler-symlink 0.4.0
Installing bundler-symlink 0.4.0
Installed plugin bundler-symlink
Symlinking bundled gems into /home/matous/projekty/nejremeslnici/web/.bundle/gems
Bundle complete! ...


&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; .bundle/gems
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 51 Mar  5 21:32 actioncable-8.0.1 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actioncable-8.0.1/
&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 53 Mar  5 21:32 actionmailbox-8.0.1 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actionmailbox-8.0.1/
&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 52 Mar  5 21:32 actionmailer-8.0.1 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actionmailer-8.0.1/
&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 50 Mar  5 21:32 actionpack-8.0.1 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actionpack-8.0.1/
&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 50 Mar  5 21:32 actiontext-8.0.1 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actiontext-8.0.1/
&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 50 Mar  5 21:32 actionview-8.0.1 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actionview-8.0.1/
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you can easily add a &lt;em&gt;relative&lt;/em&gt; gem path to the Tailwind CSS configuration file using the &lt;a href="https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-registering-sources" rel="noopener noreferrer"&gt;&lt;code&gt;@source&lt;/code&gt; directive&lt;/a&gt; (the path is relative to the location of the CSS file):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* app/assets/tailwind/application.css */&lt;/span&gt;

&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@source&lt;/span&gt; &lt;span class="s1"&gt;"../../../.bundle/gems/flowbite-components-517d2087e439/app/components/**/*.{slim,rb}"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And voila, all styles work again!&lt;/p&gt;

&lt;p&gt;There is still one issue though: the bundler plugin creates symlinks that have the gem versions in their names so this setup would break again whenever we upgraded our Flowbite components gem. This does not seem like a viable solution, long-term.&lt;/p&gt;

&lt;p&gt;We briefly tried to use a glob in the gem path in the Tailwind configuration (as in &lt;code&gt;@source ".../.bundle/gems/flowbite-*/..."&lt;/code&gt;) but this did not work, probably due to a &lt;a href="https://github.com/tailwindlabs/tailwindcss/issues/16765" rel="noopener noreferrer"&gt;limitation&lt;/a&gt; of the Tailwind scanner regarding symlinks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the ”bare symlinks“ plugin
&lt;/h3&gt;

&lt;p&gt;We ended up cloning the bundler plugin and amending its source to create symlinks that were &lt;em&gt;not&lt;/em&gt; versioned instead. We released the new plugin under the &lt;a href="https://github.com/NejRemeslnici/bundler-bare-symlink" rel="noopener noreferrer"&gt;&lt;code&gt;bundler-bare_symlink&lt;/code&gt;&lt;/a&gt; name. It is exactly the same as the original &lt;sup id="fnref1"&gt;1&lt;/sup&gt; except for one thing: it uses the &lt;a href="https://github.com/NejRemeslnici/bundler-bare-symlink/blob/93d9a4affbe66aa968896695a2c0522c706edcb9/lib/bundler/bare_symlink.rb#L23C47-L23C56" rel="noopener noreferrer"&gt;bare gem name&lt;/a&gt; instead of the (versioned) gem &lt;a href="https://github.com/petekinnecom/bundler-symlink/blob/351feef8681f0bc06b73449a2f38016b09bfde38/lib/bundler/symlink.rb#L28" rel="noopener noreferrer"&gt;directory name&lt;/a&gt; in the symlink name.&lt;/p&gt;

&lt;p&gt;So, to use it, we first uninstalled the original plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bundle plugin uninstall bundler-symlink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then put our new one in the &lt;code&gt;Gemfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;

&lt;span class="n"&gt;plugin&lt;/span&gt; &lt;span class="s2"&gt;"bundler-bare_symlink"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And after running &lt;code&gt;bundle install&lt;/code&gt; again, we got the following symlink structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; .bundle/gems
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 51 Mar  5 22:09 actioncable -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actioncable-8.0.1/
&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 53 Mar  5 22:09 actionmailbox -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actionmailbox-8.0.1/
&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 52 Mar  5 22:09 actionmailer -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actionmailer-8.0.1/
&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 50 Mar  5 22:09 actionpack -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actionpack-8.0.1/
&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 50 Mar  5 22:09 actiontext -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actiontext-8.0.1/
&lt;span class="gp"&gt;lrwxrwxrwx 1 matous users 50 Mar  5 22:09 actionview -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/matous/.gem/ruby/3.4.1/gems/actionview-8.0.1/
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, we were able to use a fully relative &lt;em&gt;and&lt;/em&gt; static path in the Tailwind configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* app/assets/tailwind/application.css */&lt;/span&gt;

&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@source&lt;/span&gt; &lt;span class="s1"&gt;"../../../.bundle/gems/flowbite/app/components/**/*.{slim,rb}"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;This setup works for us on all developer machines as well as on the servers. The fact that the plugin uses a &lt;code&gt;bundle install&lt;/code&gt; hook makes it very convenient and we did not have to explicitly install or configure it anywhere. And, most importantly, all of our web styles work again!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Would you like to read more stuff like this? Follow us on &lt;a href="https://bsky.app/profile/bora.ma" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;It even keeps the original license, the &lt;a href="http://www.wtfpl.net/txt/copying/" rel="noopener noreferrer"&gt;WTFPL&lt;/a&gt;, of course, haha. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>rails</category>
      <category>tailwindcss</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Speed up Kamal deploys in GitHub Actions</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Wed, 13 Nov 2024 12:09:29 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/speed-up-kamal-deploys-in-github-actions-oh0</link>
      <guid>https://dev.to/nejremeslnici/speed-up-kamal-deploys-in-github-actions-oh0</guid>
      <description>&lt;p&gt;Have you got your application successfully deployed using &lt;a href="https://kamal-deploy.org/" rel="noopener noreferrer"&gt;Kamal&lt;/a&gt; and &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; but the deploy time still seems a bit too much? Depending on what changes you are trying to push to the server, ”a few minute“ deploys should be perfectly possible with GitHub Actions, even shorter ones:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fllql179dobvw19sf2f7t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fllql179dobvw19sf2f7t.png" alt="A sub-minute deploy with GitHub Actions to on of our servers" width="800" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s take a look at some ways you can tweak your workflow to speed things up. This post was inspired by the nice &lt;a href="https://bsky.app/profile/adrienpoly.com/post/3laitu6i4jc22" rel="noopener noreferrer"&gt;conversations&lt;/a&gt; &lt;a href="https://bsky.app/profile/adrienpoly.com/post/3lajcv2bh5k2k" rel="noopener noreferrer"&gt;with Adrien&lt;/a&gt; on BlueSky and I also wanted to share our experience with optimizing our own GitHub Actions deploy flow that we went through recently. Part of this post will be Ruby on Rails–specific, the rest should be rather universal. &lt;/p&gt;

&lt;p&gt;Deploying with Kamal involves building a Docker image, a process with its own set of intricate rules. There are many &lt;a href="https://docs.docker.com/build/building/best-practices/" rel="noopener noreferrer"&gt;best practices&lt;/a&gt; around optimizing the Dockerfile itself and/or the building process but as they are not Kamal nor GitHub Actions–specific, we won’t cover them here. Besides, the default Ruby on Rails Dockerfile is already pretty optimized. So what &lt;em&gt;other&lt;/em&gt; options do we have?&lt;/p&gt;

&lt;h2&gt;
  
  
  Cash everything you can
&lt;/h2&gt;

&lt;p&gt;While I never recommend employing caching as the first step of optimizing things, Docker is an exception. The Docker image format is well suited for caching, the cached layers are immutable and their &lt;a href="https://docs.docker.com/build/cache/invalidation/" rel="noopener noreferrer"&gt;invalidation strategy&lt;/a&gt; seems simple and robust. Therefore, we really want to cache the builds right after we get our deployment workflow working.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching Docker build images
&lt;/h3&gt;

&lt;p&gt;To cache build image layers, we need to tweak two things, one of them is the Kamal config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/deploy.yml&lt;/span&gt;
&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gha&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mode=max&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kamal-app-build-cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since GitHub offers a &lt;a href="https://github.com/actions/cache" rel="noopener noreferrer"&gt;cache storage back-end&lt;/a&gt; &lt;a href="https://docs.docker.com/build/cache/backends/gha/" rel="noopener noreferrer"&gt;supported by Docker&lt;/a&gt;, we use the &lt;code&gt;gha&lt;/code&gt; cache type so that the cache storage is as close to our runners as possible. The &lt;code&gt;mode=max&lt;/code&gt; option &lt;a href="https://docs.docker.com/build/cache/backends/#cache-mode" rel="noopener noreferrer"&gt;instructs&lt;/a&gt; Docker to cache even the intermediate build layers, not only those exported to the final image. And we also give our build image some (arbitrary) name.&lt;/p&gt;

&lt;p&gt;But in the context of GitHub Actions, this is not yet sufficient for caching to work. If you looked at the Actions ⟶ Caches tab in your GitHub repository, it would still be empty. How come?&lt;/p&gt;

&lt;p&gt;By default, Kamal &lt;a href="https://kamal-deploy.org/docs/configuration/builders/#driver" rel="noopener noreferrer"&gt;uses&lt;/a&gt; the &lt;a href="https://docs.docker.com/build/builders/drivers/docker-container/" rel="noopener noreferrer"&gt;&lt;code&gt;docker-container&lt;/code&gt; driver&lt;/a&gt; to build images which, in turn, uses the &lt;a href="https://github.com/moby/buildkit" rel="noopener noreferrer"&gt;BuildKit toolkit&lt;/a&gt; internally. While Kamal sets up registry caching correctly, caching still fails in the end because the BuildKit process is isolated from our GitHub Action runtime process. To connect the two, we need to expose the GitHub runtime to the workflow. Luckily, there is a &lt;a href="https://github.com/crazy-max/ghaction-github-runtime" rel="noopener noreferrer"&gt;GitHub Action ready&lt;/a&gt; just for this so all that is needed is adding the action to the workflow file. We put it right after setting up Docker Buildx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflow/deploy.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s"&gt;...&lt;/span&gt;
      &lt;span class="s"&gt;- name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
        &lt;span class="s"&gt;uses&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;

      &lt;span class="s"&gt;- name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Expose GitHub Runtime for cache&lt;/span&gt;
        &lt;span class="s"&gt;uses&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crazy-max/ghaction-github-runtime@v3&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run the build action again, you should start seeing "buildkit" entries in the Actions tab ⟶ Caches in your GitHub repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8a6j1mao3d3ar8w8yqdu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8a6j1mao3d3ar8w8yqdu.png" alt="Cached Actions entries in a GitHub repository" width="800" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More importantly, your builds should now be running &lt;strong&gt;about twice as fast!&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching application dependencies
&lt;/h3&gt;

&lt;p&gt;There is one more thing that we can cache – application dependencies, i.e. the libraries it uses, etc. GitHub Actions &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows" rel="noopener noreferrer"&gt;support caching the assets&lt;/a&gt; of various package managers. For ruby, caching the bundled gems is &lt;a href="https://github.com/ruby/setup-ruby#caching-bundle-install-automatically" rel="noopener noreferrer"&gt;configured&lt;/a&gt; in the workflow file with a single &lt;code&gt;bundler-cache: true&lt;/code&gt; option like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflow/deploy.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s"&gt;...&lt;/span&gt;
      &lt;span class="s"&gt;- name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Ruby&lt;/span&gt;
        &lt;span class="s"&gt;uses&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
        &lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Respect the build architecture
&lt;/h2&gt;

&lt;p&gt;While Docker allows cross–platform builds quite easily, they tend to be &lt;em&gt;much&lt;/em&gt; slower than when building an image on the same platform as is the target server. Especially those builds that involve a lot of computational work, such as when building ruby gems with native extensions.&lt;/p&gt;

&lt;p&gt;Most of the &lt;a href="https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories" rel="noopener noreferrer"&gt;standard GitHub Action runners&lt;/a&gt; are based on the x64 architecture so if your target server is ARM-based, your options are currently quite limited. You can: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;switch to one of the &lt;a href="https://docs.github.com/en/actions/using-github-hosted-runners/using-larger-runners/about-larger-runners#specifications-for-general-larger-runners" rel="noopener noreferrer"&gt;larger Linux arm64–based runners&lt;/a&gt; but these are &lt;a href="https://docs.github.com/en/actions/using-github-hosted-runners/using-larger-runners/about-larger-runners#understanding-billing" rel="noopener noreferrer"&gt;billed separately&lt;/a&gt; and are not free even for public repositories,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#architectures" rel="noopener noreferrer"&gt;self-host&lt;/a&gt; a GitHub Action runner on a server of your choice,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;migrate your application to an x64–architecture server,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;switch to a completely different CI service which provides ARM-based builds (out of scope for this post),&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;or, of course, you can bite the bullet and reconcile to waiting for cross–platform builds.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since 2024, there actually &lt;em&gt;are&lt;/em&gt; some standard ARM–based runners &lt;a href="https://github.blog/changelog/2024-01-30-github-actions-macos-14-sonoma-is-now-available/" rel="noopener noreferrer"&gt;available&lt;/a&gt; in GitHub Actions, namely &lt;code&gt;macos-14&lt;/code&gt;, &lt;code&gt;macos-15&lt;/code&gt;, and &lt;code&gt;macos-latest&lt;/code&gt; that run on the Apple silicon (M1) chip. Unfortunately, this setup &lt;a href="https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#limitations-for-arm64-macos-runners" rel="noopener noreferrer"&gt;has its own limitations and quirks&lt;/a&gt;, most importantly, the M1 chip &lt;a href="https://github.com/abiosoft/colima/issues/970#issuecomment-1917751242" rel="noopener noreferrer"&gt;does not support nested virtualization&lt;/a&gt;. Since the GitHub Action runner itself is virtualized and Docker needs that too, there is effectively &lt;a href="https://github.com/douglascamata/setup-docker-macos-action" rel="noopener noreferrer"&gt;no way to run&lt;/a&gt; Docker on M1–chip runners. Until GitHub releases hosted runners based on the newest M3 Apple silicon chips (which allegedly &lt;a href="https://developer.apple.com/documentation/virtualization/vzgenericplatformconfiguration/4360553-isnestedvirtualizationsupported#discussion" rel="noopener noreferrer"&gt;do support&lt;/a&gt; nested virtualization), there won’t be a feasible way to deploy to ARM-based servers natively using MacOS runners. In other words, until then, standard GitHub Action runners are best suited for deploying to x64 architecture servers only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don’t build multi-platform images unless you really need them
&lt;/h2&gt;

&lt;p&gt;Unless you have multiple target servers that are mixed in their architectures, do not build multi-platform images. Building images for multiple architectures is inherently slow and there isn’t much you can do about it.&lt;/p&gt;

&lt;p&gt;So, be sure you have a single &lt;code&gt;arch&lt;/code&gt; specified in the &lt;code&gt;builder&lt;/code&gt; section of Kamal config. For reasons mentioned above, preferably &lt;code&gt;amd64&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/deploy.yml&lt;/span&gt;
&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Parallelize building native gems (misc tweak)
&lt;/h2&gt;

&lt;p&gt;We have also tested whether Bundler is able to determine the number of processors in a GitHub Action run so that it can parallelize gem downloads and builds (see its &lt;a href="https://bundler.io/v2.4/man/bundle-install.1.html" rel="noopener noreferrer"&gt;&lt;code&gt;--jobs&lt;/code&gt; option&lt;/a&gt;) and yes, it works great without having to configure anything.&lt;/p&gt;

&lt;p&gt;This is not the case though when Bundler builds gems with native extensions. Usually, it calls &lt;code&gt;make&lt;/code&gt; to compile the C extensions but these calls are not parallelized out of the box. So if you build a lot of gems with native extensions, you might want to &lt;a href="https://github.com/rubygems/rubygems/issues/5276#issuecomment-1087400919" rel="noopener noreferrer"&gt;define&lt;/a&gt; an &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables" rel="noopener noreferrer"&gt;environment variable&lt;/a&gt; that instructs &lt;code&gt;make&lt;/code&gt; to run multiple compile jobs in parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAKE="make -j4"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;4&lt;/code&gt; is the number of processors available in your runner or even slightly more, you can experiment with that. &lt;/p&gt;

&lt;h2&gt;
  
  
  Watch your runners performance
&lt;/h2&gt;

&lt;p&gt;There is a new feature that has been &lt;a href="https://github.blog/changelog/2024-10-31-actions-performance-metrics-in-public-preview/" rel="noopener noreferrer"&gt;released&lt;/a&gt; by GitHub just a few days ago: a place to monitor the performance statistics of your GitHub Actions. To view them, go to Insights ⟶ Actions Performance Metrics in your repository and you will see something like the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnt7ih1yag00i2ardbxny.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnt7ih1yag00i2ardbxny.png" alt="GitHub Actions Performance stats" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just keep on mind that the numbers here are average numbers. In the case of deployment workflows the run time usually differs quite a lot based on whether gems need to be re-bundled, assets recompiled or not.&lt;/p&gt;

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

&lt;p&gt;GitHub Actions are a specific CI/CD environment and it is quite easy to unknowingly create a Kamal deployment workflow that will under-perform in it. We shared a few tips that could help leveling up the runners speed so that we don’t have to wait for our deploys for longer than necessary.&lt;/p&gt;

&lt;p&gt;Want more stuff like this? Follow us here or at &lt;a href="https://bsky.app/profile/bora.ma" rel="noopener noreferrer"&gt;BlueSky&lt;/a&gt; 🦋.&lt;/p&gt;

</description>
      <category>kamal</category>
      <category>githubactions</category>
      <category>performance</category>
      <category>rails</category>
    </item>
    <item>
      <title>Strict locals in Slim / Haml partials in Rails</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Thu, 22 Feb 2024 17:13:09 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/strict-locals-in-slim-haml-partials-in-rails-2f73</link>
      <guid>https://dev.to/nejremeslnici/strict-locals-in-slim-haml-partials-in-rails-2f73</guid>
      <description>&lt;p&gt;When I saw the announcement about &lt;a href="https://guides.rubyonrails.org/7_1_release_notes.html#allow-templates-to-set-strict-locals" rel="noopener noreferrer"&gt;partial template strict locals&lt;/a&gt; in Rails 7.1, I was excited but it took me a long time to realize that this is not an ERB-only feature but that it should actually work in any template language, including my favorite – &lt;a href="https://slim-template.github.io/" rel="noopener noreferrer"&gt;Slim&lt;/a&gt;! I even remember trying it back then but failing, probably due to an incorrect syntax. While there are &lt;a href="https://masilotti.com/safer-rails-partials-with-strict-locals/" rel="noopener noreferrer"&gt;many&lt;/a&gt; &lt;a href="https://blog.kiprosh.com/allow-template-to-set-strict-locals/" rel="noopener noreferrer"&gt;nice&lt;/a&gt; &lt;a href="https://www.driftingruby.com/episodes/strict-locals" rel="noopener noreferrer"&gt;tutorials&lt;/a&gt; explaining template strict locals out there, they are all ERB-centric so let’s take a look how this feature can be used in Slim or Haml templates, respectively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preparations
&lt;/h3&gt;

&lt;p&gt;Suppose we want to render a partial template from a main one one, to show an important message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="c"&gt;/ main.html.slim&lt;/span&gt;

&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"notice"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="c"&gt;/ _notice.html.slim&lt;/span&gt;

&lt;span class="nt"&gt;h1&lt;/span&gt;
  &lt;span class="p"&gt;'&lt;/span&gt; Note:
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we run this code, it will fail with an ugly and misleading error message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;undefined local variable or method &lt;code&gt;message&lt;/code&gt; for an instance of &lt;code&gt;#&amp;lt;...&amp;gt;&lt;/code&gt;&lt;br&gt;
Hint: &lt;code&gt;message&lt;/code&gt; is probably misspelled.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;saying that we probably misspelled &lt;code&gt;message&lt;/code&gt; which we did not - we forgot to pass the local variable in the first place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a magic comment
&lt;/h3&gt;

&lt;p&gt;Let’s make the partial template recognize our local variable using a &lt;strong&gt;strict locals magic comment&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="c"&gt;/ _notice.html.slim&lt;/span&gt;

&lt;span class="c"&gt;/# locals: (message:)&lt;/span&gt;

&lt;span class="nt"&gt;h1&lt;/span&gt;
  &lt;span class="p"&gt;'&lt;/span&gt; Note:
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the exact syntax is important here: it must be a &lt;a href="https://rubydoc.info/gems/slim/frames#code-comment" rel="noopener noreferrer"&gt;comment&lt;/a&gt; (denoted by the slash &lt;code&gt;/&lt;/code&gt; character in Slim), then a hash (&lt;code&gt;#&lt;/code&gt;), a space, then &lt;code&gt;locals:&lt;/code&gt;, another space and finally something that resembles a method arguments definition with keyword arguments specifying the needed local variables. The reason this syntax is so strict is that it gets matched by a &lt;a href="https://github.com/rails/rails/blob/9f1dec2ea5155205a880f6e6e232cf9ea6da2d8c/actionview/lib/action_view/template.rb#L11" rel="noopener noreferrer"&gt;regular expression&lt;/a&gt; deep in the Action View templates processing code.&lt;/p&gt;

&lt;p&gt;In case of a &lt;a href="https://haml.info/" rel="noopener noreferrer"&gt;HAML template&lt;/a&gt;, the syntax differs only by the magic comment leading character, it must be &lt;code&gt;-#&lt;/code&gt; instead of &lt;code&gt;/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haml"&gt;&lt;code&gt;&lt;span class="c"&gt;/ _notice.html.haml
&lt;/span&gt;
&lt;span class="c"&gt;-# locals: (message:)
&lt;/span&gt;
&lt;span class="nt"&gt;%h1&lt;/span&gt; Note!
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that both magic comments are code comments, i.e. they are &lt;em&gt;not&lt;/em&gt; output to the final HTML.&lt;/p&gt;

&lt;p&gt;Once we have this magic comment in place, we get a much better error:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;missing local: :message&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and the stack trace leads us to the proper place (to the line with &lt;code&gt;render&lt;/code&gt; in the main template) right away.&lt;/p&gt;

&lt;p&gt;And of course, all other &lt;a href="https://guides.rubyonrails.org/7_1_release_notes.html#allow-templates-to-set-strict-locals" rel="noopener noreferrer"&gt;strict locals goodies&lt;/a&gt; work, too, such as the default values or prohibiting any locals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing thoughts
&lt;/h3&gt;

&lt;p&gt;Up till now, we commonly used the &lt;a href="https://guides.rubyonrails.org/action_view_overview.html#using-local-assigns" rel="noopener noreferrer"&gt;&lt;code&gt;local_assigns&lt;/code&gt; hash&lt;/a&gt; to notify the reader that our variable is to be passed from the outside and to set its default value. We think that strict locals make this hash a little obsolete as they serve very similar purposes with a nicer syntax.&lt;/p&gt;

&lt;p&gt;Although we still &lt;a href="https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o"&gt;prefer&lt;/a&gt; View Components for our most sophisticated view template blocks, we will definitely use Rails partials with more pleasure since we can be sure now to write them in a safer way. 👍🏻&lt;/p&gt;

</description>
      <category>rails</category>
      <category>slim</category>
      <category>erb</category>
    </item>
    <item>
      <title>How to use View Transitions in Hotwire Turbo</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Thu, 16 Feb 2023 09:55:37 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/how-to-use-view-transitions-in-hotwire-turbo-1kdi</link>
      <guid>https://dev.to/nejremeslnici/how-to-use-view-transitions-in-hotwire-turbo-1kdi</guid>
      <description>&lt;p&gt;Have you heard the news? &lt;strong&gt;Google Chrome 111&lt;/strong&gt; will be released on March 1st 2023 and – among other things – &lt;a href="https://chromestatus.com/feature/5193009714954240" rel="noopener noreferrer"&gt;will ship&lt;/a&gt; a feature I was eager to try since I first heard about it: &lt;strong&gt;View Transitions&lt;/strong&gt;. This is a new API proposed by Google at W3C, currently as a &lt;a href="https://drafts.csswg.org/css-view-transitions-1/" rel="noopener noreferrer"&gt;1st Working Draft&lt;/a&gt;. An informal summary for it can be found in &lt;a href="https://github.com/WICG/view-transitions/blob/main/explainer.md" rel="noopener noreferrer"&gt;this Explainer&lt;/a&gt; which seems to be a more up-to-date version of the original &lt;a href="https://developer.chrome.com/docs/web-platform/view-transitions/#changing-on-navigation-type" rel="noopener noreferrer"&gt;introductory article&lt;/a&gt; published at the Chrome Developers site last year. I highly recommend reading either of the documents. &lt;strong&gt;Update:&lt;/strong&gt; there is now some &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API" rel="noopener noreferrer"&gt;MDN documentation&lt;/a&gt; available, too!&lt;/p&gt;

&lt;p&gt;So what are View Transitions good for? In short, they allow adding &lt;strong&gt;animated page transitions&lt;/strong&gt;. Although we already have several standard options to animate stuff on web pages (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions" rel="noopener noreferrer"&gt;CSS Transitions&lt;/a&gt;, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations" rel="noopener noreferrer"&gt;CSS Animations&lt;/a&gt; or the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API" rel="noopener noreferrer"&gt;Web Animations API&lt;/a&gt;) and countless more options in particular JavaScript frameworks and libraries (&lt;a href="https://www.framer.com/motion/" rel="noopener noreferrer"&gt;Framer Motion&lt;/a&gt; for React, &lt;a href="https://vuejs.org/guide/built-ins/transition.html" rel="noopener noreferrer"&gt;Vue Transitions&lt;/a&gt;, &lt;a href="https://svelte.dev/docs#template-syntax-element-directives-transition-fn" rel="noopener noreferrer"&gt;Svelte Transitions&lt;/a&gt;, &lt;a href="https://swup.js.org" rel="noopener noreferrer"&gt;Swup&lt;/a&gt;, &lt;a href="https://barba.js.org/" rel="noopener noreferrer"&gt;Barba.js&lt;/a&gt; or &lt;a href="https://animate.style/" rel="noopener noreferrer"&gt;Animate.css&lt;/a&gt; to name just a few), the web still lacks a generic, standards-based and easy-to-use solution to &lt;strong&gt;animate transitions between pages or during DOM updates&lt;/strong&gt;. At least that’s what Google engineers say and I tend to agree with them.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;&lt;a href="https://turbo.hotwired.dev/" rel="noopener noreferrer"&gt;Hotwire Turbo&lt;/a&gt;&lt;/strong&gt; world specifically, several &lt;a href="https://discuss.hotwired.dev/t/are-transitions-and-animations-on-hotwire-roadmap/1547" rel="noopener noreferrer"&gt;discussions&lt;/a&gt; about integrating transition animations also took place and a few promising approaches emerged, namely the &lt;a href="https://github.com/domchristie/turn" rel="noopener noreferrer"&gt;Turn project&lt;/a&gt; or the &lt;a href="https://github.com/bridgetownrb/bridgetown/blob/13a9c4faf4df8efa1b1a00bd08929e32f66e5f8b/bridgetown-core/lib/bridgetown-core/configurations/turbo/turbo_transitions.js" rel="noopener noreferrer"&gt;transitions in Bridgetown&lt;/a&gt;. There is also a chapter in the &lt;a href="https://pragprog.com/titles/nrclient2/modern-front-end-development-for-rails-second-edition/" rel="noopener noreferrer"&gt;Noel Rappin’s Modern Front-End book&lt;/a&gt; and an interesting &lt;a href="https://edforshaw.co.uk/hotwire-turbo-stream-animations" rel="noopener noreferrer"&gt;article&lt;/a&gt; but overall, frankly, this topic still fells somewhat early-stage and exploratory.&lt;/p&gt;

&lt;p&gt;And here comes the View Transitions proposal. Could it change the situation for Hotwire and others? It of course depends on many things, most importantly on the adoption rate by &lt;a href="https://github.com/mozilla/standards-positions/issues/677" rel="noopener noreferrer"&gt;other browser&lt;/a&gt; &lt;a href="https://github.com/WebKit/standards-positions/issues/48" rel="noopener noreferrer"&gt;vendors&lt;/a&gt; and the community in general and whether the W3C Draft Recommendation track succeeds. But I have a gut feeling that yes, it might change things!&lt;/p&gt;

&lt;h2&gt;
  
  
  A ”Hello world“ example
&lt;/h2&gt;

&lt;p&gt;Let’s take a look at a simple example. Coincidentally, a bunch of examples for an animated counter have recently been created by &lt;a href="https://twitter.com/jaffathecake" rel="noopener noreferrer"&gt;Jake Archibald&lt;/a&gt; in &lt;a href="https://codesandbox.io/s/nervous-mclaren-j8v8y0?file=/src/App.tsx:0-57" rel="noopener noreferrer"&gt;React&lt;/a&gt;, &lt;a href="https://svelte.dev/repl/84cffc3241514c1581bf951bdf818def?version=3.55.1" rel="noopener noreferrer"&gt;Svelte&lt;/a&gt; and &lt;a href="https://lit.dev/playground/#project=W3sibmFtZSI6ImFwcC50cyIsImNvbnRlbnQiOiJpbXBvcnQge2h0bWwsIGNzcywgTGl0RWxlbWVudH0gZnJvbSAnbGl0JztcbmltcG9ydCB7Y3VzdG9tRWxlbWVudCwgcHJvcGVydHl9IGZyb20gJ2xpdC9kZWNvcmF0b3JzLmpzJztcbmltcG9ydCB7IHRyYW5zaXRpb25IZWxwZXIgfSBmcm9tICcuL3V0aWxzLmpzJztcblxuQGN1c3RvbUVsZW1lbnQoJ2RlbW8tYXBwJylcbmV4cG9ydCBjbGFzcyBEZW1vQXBwIGV4dGVuZHMgTGl0RWxlbWVudCB7XG4gIHN0YXRpYyBzdHlsZXMgPSBjc3NgXG4gICAgLmNvdW50IHtcbiAgICAgIGZvbnQtZmFtaWx5OiBzYW5zLXNlcmlmO1xuICAgICAgdGV4dC1hbGlnbjogY2VudGVyO1xuICAgICAgcG9zaXRpb246IGFic29sdXRlO1xuICAgICAgaW5zZXQ6IDUwJSAwIGF1dG87XG4gICAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoLTUwJSk7XG4gICAgICBmb250LXNpemU6IDI1dnc7XG4gICAgICB2aWV3LXRyYW5zaXRpb24tbmFtZTogY291bnQ7XG4gICAgICAvKiBUaGlzIHdvbid0IGJlIHJlcXVpcmVkIHNvb24uIEluIGZhY3QsIGl0IGFscmVhZHkgd29ya3Mgd2l0aG91dCB0aGlzIGluIENhbmFyeSAqL1xuICAgICAgY29udGFpbjogbGF5b3V0O1xuICAgIH1cbiAgYDtcbiAgXG4gIGluY3JlbWVudENsaWNrKCkge1xuICAgIHRyYW5zaXRpb25IZWxwZXIoe1xuICAgICAgdXBkYXRlRE9NOiBhc3luYyAoKSA9PiB7XG4gICAgICAgIHRoaXMuY291bnQrKztcbiAgICAgICAgYXdhaXQgdGhpcy51cGRhdGVDb21wbGV0ZTtcbiAgICAgIH1cbiAgICB9KTtcbiAgfVxuXG4gIEBwcm9wZXJ0eSgpXG4gIGNvdW50ID0gMDtcblxuICByZW5kZXIoKSB7XG4gICAgcmV0dXJuIGh0bWxgXG4gICAgICA8YnV0dG9uIEBjbGljaz0ke3RoaXMuaW5jcmVtZW50Q2xpY2t9PkluY3JlbWVudDwvYnV0dG9uPlxuICAgICAgPGRpdiBjbGFzcz1cImNvdW50XCI-JHt0aGlzLmNvdW50fTwvZGl2PlxuICAgIGA7XG4gIH1cbn1cbiJ9LHsibmFtZSI6ImluZGV4Lmh0bWwiLCJjb250ZW50IjoiPCFET0NUWVBFIGh0bWw-XG48aGVhZD5cbiAgPHN0eWxlPlxuICAgIGh0bWwge1xuICAgICAgYmFja2dyb3VuZDogYmxhY2s7XG4gICAgICBjb2xvcjogI2ZmZjtcbiAgICB9XG5cbiAgICAvKiBDdXN0b20gdHJhbnNpdGlvbiAqL1xuICAgIEBrZXlmcmFtZXMgcm90YXRlLW91dCB7XG4gICAgICB0byB7XG4gICAgICAgIHRyYW5zZm9ybTogcm90YXRlKDkwZGVnKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBAa2V5ZnJhbWVzIHJvdGF0ZS1pbiB7XG4gICAgICBmcm9tIHtcbiAgICAgICAgdHJhbnNmb3JtOiByb3RhdGUoLTkwZGVnKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBodG1sOjp2aWV3LXRyYW5zaXRpb24tb2xkKGNvdW50KSxcbiAgICBodG1sOjp2aWV3LXRyYW5zaXRpb24tbmV3KGNvdW50KSB7XG4gICAgICBhbmltYXRpb24tZHVyYXRpb246IDIwMG1zO1xuICAgICAgYW5pbWF0aW9uLW5hbWU6IC11YS12aWV3LXRyYW5zaXRpb24tZmFkZS1pbiwgcm90YXRlLWluO1xuICAgIH1cblxuICAgIGh0bWw6OnZpZXctdHJhbnNpdGlvbi1vbGQoY291bnQpIHtcbiAgICAgIGFuaW1hdGlvbi1uYW1lOiAtdWEtdmlldy10cmFuc2l0aW9uLWZhZGUtb3V0LCByb3RhdGUtb3V0O1xuICAgIH1cbiAgPC9zdHlsZT5cbiAgPHNjcmlwdCB0eXBlPVwibW9kdWxlXCIgc3JjPVwiLi9hcHAuanNcIj48L3NjcmlwdD5cbjwvaGVhZD5cbjxib2R5PlxuICA8ZGVtby1hcHA-PC9kZW1vLWFwcD5cbjwvYm9keT4ifSx7Im5hbWUiOiJwYWNrYWdlLmpzb24iLCJjb250ZW50Ijoie1xuICBcImRlcGVuZGVuY2llc1wiOiB7XG4gICAgXCJsaXRcIjogXCJeMi4wLjBcIixcbiAgICBcIkBsaXQvcmVhY3RpdmUtZWxlbWVudFwiOiBcIl4xLjAuMFwiLFxuICAgIFwibGl0LWVsZW1lbnRcIjogXCJeMy4wLjBcIixcbiAgICBcImxpdC1odG1sXCI6IFwiXjIuMC4wXCJcbiAgfVxufSIsImhpZGRlbiI6dHJ1ZX0seyJuYW1lIjoidXRpbHMudHMiLCJjb250ZW50IjoiZXhwb3J0IGZ1bmN0aW9uIHRyYW5zaXRpb25IZWxwZXIoe1xuICBza2lwVHJhbnNpdGlvbiA9IGZhbHNlLFxuICBjbGFzc05hbWVzID0gW10sXG4gIHVwZGF0ZURPTSxcbn0pIHtcbiAgaWYgKHNraXBUcmFuc2l0aW9uIHx8ICFkb2N1bWVudC5zdGFydFZpZXdUcmFuc2l0aW9uKSB7XG4gICAgY29uc3QgdXBkYXRlQ2FsbGJhY2tEb25lID0gUHJvbWlzZS5yZXNvbHZlKHVwZGF0ZURPTSgpKS50aGVuKCgpID0-IHt9KTtcbiAgICBjb25zdCByZWFkeSA9IFByb21pc2UucmVqZWN0KEVycm9yKCdWaWV3IHRyYW5zaXRpb25zIHVuc3VwcG9ydGVkJykpO1xuXG4gICAgLy8gQXZvaWQgc3BhbW1pbmcgdGhlIGNvbnNvbGUgd2l0aCB0aGlzIGVycm9yIHVubGVzcyB0aGUgcHJvbWlzZSBpcyB1c2VkLlxuICAgIHJlYWR5LmNhdGNoKCgpID0-IHt9KTtcblxuICAgIHJldHVybiB7XG4gICAgICByZWFkeSxcbiAgICAgIHVwZGF0ZUNhbGxiYWNrRG9uZSxcbiAgICAgIGZpbmlzaGVkOiB1cGRhdGVDYWxsYmFja0RvbmUsXG4gICAgICBza2lwVHJhbnNpdGlvbjogKCkgPT4ge30sXG4gICAgfTtcbiAgfVxuXG4gIGRvY3VtZW50LmRvY3VtZW50RWxlbWVudC5jbGFzc0xpc3QuYWRkKC4uLmNsYXNzTmFtZXMpO1xuXG4gIGNvbnN0IHRyYW5zaXRpb24gPSBkb2N1bWVudC5zdGFydFZpZXdUcmFuc2l0aW9uKHVwZGF0ZURPTSk7XG5cbiAgdHJhbnNpdGlvbi5maW5pc2hlZC5maW5hbGx5KCgpID0-XG4gICAgZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LmNsYXNzTGlzdC5yZW1vdmUoLi4uY2xhc3NOYW1lcylcbiAgKTtcblxuICByZXR1cm4gdHJhbnNpdGlvbjtcbn0ifV0" rel="noopener noreferrer"&gt;Lit&lt;/a&gt;, so let’s try building the same in Turbo! I will demonstrate it in a Ruby on Rails app as this is what I’m most accustomed to but it should work the same in any other Turbo-enabled framework.&lt;/p&gt;

&lt;p&gt;So, we will create a simple page with a big bold number and a link that will increment this number using a &lt;a href="https://turbo.hotwired.dev/handbook/frames" rel="noopener noreferrer"&gt;Turbo Frame&lt;/a&gt;. First, we’ll build a bare-bones page and then we’ll add a View Transition animation.&lt;/p&gt;

&lt;p&gt;Turbo Frame needs to route it’s requests somewhere so let’s add a route first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: :index&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding controller (by the way, a &lt;a href="https://guides.rubyonrails.org/routing.html#singular-resources" rel="noopener noreferrer"&gt;”singular“ one&lt;/a&gt;) can stay basically empty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/counter_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CounterController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template renders the &lt;code&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; tag and inside it the link and the counter itself (the &lt;a href="https://github.com/slim-template/slim" rel="noopener noreferrer"&gt;Slim&lt;/a&gt; template language and &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt; styling are used here, hopefully the notation is sufficiently self-explaining):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="c"&gt;/ app/views/counter/index.html.slim&lt;/span&gt;
&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:incrementing_counter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:counter&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"Increment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;counter_index_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;counter: &lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"block p-2 bg-gray-100 active:bg-gray-200"&lt;/span&gt;

  &lt;span class="nf"&gt;#counter&lt;/span&gt;&lt;span class="nc"&gt;.absolute.w-full.text-8xl.font-bold.text-center&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is how it looks so far: Turbo makes requests and replaces the Frame upon clicking the link and no animations are triggered (as can be seen in the lower-right panel):&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/99dPzcOQ_SA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Now, &lt;strong&gt;let’s add View Transitions to the Turbo Frame&lt;/strong&gt;. To do this, we need to &lt;a href="https://turbo.hotwired.dev/handbook/frames#custom-rendering" rel="noopener noreferrer"&gt;override the default rendering function&lt;/a&gt; for Turbo Frames in the &lt;a href="https://turbo.hotwired.dev/reference/events" rel="noopener noreferrer"&gt;&lt;code&gt;turbo:before-frame-render&lt;/code&gt; event&lt;/a&gt; handler with a custom one that utilizes View Transitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/application.js&lt;/span&gt;
&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turbo:before-frame-render&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalRender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;originalRender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newElement&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler code first checks whether View Transitions are supported by the browser and if so, it wraps the original rendering function with the &lt;a href="https://github.com/WICG/view-transitions/blob/main/explainer.md#how-the-cross-fade-worked" rel="noopener noreferrer"&gt;&lt;code&gt;document.startViewTransition&lt;/code&gt;&lt;/a&gt; function. &lt;strong&gt;And that’s it!&lt;/strong&gt; These couple of lines trigger a &lt;a href="https://github.com/WICG/view-transitions/blob/main/explainer.md#revisiting-the-cross-fade-example" rel="noopener noreferrer"&gt;default cross-fade animation&lt;/a&gt; of the whole page whenever any Turbo Frame renders. Yes, the &lt;strong&gt;whole page&lt;/strong&gt; is indeed animated by default but since most elements on the page are either identical (those outside the Turbo Frame) or replaced by identically looking elements (e.g. the link inside the Frame), in practice only the counter itself feels animated. It looks like this:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/WMaSNm6n918"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Finally, we can customize the transition a bit. To match the examples mentioned above, let’s rotate the counter during transitions. For this we will need to utilize &lt;strong&gt;”named“ View Transitions&lt;/strong&gt;, explained &lt;a href="https://github.com/WICG/view-transitions/blob/main/explainer.md#transitioning-multiple-elements" rel="noopener noreferrer"&gt;here&lt;/a&gt; in the documentation. Whenever we select a part of the screen with a CSS selector and name it using the &lt;code&gt;view-transition-name&lt;/code&gt; attribute, the browser starts to animate the corresponding screen region &lt;strong&gt;independently of all other parts&lt;/strong&gt;. This way, we can animate one or more elements on the page without affecting the rest of the page.&lt;/p&gt;

&lt;p&gt;So let’s add the following CSS to the &lt;code&gt;index&lt;/code&gt; template (Slim recognizes a &lt;code&gt;css:&lt;/code&gt; block that just renders a normal &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="c"&gt;/ app/views/counter/index.html.slim&lt;/span&gt;
&lt;span class="c"&gt;/ (anywhere outside the Turbo Frame tag)&lt;/span&gt;
&lt;span class="nd"&gt;css:
&lt;/span&gt;  &lt;span class="c"&gt;/* (1) */&lt;/span&gt;
  &lt;span class="nf"&gt;#counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;contain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;/* (2) */&lt;/span&gt;
  &lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;rotate-out&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;90deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;rotate-in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-90deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;/* (3) */&lt;/span&gt;
  &lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;animation-duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200ms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;animation-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;-ua-view-transition-fade-out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rotate-out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;animation-duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200ms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;animation-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;-ua-view-transition-fade-in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rotate-in&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s break this code down a bit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;The CSS selector &lt;code&gt;#counter&lt;/code&gt; matches the counter div and the &lt;strong&gt;&lt;code&gt;view-transition-name&lt;/code&gt; property&lt;/strong&gt; names this area of the screen, for the purpose of View Transitions, as &lt;code&gt;counter&lt;/code&gt;. This name will be used in the animation declarations below.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;clone&lt;/code&gt; property currently must be added here for some reasons internal to the current View Transitions implementation in Chrome and must be set to &lt;code&gt;paint&lt;/code&gt; or &lt;code&gt;layout&lt;/code&gt;. This restriction is planned to be removed from the specification, though, and in fact I’ve heard that it is not needed in Chrome Canary any more.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The rotation animation &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes" rel="noopener noreferrer"&gt;keyframes&lt;/a&gt; are defined here. Note that while the transition also uses fade-in and fade-out animations, they don’t have to be defined here because the spec &lt;a href="https://www.w3.org/TR/css-view-transitions-1/#ua-keyframes" rel="noopener noreferrer"&gt;requires&lt;/a&gt; browsers to implement them natively under the name &lt;code&gt;-ua-view-transition-fade-in/out&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;a href="https://devdocs.io/css/css_animations/using_css_animations" rel="noopener noreferrer"&gt;CSS animations&lt;/a&gt; for the counter (the View Transition area named &lt;code&gt;counter&lt;/code&gt;) are configured here. The CSS selectors here are some of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements" rel="noopener noreferrer"&gt;pseudo-elements&lt;/a&gt; &lt;a href="https://github.com/WICG/view-transitions/blob/main/explainer.md#how-the-cross-fade-worked" rel="noopener noreferrer"&gt;automatically created&lt;/a&gt; during the transition. The &lt;code&gt;-old&lt;/code&gt; pseudo-element represents a screenshot of the old DOM state that should somehow disappear or ”go away“ from the viewport and the &lt;code&gt;-new&lt;/code&gt; pseudo-element represents a live version of the final DOM state that should be brought into sight.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, overall, this code selects a portion of the page and &lt;strong&gt;animates it independently from the rest of the page during Turbo Frames DOM updates&lt;/strong&gt;. Behind the scenes, the default cross-fade for the rest of the page still also takes place, it just is not visible because all its elements are visually identical. The result looks like this:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/Op0Fyrg0MJo"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  A few initial tips &amp;amp; tricks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this work for &lt;a href="https://turbo.hotwired.dev/handbook/drive" rel="noopener noreferrer"&gt;Turbo Drive&lt;/a&gt; visits, too?
&lt;/h3&gt;

&lt;p&gt;Sure it does and it’s actually pretty easy! All we have to do is define the same event handler as we did above but &lt;a href="https://turbo.hotwired.dev/handbook/drive#custom-rendering" rel="noopener noreferrer"&gt;attach&lt;/a&gt; it to the &lt;a href="https://turbo.hotwired.dev/reference/events" rel="noopener noreferrer"&gt;&lt;code&gt;turbo:before-render&lt;/code&gt; event&lt;/a&gt; instead. By default we’ll get a cross-fade animation of the whole page during Turbo Drive page visits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not try to ”name“ the Turbo Frame itself
&lt;/h3&gt;

&lt;p&gt;When playing with Turbo Frame View Transitions I first tried to use a custom animation for the whole Turbo Frame element by naming it via the &lt;code&gt;view-transition-name&lt;/code&gt; property. For some reason, this does not work and you end up with a very cryptic and misleading error message in the console (yes I &lt;em&gt;did&lt;/em&gt; have the &lt;code&gt;contain&lt;/code&gt; property in the CSS declaration):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Aborting transition. Element must contain paint or layout for view-transition-name : counter&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, when using custom animations, an element from &lt;em&gt;inside&lt;/em&gt; the Frame must be selected and named.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging View Transitions
&lt;/h3&gt;

&lt;p&gt;Since View Transitions are technically just normal CSS animations, they can be &lt;a href="https://github.com/WICG/view-transitions/blob/main/explainer.md#compatibility-with-existing-developer-tooling" rel="noopener noreferrer"&gt;inspected&lt;/a&gt; with the Animations panel in the Dev Tools. Also, the automatically created pseudo-elements are visible in the Elements tab during the transitions:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/GeLTAEv7ENU"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;I confess I am quite excited about the new View Transitions API. Among the things I particularly like about it are the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; It is &lt;strong&gt;surprisingly easy&lt;/strong&gt; to plug this inside Hotwire Turbo and you get the default cross-fade transition animation immediately for free (in latest Chrome-like browsers, that is).&lt;/li&gt;
&lt;li&gt;Since this is implemented natively in the browser, the animations are &lt;strong&gt;highly optimized and performant&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;View Transitions should allow (today or in the future) building highly interactive transitions similar to &lt;a href="https://m3.material.io/styles/motion/transitions/transition-patterns" rel="noopener noreferrer"&gt;those in Material Design&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;There is some initial &lt;strong&gt;&lt;a href="https://github.com/WICG/view-transitions/blob/main/explainer.md#cross-document-same-origin-transitions" rel="noopener noreferrer"&gt;support for Multi-Page Applications&lt;/a&gt;&lt;/strong&gt;, too, which is great news because we can bring transition animations declared in CSS to our old but gold apps.&lt;/li&gt;
&lt;li&gt;It &lt;a href="https://github.com/WICG/view-transitions/blob/main/explainer.md#customizing-the-transition-based-on-the-type-of-navigation" rel="noopener noreferrer"&gt;should be possible&lt;/a&gt; to use a different animation based on the ”direction“ of the visit (Back/Forward) using the Navigation API (also still experimental and not very well supported, though).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things I am still concerned about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser support&lt;/strong&gt;: the Firefox team &lt;a href="https://github.com/mozilla/standards-positions/issues/677" rel="noopener noreferrer"&gt;evaluates it&lt;/a&gt;, the Safari team is &lt;a href="https://github.com/WebKit/standards-positions/issues/48" rel="noopener noreferrer"&gt;silent&lt;/a&gt;. This will be a log run and making a polyfill is probably &lt;a href="https://developer.chrome.com/docs/web-platform/view-transitions/#not-a-polyfill" rel="noopener noreferrer"&gt;too difficult&lt;/a&gt;. For web sites where transition animations are critical, this is still a no go.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If you’re not careful enough, the transition feels &lt;strong&gt;more fluid but also a little bit slower&lt;/strong&gt;. The reason for it is that View Transitions start the animations at the moment when both the old and new DOM states are already rendered. This means that the exit animation is delayed until new content is available and until that time, nothing happens. Also, the entry animations for the new state usually delay its appearance a little bit more.&lt;/p&gt;

&lt;p&gt;This is not a problem of View Transitions themselves but rather a more generic one. If the exit animation (e.g. a fade out) started immediately after user interaction (e.g. a link click), sometimes the user would have to stare at a blank page until the new page content is grabbed, rendered and run through an entry animation. Still, some kind of support for this scenario (possibly with custom loaders or skeletons) would be nice.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tailwind support: I think the current Tailwind syntax does not allow targeting the HTML document-connected pseudo-elements so we have to resort to custom CSS (which is not a big problem, actually).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All transitions target the whole page, there is currently no option to make, say, two components (Frames) animate totally independently. An initial proposal for ”scoped transitions“ can be found &lt;a href="https://github.com/WICG/view-transitions/blob/main/scoped-transitions.md" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, I like this feature and wish it matures enough and gets wider support soon!&lt;/p&gt;

</description>
      <category>smartcontract</category>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Styling Simple Form forms with Tailwind</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Mon, 20 Jun 2022 17:24:30 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/styling-simple-form-forms-with-tailwind-4pel</link>
      <guid>https://dev.to/nejremeslnici/styling-simple-form-forms-with-tailwind-4pel</guid>
      <description>&lt;p&gt;During our &lt;a href="https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o"&gt;recent redesign&lt;/a&gt; of the admin section of our web, we wanted to set up a system that would allow us to &lt;strong&gt;quickly build good looking forms&lt;/strong&gt;. We like to use the &lt;strong&gt;&lt;a href="https://github.com/heartcombo/simple_form" rel="noopener noreferrer"&gt;Simple Form&lt;/a&gt; gem&lt;/strong&gt; for our forms, we have been using it for nearly 10 years, and we still appreciate its very succinct but flexible syntax. Also, we style our web with &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt; and we definitely wanted to leverage it for the new admin pages, too. So, at one point of the redesign effort we had to deal with the &lt;strong&gt;”How do we style our new admin forms with Tailwind?“&lt;/strong&gt; task and we wanted to do it in the most reusable way possible and without losing Simple Form's flexibility.&lt;/p&gt;

&lt;p&gt;In our &lt;a href="https://dev.to/nejremeslnici/from-html-to-simple-form-anatomy-of-rails-forms-19m6"&gt;previous post&lt;/a&gt; we ran through the basics of Rails forms rendering and how custom Rails form builders work. In this post, we will build on that remembering that &lt;strong&gt;Simple Form is basically just a custom Rails form builder&lt;/strong&gt;. We will try to briefly describe what components it uses to render the forms and how to amend them to enable flexible Tailwind styling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Simple Form support Tailwind?
&lt;/h2&gt;

&lt;p&gt;Well, yes and no. Simple Form has no Tailwind-specific configuration baked in currently – the details are discussed in &lt;a href="https://github.com/heartcombo/simple_form/issues/1723" rel="noopener noreferrer"&gt;this GitHub issue&lt;/a&gt;. However, &lt;strong&gt;Simple Form is styling system-agnostic&lt;/strong&gt; so nothing should stop us if we sprinkled the Simple Form &lt;a href="https://github.com/heartcombo/simple_form#configuration" rel="noopener noreferrer"&gt;configuration&lt;/a&gt; with our set of Tailwind classes for each of the form components. (A &lt;a href="https://github.com/heartcombo/simple_form#custom-components" rel="noopener noreferrer"&gt;”component“&lt;/a&gt;, in Simple Form jargon, is an input, its label, a hint and error message by default and we can define a custom component for anything else if we like.). &lt;/p&gt;

&lt;p&gt;And indeed, for simple scenarios, this should work well. Problems arise when we need to &lt;strong&gt;divert from the default look&lt;/strong&gt; for a specific field. To understand that, we need to talk a bit about how Simple Form combines the default classes from the configuration with the custom classes that are meant to override them. For each component (input, label, etc.), Simple Form recognizes a &lt;strong&gt;&lt;code&gt;&amp;lt;component&amp;gt;_html&lt;/code&gt; hash of options&lt;/strong&gt;. CSS classes are handled via the &lt;code&gt;:class&lt;/code&gt; key of this hash. So, for example, &lt;code&gt;label_html: { class: "custom-class" }&lt;/code&gt; represents a way to tell Simple Form that we want a specific input’s label to have the &lt;code&gt;"custom-class"&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;Now, how does this &lt;code&gt;"custom-class"&lt;/code&gt; get merged with the default classes that we may have defined for the labels? The answer is that Simple Form simply &lt;strong&gt;appends the custom class(es) to the default classes&lt;/strong&gt;. This may work well for traditional CSS styling where we can easily target arbitrary combinations of elements, ids and classes in the CSS and thus ensure that the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity" rel="noopener noreferrer"&gt;specificity&lt;/a&gt; of the &lt;code&gt;"custom-class"&lt;/code&gt; will be high enough to override the defaults. In Tailwind, most classes have the same specificity and in that situation, according to the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade#cascading_order" rel="noopener noreferrer"&gt;CSS cascading rules&lt;/a&gt;, &lt;strong&gt;the class that appears &lt;em&gt;last&lt;/em&gt; in the Tailwind-generated CSS file is the one that wins&lt;/strong&gt; which is most probably not what we want.&lt;/p&gt;

&lt;p&gt;To demonstrate this problem with an example, let’s say we have the following default styles defined for a label in the Simple Form configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/simple_form.rb&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrappers&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="ss"&gt;:label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"font-medium"&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This config sets a ”medium“ font weight for our form labels by default. Now, suppose we want a specific input’s label to be bold instead, we might want to try the following naive approach (we’re using the &lt;a href="http://slim-lang.com/" rel="noopener noreferrer"&gt;Slim template&lt;/a&gt; notation here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="c"&gt;/ some_template.html.slim&lt;/span&gt;
&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;simple_form_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;:e_mail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Important e-mail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
                     &lt;span class="ss"&gt;label_html: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"font-bold"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we inspect the output HTML, everything seems to be in order – the &lt;code&gt;"font-bold"&lt;/code&gt; is properly added to the set of classes generated for the label and to the default class &lt;code&gt;"font-medium"&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2uev50nnric5kpjv0xk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2uev50nnric5kpjv0xk.png" alt="Generated HTML for our sample form showing the CSS cascade problem" width="800" height="83"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But the label is still not rendered bold! The explanation is that the &lt;code&gt;"font-medium"&lt;/code&gt; class &lt;strong&gt;happens to be listed below&lt;/strong&gt; the &lt;code&gt;"font-bold"&lt;/code&gt; class in the CSS file generated by Tailwind so, according to the CSS cascade rules, it wins:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fap0qwb3ikw5z9ry10caw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fap0qwb3ikw5z9ry10caw.png" alt="The CSS cascade problem" width="800" height="126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When trying to override default Tailwind classes in Simple Form, we need to be able to &lt;strong&gt;not only &lt;em&gt;add&lt;/em&gt; new classes but also &lt;em&gt;remove&lt;/em&gt;&lt;/strong&gt; some of the default ones. This is not something that Simple Form supports out of the box so we must help it a bit. &lt;/p&gt;

&lt;p&gt;One option is the &lt;a href="https://github.com/abevoelker/simple_form_tailwind_css" rel="noopener noreferrer"&gt;simple_form_tailwind_css gem&lt;/a&gt; by Abe Voelker, &lt;a href="https://github.com/heartcombo/simple_form/issues/1723#issuecomment-780303125" rel="noopener noreferrer"&gt;mentioned&lt;/a&gt; also in the GitHub issue. It uses a combination of default Tailwind styles in the &lt;a href="https://github.com/abevoelker/simple_form_tailwind_css/blob/master/lib/generators/simple_form/tailwind/templates/simple_form.rb" rel="noopener noreferrer"&gt;Simple Form configuration&lt;/a&gt; with a &lt;a href="https://github.com/abevoelker/simple_form_tailwind_css/blob/master/lib/simple_form/tailwind/form_builder.rb" rel="noopener noreferrer"&gt;custom form builder&lt;/a&gt; and a few &lt;a href="https://github.com/abevoelker/simple_form_tailwind_css/tree/master/lib/simple_form/tailwind/inputs" rel="noopener noreferrer"&gt;custom inputs&lt;/a&gt;. The gem supports replacing the default classes related to error messages with custom ones.&lt;/p&gt;

&lt;p&gt;This gem was a great source of inspiration for us but we wanted to add a bit more flexibility to the process of overriding default Tailwind classes so we chose our own custom solution that we’ll further describe below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our solution to the problem
&lt;/h2&gt;

&lt;p&gt;Let’s first recap the goals that we wanted to achieve with this solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we wanted great looking, Tailwind-styled Simple Form forms on our new admin pages by default,&lt;/li&gt;
&lt;li&gt;but with the option to amend the style of a particular field or its part if needed,&lt;/li&gt;
&lt;li&gt;from a technical point of view, we wanted the default form style and structure to be defined in a single place in the code base,&lt;/li&gt;
&lt;li&gt;and we preferred to be able to replace default classes selectively rather than all of them in bulk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the visual design and layout of the forms, we chose &lt;strong&gt;&lt;a href="https://tailwindui.com/" rel="noopener noreferrer"&gt;Tailwind UI&lt;/a&gt;&lt;/strong&gt;, a beautifully crafted set of Tailwind-styled components (more on that in a &lt;a href="https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o#the-design"&gt;previous post&lt;/a&gt; if you like). Also, inspired by Tailwind UI, we decided to layout our new admin forms on a 12-column grid. That was the relatively easy part.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coming to a default class overriding API
&lt;/h3&gt;

&lt;p&gt;To handle flexible overriding of the class methods, we explored several options that Simple Form would allow. First, we looked into the conventional way – defining default classes for all form parts in the Simple Form initialization file but in the end overriding them didn’t seem possible without monkey-patching Simple Form or re-defining all input types as custom inputs.&lt;/p&gt;

&lt;p&gt;What we wanted to achieve here, was to be able to &lt;strong&gt;override the defaults selectively&lt;/strong&gt;, i.e. we wanted to list the particular classes to remove from the defaults and add the ones that should override them. See the following sample usage for an overridden label style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;simple_form_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;:e_mail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Important e-mail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
                     &lt;span class="ss"&gt;label_html: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;remove_default_class: &lt;/span&gt;&lt;span class="s2"&gt;"font-medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
                                   &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"font-bold"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, by specifying the &lt;strong&gt;&lt;code&gt;:remove_default_class&lt;/code&gt;&lt;/strong&gt; key in the options hash we wanted to selectively remove the named default classes and then the ”standard“ &lt;code&gt;:class&lt;/code&gt; key would add the overriding classes. Thus, &lt;strong&gt;this API would allow flexible and selective replacement of the form defaults&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the end, we decided to divert a bit from Simple Form conventions and build a &lt;strong&gt;combination of a style-less wrapper configuration and a custom form builder&lt;/strong&gt;. Let’s show you how.&lt;/p&gt;

&lt;h3&gt;
  
  
  The style-less wrapper configuration
&lt;/h3&gt;

&lt;p&gt;We decided to put all class defaults &lt;strong&gt;not&lt;/strong&gt; in the Simple Form configuration but into a custom form builder. The configuration then defined a ”wrapper“, i.e. a form layout structure, called &lt;code&gt;:plain&lt;/code&gt;, that was intentionally class-less. The main part of it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="c1"&gt;# config/initializers/simple_form.rb&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrappers&lt;/span&gt; &lt;span class="ss"&gt;:plain&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="ss"&gt;:label&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrapper&lt;/span&gt; &lt;span class="ss"&gt;:input_wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;tag: :div&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="ss"&gt;:input&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="ss"&gt;:hint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="ss"&gt;wrap_with: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;tag: :p&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;wrap_with: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;tag: :p&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This wrapper can render the following raw form structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
 &lt;span class="c"&gt;&amp;lt;!-- for each field: --&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt; &lt;span class="c"&gt;&amp;lt;!-- wrapper --&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- optional label --&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt; &lt;span class="c"&gt;&amp;lt;!-- input wrapper --&amp;gt;&lt;/span&gt;
     &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- the input itself --&amp;gt;&lt;/span&gt;
     &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- optional hint --&amp;gt;&lt;/span&gt;
     &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- optional error message --&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
 ...
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes about this structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the input is wrapped in a div: this is not essential but it makes it easier to add margins or set a flexbox layout for certain types of inputs (namely booleans),&lt;/li&gt;
&lt;li&gt;all other form components are as basic as it gets, no styling, no wrappers,&lt;/li&gt;
&lt;li&gt;this configuration allows the raw form layout to be potentially reused for form variants that should look different – we would just have to create a custom form builder for each form type.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The custom form builder
&lt;/h3&gt;

&lt;p&gt;Next, we built a &lt;a href="https://github.com/heartcombo/simple_form#custom-form-builder" rel="noopener noreferrer"&gt;custom form builder&lt;/a&gt; that inherits from the &lt;a href="https://github.com/heartcombo/simple_form/blob/main/lib/simple_form/form_builder.rb" rel="noopener noreferrer"&gt;default Simple Form builder&lt;/a&gt; and that takes care of both defining and overriding the default classes. (Have a look at our &lt;a href="https://dev.to/nejremeslnici/from-html-to-simple-form-anatomy-of-rails-forms-19m6#rails-form-builders"&gt;previous post&lt;/a&gt; if you’re not sure what a Rails form builder is.)&lt;/p&gt;

&lt;p&gt;In the custom builder, we re-defined the most important methods to build various types of form components, such as the &lt;code&gt;input&lt;/code&gt; or &lt;code&gt;label&lt;/code&gt; methods. The methods basically do just a couple of things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;they define the default Tailwind classes for the given component (with a bit of programmatic logic applied, if needed),&lt;/li&gt;
&lt;li&gt;they allow to amend the defaults,&lt;/li&gt;
&lt;li&gt;and finally they pass the handling to the parent method via &lt;code&gt;super&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s show an example for a &lt;code&gt;label&lt;/code&gt; method in the custom builder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Builders::AdminFormBuilder&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;SimpleForm&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FormBuilder&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_options!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dup&lt;/span&gt;

    &lt;span class="n"&gt;default_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"block text-sm font-medium text-gray-700"&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arguments_with_updated_default_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;label&lt;/code&gt; method signature is &lt;a href="https://github.com/heartcombo/simple_form/blob/main/lib/simple_form/form_builder.rb#L324" rel="noopener noreferrer"&gt;the same&lt;/a&gt; as the one in the default Simple Form builder (we’re only using a named &lt;code&gt;block&lt;/code&gt; variable instead of an implicit block),&lt;/li&gt;
&lt;li&gt;the method extracts the options that are passed to a field via the &lt;code&gt;label_html&lt;/code&gt; options hash,&lt;/li&gt;
&lt;li&gt;it defines the default Tailwind classes for our form labels, including the &lt;code&gt;"font-medium"&lt;/code&gt; class,&lt;/li&gt;
&lt;li&gt;it calls the &lt;code&gt;arguments_with_updated_default_class&lt;/code&gt; method to update the hash with overridden classes (more on that below),&lt;/li&gt;
&lt;li&gt;and it reconstructs the original arguments and passes them to the parent method.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;arguments_with_updated_default_class&lt;/code&gt; method&lt;/strong&gt; is a private method in the builder. It takes the default classes string and a hash of options, such as the &lt;code&gt;label_html&lt;/code&gt; hash. From it it removes the classes named in the &lt;code&gt;:remove_default_class&lt;/code&gt; key of the options hash and adds the new classes listed in the &lt;code&gt;:class&lt;/code&gt; key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;arguments_with_updated_default_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;kwargs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dup&lt;/span&gt;
  &lt;span class="n"&gt;classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;default_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;

  &lt;span class="n"&gt;remove_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:remove_default_class&lt;/span&gt;
  &lt;span class="n"&gt;class_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:class&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;remove_key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
    &lt;span class="n"&gt;classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;remove_key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remove_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# simple_form sometimes uses array of classes instead of strings&lt;/span&gt;
  &lt;span class="c1"&gt;# so we have to account for that&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;class_key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;class_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;class_key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:to_s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;class_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;class_key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;class_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;class_key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;kwargs&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only non-obvious line is hopefully the one with the comment – it is here because we found out that Simple Form uses arrays of strings  instead of a space-separated string in certain cases so we had to unify the two.&lt;/p&gt;

&lt;p&gt;With these methods in the custom builder, repeating the ”bold label“ example above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;simple_form_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;:e_mail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Important e-mail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
                     &lt;span class="ss"&gt;label_html: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;remove_default_class: &lt;/span&gt;&lt;span class="s2"&gt;"font-medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
                                   &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"font-bold"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…finally produces the correct HTML with the default classes applied &lt;strong&gt;except for the default &lt;code&gt;"font-medium"&lt;/code&gt; class which is replaced by &lt;code&gt;"font-bold"&lt;/code&gt;&lt;/strong&gt;. And the label finally gets rendered bold, phew!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuc6ooq3i1o8tfjub2kdc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuc6ooq3i1o8tfjub2kdc.png" alt="The proper HTML is generated now" width="800" height="72"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Miscellaneous goodies
&lt;/h2&gt;

&lt;p&gt;Using a custom builder allows to play with the form building API in unlimited ways. For example, as we’ve said above, we lay out our admin forms in a 12-column grid. To support spanning columns in a convenient way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;:e_mail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;col_span: &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…we defined a private helper method in the builder that translates this custom &lt;code&gt;col_span&lt;/code&gt; option to the &lt;a href="https://tailwindcss.com/docs/grid-column#spanning-columns" rel="noopener noreferrer"&gt;Tailwind &lt;code&gt;col-span-X&lt;/code&gt; class&lt;/a&gt; on the wrapper div.&lt;/p&gt;

&lt;p&gt;We added more syntactic sugar to e.g. autofocus the first field of the form or to add some &lt;code&gt;data&lt;/code&gt; attributes to connect a Stimulus controller for certain special input types. The options are endless 🙂.&lt;/p&gt;

&lt;h2&gt;
  
  
  (An almost) complete form builder example
&lt;/h2&gt;

&lt;p&gt;We saw a few requests to publish some more of the actual code of our custom form builder. OK, please have a look at the &lt;a href="https://gist.github.com/borama/9054e85391d1aef37a7a5347a32e5574" rel="noopener noreferrer"&gt;&lt;code&gt;TailwindFormBuilder&lt;/code&gt; at this gist&lt;/a&gt;! The builder works as a thin wrapper around the default Simple Form builder. It cooperates closely with the style-less Simple Form wrapper configuration &lt;a href="https://dev.to/nejremeslnici/styling-simple-form-forms-with-tailwind-4pel#the-styleless-wrapper-configuration"&gt;mentioned above&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The builder defines a few methods that override methods in the Simple Form default builder. Their main purpose is that they add a set of predefined Tailwind classes to the various elements of the form field wrapper and to the field itself. Still, the methods allow to override every default class involved, by the &lt;code&gt;remove_default_class&lt;/code&gt; and &lt;code&gt;class&lt;/code&gt; parameters &lt;a href="https://dev.to/nejremeslnici/styling-simple-form-forms-with-tailwind-4pel#the-custom-form-builder"&gt;discussed above&lt;/a&gt;. Just note that in the gist there is a slightly expanded version of the helper method &lt;code&gt;arguments_with_updated_default_class&lt;/code&gt; than in the sample listing above.&lt;/p&gt;

&lt;p&gt;Please also note that the default classes defined in the builder &lt;strong&gt;are by no means complete&lt;/strong&gt; and you will have to fill them in to make the builder actually usable. The reason is that we use Tailwind UI for styling the form components and &lt;a href="https://tailwindui.com/license" rel="noopener noreferrer"&gt;its license&lt;/a&gt; (as far as we understand) does not allow us to publish  the whole styling as the builder is a ”derivative library“ rather than an end-product.&lt;/p&gt;

&lt;p&gt;To use this custom builder, you can just name it together with the custom wrapper when rendering a Simple Form form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="c"&gt;/ app/views/users/edit.html.slim&lt;/span&gt;
&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;simple_form_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;builder: &lt;/span&gt;&lt;span class="no"&gt;Builders&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TailwindFormBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;wrapper: :plain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, to use it even more conveniently, you can define a Rails helper to set the custom form builder and wrapper for us as well as style the form tag itself, for example in a 12-columns grid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/helpers/form_helper.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;FormHelper&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tailwind_form_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;default_form_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"grid grid-cols-1 gap-x-4 gap-y-6 items-start sm:grid-cols-12"&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:html&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:html&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="ss"&gt;:class&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;default_form_class&lt;/span&gt; &lt;span class="c1"&gt;# you can even use arguments_with_updated_default_class here to make the default classes more flexible&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:wrapper&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:plain&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:builder&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Builders&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TailwindFormBuilder&lt;/span&gt;

    &lt;span class="n"&gt;simple_form_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can just call this helper instead in a form template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="c"&gt;/ app/views/users/edit.html.slim&lt;/span&gt;
&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tailwind_form_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please get in touch with us if something isn’t clear enough, we can pair a bit or something…&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Using a style-less configuration and a custom form builder, we were able to &lt;strong&gt;create a system for styling our Simple Form forms with Tailwind&lt;/strong&gt;, without giving up any flexibility and without resorting to monkey-patches. This combo lives happily on our production web now. If you try a similar approach, please drop us a note!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you don’t want to miss future posts like this, follow me here or &lt;a href="https://twitter.com/boramacz" rel="noopener noreferrer"&gt;on Twitter&lt;/a&gt;. Cheers!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>rails</category>
      <category>simpleform</category>
      <category>webdev</category>
    </item>
    <item>
      <title>From HTML to Simple Form: anatomy of Rails forms</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Fri, 17 Jun 2022 17:08:09 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/from-html-to-simple-form-anatomy-of-rails-forms-19m6</link>
      <guid>https://dev.to/nejremeslnici/from-html-to-simple-form-anatomy-of-rails-forms-19m6</guid>
      <description>&lt;p&gt;In our main project, we like to use the &lt;strong&gt;&lt;a href="https://github.com/heartcombo/simple_form" rel="noopener noreferrer"&gt;Simple Form&lt;/a&gt; gem&lt;/strong&gt; for our forms, we have been using it for nearly 10 years, and we still appreciate its very succinct but flexible syntax. &lt;/p&gt;

&lt;p&gt;We also like that using Simple Form, we can build &lt;strong&gt;consistently looking and structured forms&lt;/strong&gt; with minimum effort. If you had a chance to read our &lt;a href="https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o"&gt;previous post&lt;/a&gt; about our initial experience with View Components, you may remember that we &lt;a href="https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o#we-did-not-use-view-components-for-forms-at-all"&gt;decided to not use View Components&lt;/a&gt; at all for our forms. Why? Mainly because &lt;strong&gt;Simple Form has its own component system&lt;/strong&gt; already built in and in this post we will briefly describe how it works under the hood. Also, as we will see, &lt;strong&gt;the same principles are valid for all Rails form builders&lt;/strong&gt; so findings in this post can be helpful for understanding the default style of building forms under Rails in general.&lt;/p&gt;

&lt;p&gt;That said, diving into the details of Simple Form forms may be a bit confusing at first because it’s syntax looks so different from the default Rails form helpers. For example, in Simple Form, a single line of code may define all of: the form input field, its label, a hint or even the corresponding validation error message. So how does Simple Form actually generate the final HTML markup?&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a Simple Form (and Rails) form
&lt;/h2&gt;

&lt;p&gt;To show this, let’s try a bottom-up approach. We’ll start with pure HTML and will build gradually, layer by layer, the same form using more and more abstracted syntax until we reach the Simple Form style. This will hopefully help us clarify the underlying concepts.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTML
&lt;/h3&gt;

&lt;p&gt;A basic form may be rendered like the following in the final HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/users"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Enter your name: &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Enter your email: &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Subscribe!"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This HTML may be equally coded in a template (we use a &lt;a href="http://slim-lang.com/" rel="noopener noreferrer"&gt;Slim template&lt;/a&gt; syntax here) like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/users"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;
  &lt;span class="nt"&gt;div&lt;/span&gt;
    &lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Enter&lt;span class="w"&gt; &lt;/span&gt;your&lt;span class="w"&gt; &lt;/span&gt;name:
    &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;#name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;
  &lt;span class="nt"&gt;div&lt;/span&gt;
    &lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Enter&lt;span class="w"&gt; &lt;/span&gt;your&lt;span class="w"&gt; &lt;/span&gt;email:
    &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;#email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
  &lt;span class="nt"&gt;div&lt;/span&gt;
    &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Subscribe!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rails form tag helpers
&lt;/h3&gt;

&lt;p&gt;We can get the same HTML output (except negligible details) using the &lt;a href="https://guides.rubyonrails.org/form_helpers.html#using-tag-helpers-without-a-form-builder" rel="noopener noreferrer"&gt;Rails form tag helpers&lt;/a&gt;. The word ”tag“ here illustrates that these are helper functions that can each do only one thing – &lt;strong&gt;render a particular HTML tag&lt;/strong&gt;. In fact we are just using a slightly different syntax to render our form than before while the code structure stays the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;form_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nt"&gt;div&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;label_tag&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Enter your name: "&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text_field_tag&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
  &lt;span class="nt"&gt;div&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;label_tag&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Enter your email: "&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text_field_tag&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;
  &lt;span class="nt"&gt;div&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;submit_tag&lt;/span&gt; &lt;span class="s2"&gt;"Subscribe!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rails form helpers
&lt;/h3&gt;

&lt;p&gt;OK, now we’re getting to more interesting stuff: Rails form helpers, &lt;strong&gt;&lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with" rel="noopener noreferrer"&gt;&lt;code&gt;form_with&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; or its older cousin &lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for" rel="noopener noreferrer"&gt;&lt;code&gt;form_for&lt;/code&gt;&lt;/a&gt;, make a bigger change, especially when the form is &lt;a href="https://guides.rubyonrails.org/form_helpers.html#binding-a-form-to-an-object" rel="noopener noreferrer"&gt;bound to an model object&lt;/a&gt;, such as &lt;code&gt;@user&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="nt"&gt;div&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Enter your name: "&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
  &lt;span class="nt"&gt;div&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Enter your email: "&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;
  &lt;span class="nt"&gt;div&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="s2"&gt;"Subscribe!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with" rel="noopener noreferrer"&gt;&lt;code&gt;form_with&lt;/code&gt; form helper&lt;/a&gt; used here does at least the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it &lt;a href="https://guides.rubyonrails.org/form_helpers.html#relying-on-record-identification" rel="noopener noreferrer"&gt;checks&lt;/a&gt; whether &lt;code&gt;@user&lt;/code&gt; is a new or an existing record and generates the proper route (action and method) for the form,&lt;/li&gt;
&lt;li&gt;it recognizes that the &lt;code&gt;@user&lt;/code&gt; has e.g. its &lt;code&gt;name&lt;/code&gt; attribute set and prefills the &lt;code&gt;name&lt;/code&gt; field with this value,&lt;/li&gt;
&lt;li&gt;and it follows the &lt;a href="https://guides.rubyonrails.org/form_helpers.html#understanding-parameter-naming-conventions" rel="noopener noreferrer"&gt;Rails conventions&lt;/a&gt; to name the fields (e.g. with the &lt;code&gt;name="user[name]"&lt;/code&gt; attribute) so that the submitted POST data can be nicely processed in the target controller.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that &lt;strong&gt;we still have to layout the form by ourselves&lt;/strong&gt;: we define the form structure and Rails form helpers take care of the field / form tags. This pattern is the default in Rails and gives us great flexibility – we can structure each form any way we like – but perhaps makes it a bit &lt;strong&gt;harder to keep the forms consistent&lt;/strong&gt;, especially when we have many complex forms (as is typical in a web admin section) because we have to keep all the form templates structure the same, too. To achieve greater consistency, we could get some help from Rails form builders.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rails form builders
&lt;/h3&gt;

&lt;p&gt;A Rails form builder is an &lt;strong&gt;object used by Rails to build forms&lt;/strong&gt;. It is instantiated in the form helper &lt;code&gt;form_with&lt;/code&gt; / &lt;code&gt;form_for&lt;/code&gt; and is yielded in the form block. In the code sample above, the &lt;code&gt;f&lt;/code&gt; variable is a Rails form builder object. The builder methods (&lt;code&gt;text_field&lt;/code&gt;, &lt;code&gt;label&lt;/code&gt;, etc.) know about the form model object (&lt;code&gt;@user&lt;/code&gt;) and use the form tag helpers to render the corresponding fields, labels or other elements, potentially wrapped with arbitrary HTML tags. The &lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html" rel="noopener noreferrer"&gt;default Rails form builder&lt;/a&gt; though serves just as a simple proxy to the form tag helpers and that is why the overall form HTML structure is, by default, fully on the developer.&lt;/p&gt;

&lt;h4&gt;
  
  
  Custom form builder
&lt;/h4&gt;

&lt;p&gt;Now, we can create a &lt;strong&gt;&lt;a href="https://guides.rubyonrails.org/form_helpers.html#customizing-form-builders" rel="noopener noreferrer"&gt;custom form builder&lt;/a&gt;&lt;/strong&gt; to, for example, tighten the rendered form HTML. The simplest option is to derive it from the default Rails form builder. There are a few internal variables that we need to be aware of when working with form builders: &lt;code&gt;@template&lt;/code&gt; represents the view context that we use to call the underlying tag helper methods, &lt;code&gt;@object&lt;/code&gt; is the model object (&lt;code&gt;@user&lt;/code&gt;) and &lt;code&gt;@object_name&lt;/code&gt; is its name (&lt;code&gt;"user"&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;So, for example, to support a possibility to wrap a text field and a label together with a wrapper div, we can define the following custom builder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/form_builders/labelling_form_builder.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LabellingFormBuilder&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionView&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Helpers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FormBuilder&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="vi"&gt;@template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:div&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"wrapper"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:label&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; 
         &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;except&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:label&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and use it in a form template like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="c"&gt;/ app/views/users/new.html.slim&lt;/span&gt;
&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;builder: &lt;/span&gt;&lt;span class="no"&gt;LabellingFormBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Enter your email"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…which generates the following HTML output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/users"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wrapper"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"user_email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Enter your email&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"user[email]"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"user_email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s see what is going on here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we inherit our custom builder class &lt;code&gt;LabellingFormBuilder&lt;/code&gt; from the default Rails form builder&lt;/li&gt;
&lt;li&gt;we override its method called &lt;code&gt;text_field&lt;/code&gt; so that it renders both label and the input field itself&lt;/li&gt;
&lt;li&gt;we can pass a custom label text via our custom option key &lt;code&gt;:label&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;both label and input are wrapped with a wrapper div, note that we have to use the &lt;code&gt;@template&lt;/code&gt; variable the a view context for calling Rails view helpers&lt;/li&gt;
&lt;li&gt;in the template, we specify our custom builder with the &lt;code&gt;builder&lt;/code&gt; option and pass the custom option for the label&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OK, we successfully &lt;strong&gt;amended the generated fields HTML structure using a custom form builder&lt;/strong&gt; and now we can build a text field including its label using a single line of code and have both elements wrapped with a div that we can style.&lt;/p&gt;

&lt;p&gt;Also note that &lt;strong&gt;we’ve actually created an initial version of a ”component“ to build forms&lt;/strong&gt; – the code for the layout and structure of the form HTML is defined in a single place in the code base. We can change this one place – the custom builder – and it will affect all forms using that builder.&lt;/p&gt;

&lt;p&gt;Overall, custom form builders make it possible to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;alter the way input fields and their accompanying HTML elements are rendered and structured in the form,&lt;/li&gt;
&lt;li&gt;add methods for special types of fields,&lt;/li&gt;
&lt;li&gt;define a specific API for building forms with a concrete structure and layout, thus helping forms consistency,&lt;/li&gt;
&lt;li&gt;use all of ruby syntax to define a logic around our forms, define presets / default values, styles and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Simple Form (and others)
&lt;/h3&gt;

&lt;p&gt;By now, we should have enough information to understand how the Simple Form gem works. Technically, &lt;strong&gt;&lt;a href="https://github.com/heartcombo/simple_form" rel="noopener noreferrer"&gt;Simple Form&lt;/a&gt; just &lt;a href="https://github.com/heartcombo/simple_form/blob/main/lib/simple_form/form_builder.rb" rel="noopener noreferrer"&gt;provides&lt;/a&gt; a custom Rails form builder&lt;/strong&gt;. The same holds for other Rails-based &lt;a href="https://www.ruby-toolbox.com/categories/rails_form_builders" rel="noopener noreferrer"&gt;form builder gems&lt;/a&gt;, such as &lt;a href="https://github.com/formtastic/formtastic/blob/master/lib/formtastic/form_builder.rb" rel="noopener noreferrer"&gt;Formtastic&lt;/a&gt; or &lt;a href="https://github.com/bootstrap-ruby/bootstrap_form/blob/main/lib/bootstrap_form/form_builder.rb" rel="noopener noreferrer"&gt;bootstrap_form&lt;/a&gt; (as opposed to Rails-independent form building gems such as &lt;a href="https://github.com/jeremyevans/forme" rel="noopener noreferrer"&gt;Forme&lt;/a&gt;). &lt;/p&gt;

&lt;p&gt;So, once again, Simple Form is nothing but a custom Rails form builder that comes with a rich set of features around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using the builder’s helper methods, especially the one called &lt;code&gt;input&lt;/code&gt; we can generate everything related to a form field: the field itself, its label, hint or error message.&lt;/li&gt;
&lt;li&gt;Or anything else, actually! Simple Form makes no assumptions about the form HTML markup and the form structure is &lt;a href="https://github.com/heartcombo/simple_form#configuration" rel="noopener noreferrer"&gt;fully configurable&lt;/a&gt; in a single place – the Simple Form initializer file.&lt;/li&gt;
&lt;li&gt;It also provides a set of custom fields with their own logic for processing and rendering model data. Notably, Simple Form supports various automatic processing of &lt;a href="https://github.com/heartcombo/simple_form#associations" rel="noopener noreferrer"&gt;records associated&lt;/a&gt; to the main model object or generic &lt;a href="https://github.com/heartcombo/simple_form#collections" rel="noopener noreferrer"&gt;collections&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Simple Form, our sample form could be encoded in a template like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight slim"&gt;&lt;code&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;simple_form_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Enter your name"&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="s2"&gt;"Enter your email"&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt; &lt;span class="ss"&gt;:submit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Subscribe!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note how everything about a form field is defined in a single line (and we only touched the surface here). &lt;strong&gt;Simple Form is great if we prefer consistency&lt;/strong&gt; when building our forms. By configuring Simple Form &lt;a href="https://github.com/heartcombo/simple_form#the-wrappers-api" rel="noopener noreferrer"&gt;”wrappers“&lt;/a&gt;, we can define the default structure and layout of all our forms (plus, of course, alternative form versions if we need them). Moreover, all layout options can be &lt;strong&gt;overridden in a particular form or field&lt;/strong&gt; so we don’t lose any flexibility if we need that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mixing different types of form helpers together
&lt;/h3&gt;

&lt;p&gt;As a final note, we can &lt;strong&gt;mix different layers&lt;/strong&gt; of form building helpers, even in a single form. For example, nothing would stop us if we tried to render a form using Simple Form but used a Rails form tag helper for some particular field that must be handled specially. &lt;/p&gt;

&lt;p&gt;Doing so, we only have to think about Rails conventions, especially those for &lt;a href="https://guides.rubyonrails.org/form_helpers.html#understanding-parameter-naming-conventions" rel="noopener noreferrer"&gt;naming form fields&lt;/a&gt;, and obey them manually when rendering the field. The Rails documentation has &lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with-label-Mixing+with+other+form+helpers" rel="noopener noreferrer"&gt;an example&lt;/a&gt; showing how to mix form layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We went through all layers from the bottom of the generated HTML to the custom form builders and Simple Form. In a future post, we will build on this information to describe how we created a custom Rails form builder to conveniently allow styling our new admin forms with Tailwind.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you don’t want to miss future posts like this, follow me here or &lt;a href="https://twitter.com/boramacz" rel="noopener noreferrer"&gt;on Twitter&lt;/a&gt;. Cheers!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>forms</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>From partials to ViewComponents: writing reusable front-end code in Rails</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Fri, 03 Jun 2022 16:21:46 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o</link>
      <guid>https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o</guid>
      <description>&lt;p&gt;Recently, we began using &lt;a href="https://viewcomponent.org/" rel="noopener noreferrer"&gt;ViewComponents&lt;/a&gt; in our project to help us build the redesigned admin section of our web. We want to share our decision process that made us try this framework in the first place and how well it went. &lt;strong&gt;TL;DR? We love it! ❤&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The situation
&lt;/h2&gt;

&lt;p&gt;Being a long-term product-oriented project, every now and then we find ourselves rewriting some, even fully working, pages from scratch: to refresh the design, to speed up the page load or to apply new coding standards and get rid of the old ones. Now, the time has come to our old and rusty admin section.&lt;/p&gt;

&lt;p&gt;We have lots of – fairly standard – admin pages: with index tables, details and edit forms. We estimated that about 80-90% of our admin pages could look and behave in a more or less unified way, the rest being special pages that must be carefully optimized for the most essential needs our administrators have when doing their job. Although a large part of our admin was already written in a reusable way, we wanted to revamp the look and feel, bringing in new standards including styling via &lt;a href="https://tailwindcss.com" rel="noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt; or higher interactivity with the &lt;a href="https://hotwired.dev/" rel="noopener noreferrer"&gt;Hotwire stack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We knew from the start that rebuilding admin section was a perfect case for developing and applying components in the view layer. &lt;strong&gt;By ”a component“ we mean a piece of reusable code&lt;/strong&gt;, isolated and encapsulated from others that in the end gets rendered as a visually and functionally distinct part of a webpage. A component should easily provide a default look but still allow flexibility if needed (some support for more visual variants and / or behaviors). &lt;/p&gt;

&lt;h2&gt;
  
  
  The design
&lt;/h2&gt;

&lt;p&gt;To build a user interface made of reusable components, we first had to choose a &lt;strong&gt;template or a design library&lt;/strong&gt; that would promise a consistent look for all the various page sections that we needed. We assessed &lt;a href="https://themeforest.net/item/yeti-admin-tailwind-css/29702349" rel="noopener noreferrer"&gt;many&lt;/a&gt; &lt;a href="https://templates.iqonic.design/hope-ui/tailwind/dist/dashboard/" rel="noopener noreferrer"&gt;such&lt;/a&gt; &lt;a href="https://aatrox-demo.vercel.app/" rel="noopener noreferrer"&gt;Tailwind-ready&lt;/a&gt; &lt;a href="https://themeforest.net/item/tailstack-tailwind-admin-dashboard/28906006" rel="noopener noreferrer"&gt;templates&lt;/a&gt; focusing on admin interfaces but eventually decided to invest in &lt;strong&gt;&lt;a href="https://tailwindui.com/" rel="noopener noreferrer"&gt;Tailwind UI&lt;/a&gt;&lt;/strong&gt;. And we never regretted since! &lt;/p&gt;

&lt;p&gt;Tailwind UI is a set of wonderfully crafted visual components made with an eye for detail and with responsiveness in mind, perfectly suitable for the latest Tailwind CSS versions. It’s a showcase of what the Tailwind design gurus think is nice and functional and even though we didn’t use the advanced features such as Vue/React templates, we learned a lot from the code samples. What we particularly liked about Tailwind UI is that there was often more than one alternative laid out for a visual feature giving us more options to choose and be inspired from.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What about a full-fledged Rails admin gem?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We briefly considered migrating to a full-grown Rails admin interface, such as &lt;a href="https://activeadmin.info/" rel="noopener noreferrer"&gt;ActiveAdmin&lt;/a&gt;, &lt;a href="https://github.com/railsadminteam/rails_admin" rel="noopener noreferrer"&gt;RailsAdmin&lt;/a&gt;, &lt;a href="https://administrate-demo.herokuapp.com/" rel="noopener noreferrer"&gt;Administrate&lt;/a&gt; or &lt;a href="https://avohq.io/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt;. &lt;strong&gt;We especially liked Avo&lt;/strong&gt; which is built on a very modern stack similar to ours (Tailwind + Hotwire + ViewComponents). In the end, we didn’t go this route as we found some of the options a bit too restrictive (even though Avo is very flexible) and we did not feel like trying to amend it to our needs. For example, Avo renders forms in a &lt;a href="https://avodemo.herokuapp.com/avo/resources/projects/38/edit" rel="noopener noreferrer"&gt;1-field-per-row layout&lt;/a&gt; while we wanted something more similar to the Tailwind UI &lt;a href="https://tailwindui.com/components/application-ui/forms/form-layouts#component-30dffb06e58cdbe872820ed3f943d85a" rel="noopener noreferrer"&gt;Stacked form layout&lt;/a&gt;. Nevertheless, we found a great deal of inspiration in the Avo code and its design principles.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Options for reusable front-end code in Rails
&lt;/h2&gt;

&lt;p&gt;After we settled down on the design, we pondered about a suitable way to add the front-end components to our code base. What options do Rails actually give us in the first place? We considered partial templates, Rails helpers and then we moved on to other possible solutions. Below is a brief summary of our thought process at that time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partial templates
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://guides.rubyonrails.org/layouts_and_rendering.html#using-partials" rel="noopener noreferrer"&gt;Partial template&lt;/a&gt; (or ”partial“) is a standard Rails way to extract a piece of template code to its own file. The partial then can be called (rendered) from other templates, helpers or controllers.&lt;/p&gt;

&lt;p&gt;Partials, like all templates, are &lt;strong&gt;HTML-centric&lt;/strong&gt; – they are the strongest for embedding various HTML tags in a structure which is then rendered on a web page. However, they are not that great if you need to add some non-trivial logic – a template with more than a few control statements can quickly become messy.&lt;/p&gt;

&lt;p&gt;In our opinion, the biggest issue, from a ”component“ point of view, is their &lt;strong&gt;lack of isolation&lt;/strong&gt;. A partial template freely recognizes all instance variables (&lt;code&gt;@variable&lt;/code&gt;) and this makes the template tightly coupled with the controller layer where these variables are typically defined. Suppose we’d use a partial template on five different pages: we would have to define the same instance variable(s) in all five actions of the corresponding controllers. Even worse, instance variables default to &lt;code&gt;nil&lt;/code&gt; so forgetting to properly set a variable does not raise an exception, instead it can just lead to an unexpected blank output. &lt;/p&gt;

&lt;p&gt;Sure, we could pass the data as &lt;strong&gt;&lt;a href="https://guides.rubyonrails.org/layouts_and_rendering.html#passing-local-variables" rel="noopener noreferrer"&gt;local variables&lt;/a&gt;&lt;/strong&gt; instead (and we heartily encourage such a more explicit style of passing data to partials) but this convention would have to be guarded and enforced all over the team. We actually run a &lt;a href="https://github.com/sds/overcommit#repo-specific-hooks" rel="noopener noreferrer"&gt;custom Overcommit hook&lt;/a&gt; to encourage this explicit style in our project. Still, the &lt;code&gt;locals&lt;/code&gt; hash is just… a &lt;code&gt;Hash&lt;/code&gt; and makes the partials API only moderately flexible for us, especially since we settled down on using &lt;a href="https://thoughtbot.com/blog/ruby-2-keyword-arguments" rel="noopener noreferrer"&gt;keyword argument&lt;/a&gt; APIs wherever possible.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update as of January 2024:&lt;/strong&gt; Actually, since Rails 7.1, we have the option to define &lt;a href="https://edgeguides.rubyonrails.org/7_1_release_notes.html#allow-templates-to-set-strict-locals" rel="noopener noreferrer"&gt;strict locals&lt;/a&gt; in Rails partials, see our &lt;a href="https://dev.to/nejremeslnici/strict-locals-in-slim-haml-partials-in-rails-2f73"&gt;post about them&lt;/a&gt;. We consider strict locals a very nice feature as they allow to enforce an "API" for calling / rendering Rails partials. This is definitely a bonus point for Rails partials.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The nice thing about partial templates is that &lt;strong&gt;templates are unit-testable&lt;/strong&gt; with &lt;a href="https://relishapp.com/rspec/rspec-rails/docs/view-specs/view-spec" rel="noopener noreferrer"&gt;View specs&lt;/a&gt; (or similarly in Minitest) and the rendered output can even be verified using &lt;a href="https://github.com/teamcapybara/capybara" rel="noopener noreferrer"&gt;Capybara&lt;/a&gt; matchers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rails helpers
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.rubyguides.com/2020/01/rails-helpers/" rel="noopener noreferrer"&gt;Helpers&lt;/a&gt; are another ”Railsy“ option to componentize things in the view layer. They are simple ruby functions, living inside a &lt;code&gt;module&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As opposed to templates, Rails helpers are &lt;strong&gt;ruby-centric&lt;/strong&gt;. Being just normal ruby functions, they support any API style for passing parameters that is supported by your ruby version, including keyword arguments.&lt;/p&gt;

&lt;p&gt;But the easier it is to call and embed ruby code in a helper, &lt;strong&gt;the harder it tends to be to combine more HTML tags&lt;/strong&gt; there into a renderable structure. Simple tags are fine and there are a multitude of pre-defined ActionView helpers available, such as &lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag" rel="noopener noreferrer"&gt;&lt;code&gt;content_tag&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to" rel="noopener noreferrer"&gt;&lt;code&gt;link_to&lt;/code&gt;&lt;/a&gt; that reduce repetition. But building a more complex HTML structure inside a helper can become a painful experience. Sooner than later one finds that he or she needs to &lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-concat" rel="noopener noreferrer"&gt;&lt;code&gt;concat&lt;/code&gt;&lt;/a&gt; things, perhaps even &lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html#method-i-capture" rel="noopener noreferrer"&gt;&lt;code&gt;capture&lt;/code&gt;&lt;/a&gt; things and all the time they must be aware of the possible security implications of building such HTML structure and learn about &lt;a href="https://api.rubyonrails.org/classes/String.html#method-i-html_safe" rel="noopener noreferrer"&gt;&lt;code&gt;html_safe&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-safe_concat" rel="noopener noreferrer"&gt;&lt;code&gt;safe_concat&lt;/code&gt;&lt;/a&gt; and similar stuff while templates partly mitigate this problem by regarding raw HTML tags as &lt;code&gt;html_safe&lt;/code&gt; by default.&lt;/p&gt;

&lt;p&gt;While helpers can deal with ruby code logic more elegantly than templates, they are nowhere near ruby classes or objects in terms of flexibility. Helpers are – similarly to templates – not isolated from instance variables but it is perhaps a bit more straightforward to obey a convention of using only function parameters to pass data to them. But &lt;strong&gt;helpers are also global&lt;/strong&gt;: each helper is available in the whole view layer, which means that their names must be unique and that categorizing them into multiple files (modules) makes less sense as it brings no real encapsulation. Add to it the fact that Rails itself defines dozens of helpers so the helpers namespace can become quite cluttered and name collisions with hard-to-debug surprises may occur.&lt;/p&gt;

&lt;p&gt;It is easy to &lt;a href="https://relishapp.com/rspec/rspec-rails/v/5-1/docs/helper-specs/helper-spec" rel="noopener noreferrer"&gt;unit-test Rails helpers&lt;/a&gt; but only as plain ruby functions, for example it is not possible to use Capybara on the generated output by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Combination of partials and helpers
&lt;/h3&gt;

&lt;p&gt;We’ve seen that partial templates and helpers have each their own strengths and weaknesses in terms of building components. So why not let each of them focus on what they can do best? Indeed, &lt;strong&gt;partials and helpers are meant to cooperate&lt;/strong&gt;: one can easily call helpers from templates as well as render partials from helper functions.&lt;/p&gt;

&lt;p&gt;While we were sure that we could get pretty far using a combination of helpers and templates, by the time we were assessing this option, we already knew we wanted to look elsewhere. To us, &lt;strong&gt;the two worlds are too distinct for building components&lt;/strong&gt;, the two types of code are located too far from each other without an obvious interconnection between them. Partials and helpers may play together  well but they still don’t make a clear unit.&lt;/p&gt;

&lt;p&gt;So what about the &lt;strong&gt;world outside Rails defaults&lt;/strong&gt;? There are quite a few independent projects trying to help build components in the Rails view layer, among the more famous being &lt;a href="https://github.com/drapergem/draper" rel="noopener noreferrer"&gt;Draper&lt;/a&gt; (utilizing the decorators pattern) or &lt;a href="https://github.com/trailblazer/cells" rel="noopener noreferrer"&gt;Cells&lt;/a&gt; (full-featured components in views). In the end, we decided to take a deeper look into a relatively new one – the ViewComponent framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  View Components
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/github/view_component" rel="noopener noreferrer"&gt;ViewComponent framework&lt;/a&gt; has originally been developed and &lt;a href="https://github.blog/2020-12-15-encapsulating-ruby-on-rails-views/" rel="noopener noreferrer"&gt;used extensively&lt;/a&gt; at GitHub. It provides a set of conventions to build components in the view layer that should make them well encapsulated, reusable, flexible and testable. Below are our comments to features that we particularly liked about View Components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;View Components have an &lt;strong&gt;explicit home in the code base&lt;/strong&gt;. By ”having a home“ we not only mean that view components reside under the &lt;code&gt;app/components&lt;/code&gt; folder but also the fact that the code for the component behavior as well as its template live next to each other, in the same place in the code base. The components code can be &lt;strong&gt;categorized into folders by their meaning or function&lt;/strong&gt; rather than technology.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The component ruby file supports &lt;strong&gt;logic of any complexity&lt;/strong&gt;. A component is just a ruby class so we can leverage all features of object-oriented programming in them such as private methods, composition, inheritance and just about anything else, if needed. The template file, on the other hand, can stay virtually &lt;strong&gt;logic-less&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;View components are &lt;strong&gt;truly encapsulated&lt;/strong&gt;. All configuration and data for the component must be explicitly passed in via the &lt;code&gt;initialize&lt;/code&gt; method arguments or blocks. Even Rails helpers are &lt;a href="https://viewcomponent.org/guide/helpers.html" rel="noopener noreferrer"&gt;not automatically recognized&lt;/a&gt; and must be explicitly included or accessed through a &lt;code&gt;helpers&lt;/code&gt; proxy object.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;rendering of components is flexible&lt;/strong&gt;, too. The developer can choose whether to render the output in a template file or &lt;a href="https://viewcomponent.org/guide/templates.html#inline" rel="noopener noreferrer"&gt;inline in the ruby code&lt;/a&gt; (in the &lt;code&gt;call&lt;/code&gt; method). The former style suits well for components with a more complex HTML structure, the latter for smaller and simpler ones.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;View Components support and &lt;strong&gt;encourage &lt;a href="https://viewcomponent.org/guide/testing.html" rel="noopener noreferrer"&gt;testing via unit tests&lt;/a&gt;&lt;/strong&gt;. The tests are then very fast and validate the rendered HTML output (with support for Capybara matchers included).&lt;br&gt;
Good tests coverage of the view layer is – frankly – not that common in Rails projects because it is notoriously unpleasant. &lt;a href="https://guides.rubyonrails.org/testing.html#system-testing" rel="noopener noreferrer"&gt;System tests&lt;/a&gt; tend to give good coverage but are slow and hard to maintain while view unit tests are possible, as we saw above, but hard to isolate and prepare test data for. View Components profit from their natural encapsulation and explicit data flow, so writing unit tests for them should be much easier.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There is a &lt;strong&gt;&lt;a href="https://viewcomponent.org/guide/previews.html" rel="noopener noreferrer"&gt;preview mode&lt;/a&gt; for View Components&lt;/strong&gt;. This is especially handy because it encourages components reuse and provides an obvious place for sample code and documentation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This all looks very well but did we see &lt;strong&gt;any disadvantages&lt;/strong&gt; before starting with View Components? Not much, really. We expected that building components including meaningful previews and tests definitely requires a bit more work than creating a partial template, for example, but the benefits of doing so, in our eyes, outweighed the pain.&lt;/p&gt;

&lt;p&gt;The biggest unclear area that we saw related to view components were &lt;strong&gt;forms&lt;/strong&gt;. There were glimpses of &lt;a href="https://viewcomponent.org/known_issues.html#form_for-compatibility" rel="noopener noreferrer"&gt;compatibility issues&lt;/a&gt; with Rails form helpers in the documentation and we saw a &lt;a href="https://github.com/github/view_component/pull/1307" rel="noopener noreferrer"&gt;recent effort&lt;/a&gt; of the team to mitigate them. Moreover, we were used to building forms with &lt;a href="https://github.com/heartcombo/simple_form" rel="noopener noreferrer"&gt;Simple Form&lt;/a&gt; which added another variable to the equation. And, in general, we considered the Rails form builders (as well as the Simple Form builder) a system of form-related components in the first place so we were unsure how this would fit into the View Components ecosystem or whether we should even try to do that.&lt;/p&gt;

&lt;p&gt;Nevertheless, we decided to try building a few View Components for our new admin interface, and see how it goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  View Components after a few weeks of intensive usage
&lt;/h2&gt;

&lt;p&gt;And yes, we have some findings to share after a few weeks of using and building View Components.&lt;/p&gt;

&lt;h3&gt;
  
  
  It was easy to start with View Components
&lt;/h3&gt;

&lt;p&gt;The conventions of View Components seemed so clear and obvious that after a few tries we were able to build new components without hesitation. We routinely added ruby code, templates, unit tests and previews and used the emerging components on the admin pages that we were rebuilding. The feeling that more and more of a page is compiled from a few well-defined and well-tested components is very addictive!&lt;/p&gt;

&lt;h3&gt;
  
  
  The previews are great, Lookbook is awesome
&lt;/h3&gt;

&lt;p&gt;One of the features that we liked the most were previews, especially after we found about the &lt;strong&gt;&lt;a href="https://github.com/allmarkedup/lookbook" rel="noopener noreferrer"&gt;Lookbook project&lt;/a&gt;&lt;/strong&gt;. It is a user interface for viewing, documenting and interacting with View Component previews. The best thing is that Lookbook does not deviate from View Component preview conventions so a developer just has to write a VC preview with perhaps a few optional annotations in the comments and Lookbook automatically converts it into something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkt0ajh1qv93fbdbjomwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkt0ajh1qv93fbdbjomwu.png" alt="Lookbook" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We really love Lookbook and it immediately became the official developer version of our ”Design manual“ accessible for everyone in our company.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building view components can be a hard core API coding job
&lt;/h3&gt;

&lt;p&gt;After a few weeks we realized something unexpected. Building components, especially the more complex and universal ones (that you might expect in an admin interface), felt &lt;strong&gt;more like back-end rather than front-end work&lt;/strong&gt;. Of course, we had to encode the component into a HTML template and style it but this seemed like an icing on the cake. Instead, thinking how to meaningfully pass data into the components and how to interconnect them while still allowing reasonable flexibility became the main focus of our work. Which brings us to the next point…&lt;/p&gt;

&lt;h3&gt;
  
  
  We had the best results with an outside-in approach
&lt;/h3&gt;

&lt;p&gt;We put a lot of energy into trying to make the components as easily reusable for the developers as possible. For this we always &lt;strong&gt;started by writing code samples&lt;/strong&gt; that would use the (at that time non-existent) component. We tried a few variants and attempted to cover all the use/edge cases known at that time. Only after we were happy with the external API for the component, we moved on to solving its internals. The result is a set of components that we find lovable to use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Helpers can help components rendering substantially
&lt;/h3&gt;

&lt;p&gt;For the components that were meant to be reused frequently, we &lt;strong&gt;always added a helper&lt;/strong&gt; for the sole purpose of simplifying calling the component. For example the &lt;code&gt;HeadingComponent&lt;/code&gt; from the image above, is actually meant to be called via an &lt;code&gt;admin_heading&lt;/code&gt; helper which is just a simple wrapper around the component rendering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;AdminComponentsHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;admin_heading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;Containers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Admin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HeadingComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Luckily, View Component previews as well as Lookbook work with helpers without issues so there was nothing stopping us from documenting the actual encouraged style of using the components.&lt;/p&gt;

&lt;h3&gt;
  
  
  We did not use View Components for forms at all
&lt;/h3&gt;

&lt;p&gt;In important conclusion regarding forms came from this effort as well: we decided to not use View Components for forms at all. While we were not particularly happy about having to maintain two different component systems in our code base, we took this pragmatic decision because we like the &lt;a href="https://github.com/heartcombo/simple_form" rel="noopener noreferrer"&gt;Simple Form&lt;/a&gt; style and &lt;strong&gt;Simple Form itself is a very flexible component system&lt;/strong&gt;, just focused on forms building. &lt;/p&gt;

&lt;p&gt;Theoretically, we could be able to mimic the Simple Form API with a set of form-related View Components but we didn’t think the effort was worth it. Instead, we dove deep in &lt;strong&gt;Simple Form builders&lt;/strong&gt; and managed to create a Tailwind-styled one that suits our needs perfectly (this might deserve a separate post; &lt;strong&gt;update&lt;/strong&gt;: &lt;a href="https://dev.to/nejremeslnici/styling-simple-form-forms-with-tailwind-4pel"&gt;there it is&lt;/a&gt;). And the best part of all: both unit tests and Lookbook work very well even for Simple Form tests and previews so we didn’t have to compromise anything important. Have a look at &lt;a href="https://gist.github.com/borama/321e733b6b75a6d6f3fcbe3569e99322" rel="noopener noreferrer"&gt;this gist&lt;/a&gt; for a basic example of such preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Overall, we are very happy with adding View Components to our project. Throughout the first few weeks, we built around ten universal components covering most of the needs for our admin pages and are quickly adding new pages in the new style using them. View Components seem like the missing piece that fit perfectly to our current view layer evolution needs.&lt;/p&gt;

&lt;p&gt;Since then, the new convention for choosing a pattern in the view layer has become as simple as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is it a form? Use Simple Form with our new helpers.&lt;/li&gt;
&lt;li&gt;Is it supposed to be reusable? Build a View Component and think well about the API and helpers.&lt;/li&gt;
&lt;li&gt;Is it critical? Build a View Component and ensure a good test coverage.&lt;/li&gt;
&lt;li&gt;Is there a non-trivial logic involved in the rendering? Build a View Component.&lt;/li&gt;
&lt;li&gt;None of the above? Choose freely among View Components, templates and helpers, whatever seems like a good fit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you for your attention, if you feel tempted to try View Components, good!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you don’t want to miss future posts like this, follow me here or &lt;a href="https://twitter.com/boramacz" rel="noopener noreferrer"&gt;on Twitter&lt;/a&gt;. Cheers!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>viewcomponent</category>
    </item>
    <item>
      <title>How to run a really long task from a Rails web request</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Tue, 19 Apr 2022 06:22:35 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/how-to-run-a-really-long-task-from-a-rails-web-request-47fb</link>
      <guid>https://dev.to/nejremeslnici/how-to-run-a-really-long-task-from-a-rails-web-request-47fb</guid>
      <description>&lt;p&gt;Recently, our management needed a way to export invoices in bulk. After the manager selects the first and last invoice for the batch in a web form, an asynchronous process should start that generates PDF files for the invoices, packs them into a zip file and sends the manager an email with a link to download the export. Now, generating the PDFs is slow, very slow. For larger batches involving hundreds or thousands of invoices, this process can easily take 10 or 15 minutes or even more.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;how do we trigger such a long-running process from a Rails request&lt;/strong&gt;? The first option that comes to mind is a background job run by some of the queuing back-ends such as &lt;a href="https://sidekiq.org/" rel="noopener noreferrer"&gt;Sidekiq&lt;/a&gt;, &lt;a href="https://github.com/resque/resque" rel="noopener noreferrer"&gt;Resque&lt;/a&gt; or &lt;a href="https://github.com/collectiveidea/delayed_job" rel="noopener noreferrer"&gt;DelayedJob&lt;/a&gt;, possibly governed by &lt;a href="https://guides.rubyonrails.org/active_job_basics.html" rel="noopener noreferrer"&gt;ActiveJob&lt;/a&gt;. While this would surely work, the problem with all these solutions is that they usually have a limited number of workers available on the server and we didn’t want to potentially block other important background tasks for so long.&lt;/p&gt;

&lt;p&gt;What we wanted instead was to run a new, separate process from the Rails request. Something like &lt;strong&gt;running a &lt;a href="https://guides.rubyonrails.org/command_line.html#custom-rake-tasks" rel="noopener noreferrer"&gt;Rake task&lt;/a&gt; but triggered by a web request&lt;/strong&gt;. In fact, we even had the bulk export already implemented as a Rake task, so what we actually wanted was to make this task accessible from our admin web interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  ”Forking“ the process
&lt;/h3&gt;

&lt;p&gt;The standard way on Unix-like systems to spawn a new process is to &lt;code&gt;fork&lt;/code&gt; it. In a Rails controller, &lt;code&gt;fork&lt;/code&gt;ing a rake task could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BulkInvoiceExportsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;fork&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bin/rails export_invoices FROM=20220001 TO=20220100 &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;
            &amp;gt;&amp;gt; /tmp/bulk_invoices_export.log 2&amp;gt;&amp;amp;1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="no"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s note a few things about the code inspired by &lt;a href="https://stackoverflow.com/a/2504528/1544012" rel="noopener noreferrer"&gt;this StackOverflow answer&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://ruby-doc.com/core/Process.html#method-c-fork" rel="noopener noreferrer"&gt;&lt;code&gt;Process#fork&lt;/code&gt;&lt;/a&gt; method splits the current process (its current thread) into two copies and the new child process runs the code in the block. &lt;/li&gt;
&lt;li&gt;The child process is then replaced with a newly loaded process using &lt;a href="https://ruby-doc.com/core/Process.html#method-c-exec" rel="noopener noreferrer"&gt;&lt;code&gt;Process#exec&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The final child process &lt;strong&gt;inherits all important settings&lt;/strong&gt; from the parent process, such as environment variables, open file descriptors or current working directory. This is why we can simply run &lt;code&gt;bin/rails&lt;/code&gt; without having to set up the correct ruby first (even when using a ruby version manager such as &lt;code&gt;rvm&lt;/code&gt;, &lt;code&gt;rbenv&lt;/code&gt; or &lt;code&gt;chruby&lt;/code&gt;) and without specifying an absolute path to the Rails binary. &lt;/li&gt;
&lt;li&gt;Because the code in the block uses shell redirection, the child Rails process is not executed directly but using a standard shell (usually &lt;code&gt;/bin/sh&lt;/code&gt;). Redirection allows us to debug and monitor what is going on in the rake task.&lt;/li&gt;
&lt;li&gt;By default, the operating system expects that the parent process is interested in the child process termination status. We are not – we want to run the rake task and forget about it, the task handles everything else such as sending the final email by itself. That’s why we call &lt;a href="https://ruby-doc.com/core/Process.html#method-c-detach" rel="noopener noreferrer"&gt;&lt;code&gt;Process#detach&lt;/code&gt;&lt;/a&gt; to let the OS know we don’t care about the child process and to prevent accumulating &lt;a href="https://en.wikipedia.org/wiki/Zombie_process" rel="noopener noreferrer"&gt;zombie processes&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ”Spawning“ the process
&lt;/h3&gt;

&lt;p&gt;If we wanted to make our code more portable (usable on Windows, for example), we would have to use &lt;a href="https://ruby-doc.com/core/Process.html#method-c-spawn" rel="noopener noreferrer"&gt;&lt;code&gt;Process#spawn&lt;/code&gt;&lt;/a&gt; instead of &lt;code&gt;fork&lt;/code&gt;, as &lt;a href="https://ruby-doc.com/core/Process.html#method-c-fork" rel="noopener noreferrer"&gt;suggested&lt;/a&gt; in the ruby documentation. The &lt;code&gt;spawn&lt;/code&gt; method also &lt;strong&gt;allows to fine-tune the child process environment&lt;/strong&gt;, file descriptors, limits or working directory.&lt;/p&gt;

&lt;p&gt;An almost equivalent way of scheduling the rake task using &lt;code&gt;spawn&lt;/code&gt; could be written this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BulkInvoiceExportsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bin/rails export_invoices FROM=20220001 TO=20220100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="sx"&gt;%i[out err]&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sx"&gt;%w[/tmp/bulk_invoices_export.log a]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Security caveats
&lt;/h3&gt;

&lt;p&gt;Please keep in mind that &lt;strong&gt;triggering such a long-running process from the controller is not safe&lt;/strong&gt;. In the previous examples, each request to the &lt;code&gt;create&lt;/code&gt; action of the controller leads to spawning one external Rails process, consuming perhaps a substantial portion of the CPU and memory resources and opening more connections to your database servers. This is a setup very vulnerable to &lt;a href="https://en.wikipedia.org/wiki/Denial-of-service_attack" rel="noopener noreferrer"&gt;DoS attacks&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The technique is probably OK only in very controlled environments such as in an internal admin area accessible to a limited number of people who know what they are doing and when the function is used only sparingly. If we wanted to make this rake task publicly accessible (as in a ”data take out“ function, for example), we would definitely resort to a &lt;strong&gt;real queuing system&lt;/strong&gt; such as those mentioned above or perhaps a queuing daemon on the system level (e.g. &lt;a href="https://linux.die.net/man/8/atd" rel="noopener noreferrer"&gt;&lt;code&gt;atd&lt;/code&gt;&lt;/a&gt; which can hold the tasks based on the server load).&lt;/p&gt;

&lt;p&gt;Anyway, for our use case, directly forking the rake task from the controller was the most pragmatic way to go and we are happy about the result.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you don’t want to miss future posts like this, follow me here or on &lt;a href="https://twitter.com/boramacz" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;. Cheers!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>linux</category>
      <category>rake</category>
    </item>
    <item>
      <title>Track and fix excessive Active Record instantiation</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Fri, 21 Jan 2022 12:45:34 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/track-and-fix-excessive-active-record-instantiation-2902</link>
      <guid>https://dev.to/nejremeslnici/track-and-fix-excessive-active-record-instantiation-2902</guid>
      <description>&lt;p&gt;When working with a Rails back-end code, grabbing too many records from the database is discouraged, and &lt;strong&gt;for very good reasons&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your database server has to work hard to find the records,&lt;/li&gt;
&lt;li&gt;a lot of data needs to be transferred from the database to your app,&lt;/li&gt;
&lt;li&gt;and — last but not least — &lt;strong&gt;Active Record instantiates too many objects&lt;/strong&gt;, leading to a memory bloat in your application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this slows down the response time of your web app for the given request. But even worse, it also &lt;strong&gt;affects all future requests&lt;/strong&gt; because the memory, newly allocated by the Rails process, usually becomes fragmented and hard to release back, unless you apply &lt;a href="https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/" rel="noopener noreferrer"&gt;special tweaks&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  OK but is this a real issue?
&lt;/h2&gt;

&lt;p&gt;Even though limiting the db queries is rather a basic rule, &lt;a href="https://guides.rubyonrails.org/active_record_querying.html#retrieving-multiple-objects-in-batches" rel="noopener noreferrer"&gt;mentioned&lt;/a&gt; early in the Rails Guides, we noticed that a huge SELECT still occasionally slips into our production code. How come? Turns out, &lt;strong&gt;it is surprisingly easy&lt;/strong&gt; for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;developers usually work with a small dev database and &lt;strong&gt;forget about the scale of production data&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;even though our dev team actually works with a large subset of  production data, it’s too easy to &lt;strong&gt;forget to test a worse case scenario&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;Active Record syntax is very succinct&lt;/strong&gt; and lets an unsuspecting developer build huge JOINs very easily; for example, this innocent-looking query: &lt;code&gt;User.recent_customers.eager_load(orders: :order_logs)&lt;/code&gt; can suddenly cause a gigantic data load when a power user with many orders falls into the &lt;code&gt;recent_customers&lt;/code&gt; scope,&lt;/li&gt;
&lt;li&gt;and, sometimes devs simply forget to &lt;strong&gt;have large data processed in batches&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We first became aware of the issue when we profiled some exceptionally slow requests in DataDog and noticed &lt;strong&gt;large Active Record instantiation spans&lt;/strong&gt;, such as this one:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffgwy6muyd5f10mo1ptd6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffgwy6muyd5f10mo1ptd6.png" alt="AR instantiation span" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The trace shows that in this particular request, Active Record instantiated over 2,100 &lt;code&gt;ZipCode&lt;/code&gt; model objects which delayed the response by ~160 ms (and this even excludes the time needed to run the query and transfer the results to the Rails app). That’s insane! We don’t think we need information about two thousand zip codes anywhere on our site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracking the problem in production
&lt;/h2&gt;

&lt;p&gt;After finding and fixing a few places, we decided that we needed a continuous monitoring of this problem in production. We could have probably done this in DataDog itself via a &lt;a href="https://docs.datadoghq.com/tracing/generate_metrics/" rel="noopener noreferrer"&gt;custom metric generated&lt;/a&gt; from the Indexed APM spans. Other APM systems may have different options, such as &lt;a href="https://book.scoutapm.com/memory-bloat.html#activerecord-rendering-a-large-number-of-objects" rel="noopener noreferrer"&gt;ScoutAPM’s memory bloat detection&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But in the end, we chose to build a custom solution that we could more easily send to our reporting system instead. Because, it turns out, &lt;strong&gt;tracing the instantiations is very well supported using &lt;a href="https://guides.rubyonrails.org/active_support_instrumentation.html" rel="noopener noreferrer"&gt;Rails instrumentation&lt;/a&gt;&lt;/strong&gt;. Each time Active Record instantiates objects after retrieving data from the database, it generates the &lt;strong&gt;&lt;a href="https://guides.rubyonrails.org/active_support_instrumentation.html#instantiation-active-record" rel="noopener noreferrer"&gt;&lt;code&gt;instantiation.active_record&lt;/code&gt; event&lt;/a&gt;&lt;/strong&gt; which we can hook into.&lt;/p&gt;

&lt;p&gt;Below is the complete code for a custom &lt;a href="https://api.rubyonrails.org/classes/ActiveSupport/LogSubscriber.html" rel="noopener noreferrer"&gt;log subscriber&lt;/a&gt; (i.e. a class to consume the given instrumentation events and log them) that processes "instantiation" events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/subscribers/active_record_instantiation_subscriber.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"active_support/log_subscriber"&lt;/span&gt;

&lt;span class="c1"&gt;# Send Rollbar error when ActiveRecord instantiates too many objects.&lt;/span&gt;
&lt;span class="c1"&gt;# Log all AR instantiation in dev log.&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActiveRecordInstantiationSubscriber&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LogSubscriber&lt;/span&gt;
  &lt;span class="no"&gt;MAX_TOLERATED_RECORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;
  &lt;span class="no"&gt;MAX_TOLERATED_DURATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="c1"&gt;# in milliseconds&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;instantiation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test?&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;
    &lt;span class="n"&gt;excessive_load&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:record_count&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;MAX_TOLERATED_RECORDS&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;MAX_TOLERATED_DURATION&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;development?&lt;/span&gt;
      &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"  Instantiated &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:record_count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; records 
                 of class &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:class_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; 
                 in &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duration&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ms"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;squish&lt;/span&gt;
      &lt;span class="n"&gt;excessive_load&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;excessive_load&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;production?&lt;/span&gt;
      &lt;span class="no"&gt;Rollbar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Too many ActiveRecord objects instantiated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="ss"&gt;record_count: &lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:record_count&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="ss"&gt;class_name: &lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:class_name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="ss"&gt;duration: &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="ss"&gt;source_code: &lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backtrace_cleaner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The location of the log subscriber file is arbitrary given that it is set up from a Rails initializer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"./app/subscribers/active_record_instantiation_subscriber"&lt;/span&gt;

&lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"instantiation.active_record"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                                       &lt;span class="no"&gt;ActiveRecordInstantiationSubscriber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In essence, the subscriber serves two purposes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;It &lt;strong&gt;logs a message to the Rails log&lt;/strong&gt; about each instantiation in development environment, so that the developer can see potential problems with creating too many model objects as early as possible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu15d3q8eo8gzd7p85l7q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu15d3q8eo8gzd7p85l7q.png" alt="Instantiation log messages" width="800" height="87"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In production, the code &lt;strong&gt;reports a custom error&lt;/strong&gt; to our tracking system &lt;strong&gt;if the number of instantiated records is especially high&lt;/strong&gt; (above 2000, as configured in the &lt;code&gt;MAX_TOLERATED_RECORDS&lt;/code&gt; constant) &lt;strong&gt;or the instantiation too slow&lt;/strong&gt; (above 200 ms, see the &lt;code&gt;MAX_TOLERATED_DURATION&lt;/code&gt; constant). This error message includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the number of instantiated objects,&lt;/li&gt;
&lt;li&gt;the time the instantiation took (in ms),&lt;/li&gt;
&lt;li&gt;the instantiated class,&lt;/li&gt;
&lt;li&gt;and a stack trace pointing to the proper place in code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60lryrlamog0lqmo6n3t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60lryrlamog0lqmo6n3t.png" alt="Instantiation reporting in Rollbar" width="800" height="208"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This way, we can continuously monitor excessive instantiation in our tracking system from the real traffic. &lt;/p&gt;

&lt;h2&gt;
  
  
  How to fix the issues found
&lt;/h2&gt;

&lt;p&gt;OK, excessive Active Record instantiation warnings successfully fill your reporting system so… what next? The general effort here is to &lt;strong&gt;try to decrease the amount of data&lt;/strong&gt; loaded from the database. The specific way to do that will depend on why the data is loaded in the first place. Let’s have a look on some of the options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add a &lt;strong&gt;&lt;a href="https://guides.rubyonrails.org/active_record_querying.html#limit-and-offset" rel="noopener noreferrer"&gt;&lt;code&gt;limit&lt;/code&gt;&lt;/a&gt; clause&lt;/strong&gt; to your queries wherever it makes sense. Use pagination for data listings and index pages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;code&gt;select&lt;/code&gt; or even better, &lt;code&gt;pluck&lt;/code&gt;: &lt;strong&gt;the &lt;a href="https://guides.rubyonrails.org/active_record_querying.html#selecting-specific-fields" rel="noopener noreferrer"&gt;&lt;code&gt;select&lt;/code&gt;&lt;/a&gt; method&lt;/strong&gt; still leads to model objects instantiation but limits instantiating attributes only to those explicitly listed. &lt;strong&gt;The &lt;a href="https://guides.rubyonrails.org/active_record_querying.html#pluck" rel="noopener noreferrer"&gt;&lt;code&gt;pluck&lt;/code&gt;&lt;/a&gt; method&lt;/strong&gt; skips model objects creation altogether and returns a simple array of the results data, saving a lot of memory and CPU cycles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In case you only look for an &lt;strong&gt;aggregate value&lt;/strong&gt;, use &lt;a href="https://guides.rubyonrails.org/active_record_querying.html#calculations" rel="noopener noreferrer"&gt;calculation methods&lt;/a&gt; instead of grabbing all records and aggregating them in ruby.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you really need to process a lot of records (e.g. in a rake task), use &lt;strong&gt;methods for &lt;a href="https://guides.rubyonrails.org/active_record_querying.html#retrieving-multiple-objects-in-batches" rel="noopener noreferrer"&gt;loading the data in batches&lt;/a&gt;&lt;/strong&gt;. The &lt;a href="https://api.rubyonrails.org/classes/ActiveRecord/Batches.html#method-i-in_batches" rel="noopener noreferrer"&gt;&lt;code&gt;in_batches&lt;/code&gt;&lt;/a&gt; method can even be combined with &lt;code&gt;pluck&lt;/code&gt;, for example. Or better yet, use &lt;strong&gt;&lt;a href="https://guides.rubyonrails.org/active_record_basics.html#update" rel="noopener noreferrer"&gt;mass update&lt;/a&gt;&lt;/strong&gt; if appropriate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Try rewriting complex (especially nested) &lt;strong&gt;eager load queries&lt;/strong&gt; into multiple simpler queries that you can control more precisely.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the way, most of these tips are more thoroughly explained in the &lt;a href="https://www.railsspeed.com/" rel="noopener noreferrer"&gt;&lt;em&gt;Complete Guide to Rails Performance&lt;/em&gt;&lt;/a&gt; by Nate Berkopec, a book we recommend with love. So, may your memory be free!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you don’t want to miss future posts like this, follow me here or on &lt;a href="https://twitter.com/boramacz" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;. Cheers!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>activerecord</category>
      <category>rails</category>
      <category>performance</category>
      <category>database</category>
    </item>
    <item>
      <title>How to get rid of Internet Explorer faster but carefully</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Mon, 17 Jan 2022 10:41:45 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/how-to-get-rid-of-internet-explorer-faster-but-carefully-40bg</link>
      <guid>https://dev.to/nejremeslnici/how-to-get-rid-of-internet-explorer-faster-but-carefully-40bg</guid>
      <description>&lt;p&gt;Many web developers eagerly look forward to &lt;a href="https://docs.microsoft.com/en-us/lifecycle/faq/internet-explorer-microsoft-edge#what-is-the-lifecycle-policy-for-internet-explorer" rel="noopener noreferrer"&gt;Internet Explorer End of Life&lt;/a&gt;, scheduled on June 15th, 2022. We definitely do as well! Of course, this EOL date doesn’t mean that all IE users will be gone by then. But it will be OK to take IE into consideration even less when updating the site.&lt;/p&gt;

&lt;p&gt;Nevertheless, we’ve already set out that journey about a year ago:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we show an overlay popup to all IE users, asking them to switch:
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6nixm67g3y1ulwoanj7p.png" alt="Internet Explorer popup" width="800" height="587"&gt;
&lt;/li&gt;
&lt;li&gt;throughout last year, we upgraded to &lt;a href="https://tailwindcss.com/docs/upgrading-to-v2#support-for-ie-11-has-been-dropped" rel="noopener noreferrer"&gt;Tailwind 2&lt;/a&gt; and &lt;a href="https://github.com/hotwired/stimulus/releases/tag/v3.0.0-beta.1" rel="noopener noreferrer"&gt;Stimulus 3&lt;/a&gt; even though both frameworks drop official support for IE,&lt;/li&gt;
&lt;li&gt;we still use a few IE-related polyfills that solve the biggest issues, but only for the most critical use cases,&lt;/li&gt;
&lt;li&gt;and we generally don’t test newly built features in IE any more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(For more details about our previous IE-related measures, see our &lt;a href="https://dev.to/nejremeslnici/upgrade-to-stimulus-3-say-bye-to-ie11-and-celebrate-b7g"&gt;last post&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Since then, we saw a clear trend: relative IE usage dropped from 1.7% in January 2021 down to ~0.5% in early November. Still, we were thinking: could we help this trend even more? Our site is a marketplace connecting customers with various craftspeople. We observed that almost all IE visits were from our customers, especially first-time visitors. We knew our customers were often not very tech-savvy and we couldn’t expect them to migrate to a newer browser unless the experience was really seamless. So, could we still help them make the switch somehow? Turns out we could!&lt;/p&gt;

&lt;h2&gt;
  
  
  The ”Need Microsoft Edge List“
&lt;/h2&gt;

&lt;p&gt;A nice migration process is actually &lt;strong&gt;offered by Microsoft itself&lt;/strong&gt; although the feature can be harder to find. Microsoft keeps an official list of websites that want their users to switch from IE to Edge. Windows systems periodically download this file and take care of the migration process on the sites listed there.&lt;/p&gt;

&lt;p&gt;The list is called &lt;strong&gt;&lt;a href="https://docs.microsoft.com/en-us/microsoft-edge/web-platform/ie-to-microsoft-edge-redirection" rel="noopener noreferrer"&gt;IE Compatibility List&lt;/a&gt;&lt;/strong&gt; and the process to get there is both surprising and lovely — because very old-school and manual 😍:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;as a prerequisite, you need to show a message to your IE visitors on your web, asking them to switch,&lt;/li&gt;
&lt;li&gt;then you can officially &lt;a href="https://docs.microsoft.com/en-us/microsoft-edge/web-platform/ie-to-microsoft-edge-redirection#request-an-update-to-the-ie-compatibility-list" rel="noopener noreferrer"&gt;request&lt;/a&gt; Microsoft to add your site to the list; you do that by sending a free-form email including the required information,&lt;/li&gt;
&lt;li&gt;a human replies (in our case it was Kelly, hi! 👋) to confirm your request or clarify details,&lt;/li&gt;
&lt;li&gt;the same person takes care of you during the whole time and informs you about the progress,&lt;/li&gt;
&lt;li&gt;about a week later, your site is added to the &lt;a href="https://edge.microsoft.com/neededge/v1" rel="noopener noreferrer"&gt;list itself&lt;/a&gt; and starts being recognized by Windows immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From now on, when a user visits your site with IE, he or she is &lt;strong&gt;redirected to the Edge browser&lt;/strong&gt; and a localized explanation pops up. Most importantly, all bookmarks, settings, cookies and passwords are &lt;strong&gt;automatically transferred&lt;/strong&gt; so everything works just the same as in IE (but better).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8mwpta2lpr39pviclld4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8mwpta2lpr39pviclld4.png" alt="IE to Edge migration" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The effect
&lt;/h2&gt;

&lt;p&gt;Of course we were very curious how effective this IE compatibility list was. Our analytics data shows that the number of IE visits &lt;strong&gt;dropped to half of the previous numbers&lt;/strong&gt; within a few days after the addition to the list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ez8k8gwj087smvqc57j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ez8k8gwj087smvqc57j.png" alt="Effect of the IE compatibility list" width="800" height="107"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s great! Using the official list, we were able to migrate even many first-time visitors and the relative proportion of IE visits dropped to ~0.2%. Still, as can be seen in the chart, there are a few IE visitors left, making around one hundred visits per week. We guess these are users of very old and long-time-not-updated Windows systems who must experience increasingly serious problems accessing the internet overall… We are sorry about that but we believe we did all we could. So, good luck to all and see you in better browser times!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ie</category>
      <category>browser</category>
      <category>edge</category>
    </item>
    <item>
      <title>Upgrade to Stimulus 3, say bye to IE11, and celebrate 🎉</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Tue, 19 Oct 2021 20:53:23 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/upgrade-to-stimulus-3-say-bye-to-ie11-and-celebrate-b7g</link>
      <guid>https://dev.to/nejremeslnici/upgrade-to-stimulus-3-say-bye-to-ie11-and-celebrate-b7g</guid>
      <description>&lt;p&gt;Most of our application JavaScript code is already written as &lt;a href="https://stimulus.hotwired.dev" rel="noopener noreferrer"&gt;Stimulus&lt;/a&gt; controllers, the rest being slowly assimilated or removed. Recently, we wanted to upgrade the Stimulus framework to &lt;strong&gt;version 3&lt;/strong&gt; to gain access to the new cool features, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/hotwired/stimulus/pull/354" rel="noopener noreferrer"&gt;debug mode&lt;/a&gt;&lt;/strong&gt; that greatly helps understanding what exactly your controllers are doing and why,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://stimulus.hotwired.dev/reference/controllers#cross-controller-coordination-with-events" rel="noopener noreferrer"&gt;dispatching events among controllers&lt;/a&gt;&lt;/strong&gt; - previously, communication between controllers required various ”hacks“, not any more as it is now official and straightforward,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://stimulus.hotwired.dev/reference/actions#action-parameters" rel="noopener noreferrer"&gt;action parameters&lt;/a&gt;&lt;/strong&gt; for even more flexibility when calling controller actions,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://stimulus.hotwired.dev/reference/values#default-values" rel="noopener noreferrer"&gt;default values&lt;/a&gt;&lt;/strong&gt; no more need to be specified in HTML , they can reside in the controller itself,&lt;/li&gt;
&lt;li&gt;and &lt;a href="https://world.hey.com/hotwired/stimulus-3-c438d432" rel="noopener noreferrer"&gt;more&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we started by fixing all deprecation warnings, then updated the Stimulus package and all imports to the &lt;a href="https://www.npmjs.com/package/@hotwired/stimulus" rel="noopener noreferrer"&gt;new package name&lt;/a&gt;. Since we are still using Webpacker (not for long, you bet…), we added the – now separate – &lt;a href="https://www.npmjs.com/package/@hotwired/stimulus-webpack-helpers" rel="noopener noreferrer"&gt;stimulus-webpack-helpers package&lt;/a&gt; and updated the &lt;a href="https://stimulus.hotwired.dev/handbook/installing#using-webpack-helpers" rel="noopener noreferrer"&gt;controllers initialization&lt;/a&gt;. All easy and clear, right?&lt;/p&gt;

&lt;p&gt;Well, not so fast. We did not read the &lt;em&gt;whole&lt;/em&gt; release notes properly enough and did not notice at first that &lt;strong&gt;&lt;a href="https://stimulus.hotwired.dev/handbook/installing#browser-support" rel="noopener noreferrer"&gt;Stimulus 3 drops support for IE11&lt;/a&gt;&lt;/strong&gt;. This made us stop for a while and do some browser usage analyses.&lt;/p&gt;

&lt;h3&gt;
  
  
  IE11 measures
&lt;/h3&gt;

&lt;p&gt;Luckily, we’ve had most of the work done from almost a year ago, when we &lt;a href="https://dev.to/nejremeslnici/migrating-tachyons-to-tailwind-css-part-i-ich"&gt;adopted Tailwind&lt;/a&gt; in our project. Tailwind 2.0 also dropped official support for IE11 and we made an important decision at that time: while the IE11 usage numbers were small, we could not afford making our web totally unusable for these users. So we employed a few polyfills, added a few styling fixes specific to IE11 so that our web was still – somehow – accessible via this old browser. Also, we put up an alert that tried to persuade people to switch. And we waited… until today.&lt;/p&gt;

&lt;p&gt;So now we looked at the numbers again and found that all seemed very good! The usage numbers, both absolute and relative, decreased steadily, our providers didn’t use IE almost at all, our customers a bit more but still negligibly. Who knows whether our pop up, Microsoft or a general innovation pressure contributed to the effect, the important thing was that we were ready to make the next step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fclp3lqo97w4x8nf6qucm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fclp3lqo97w4x8nf6qucm.png" alt="IE11 analytics stats" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, we decided to continue freely with the Stimulus upgrade and we also added our site to the &lt;strong&gt;&lt;a href="https://docs.microsoft.com/en-us/microsoft-edge/web-platform/ie-to-microsoft-edge-redirection" rel="noopener noreferrer"&gt;Need Microsoft Edge list&lt;/a&gt;&lt;/strong&gt;. Being listed here will automatically redirect IE11 users to Edge when they visit our site.&lt;/p&gt;

&lt;h3&gt;
  
  
  ”Not IE 11“
&lt;/h3&gt;

&lt;p&gt;To our surprise, we hit a weird and at first confusing error during  the Stimulus upgrade process: &lt;em&gt;Uncaught (in promise) TypeError: class constructors must be invoked with 'new'"&lt;/em&gt;. No controllers worked at all. We double-, triple-checked the configs and all seemed OK. The solution clicked after we read &lt;a href="https://stackoverflow.com/a/51860850/1544012" rel="noopener noreferrer"&gt;this response&lt;/a&gt; on Stack Overflow. Our JS code was transpiled to ES5 but Stimulus itself now &lt;a href="https://github.com/hotwired/stimulus/releases/tag/v3.0.0-beta.1" rel="noopener noreferrer"&gt;uses ES6&lt;/a&gt; as the compile target. So our ES5 controllers could not extend ES6 Stimulus classes.&lt;/p&gt;

&lt;p&gt;We found the cause in the &lt;code&gt;browserslist&lt;/code&gt; section of our &lt;code&gt;package.json&lt;/code&gt; file. This setting is &lt;a href="https://babeljs.io/docs/en/babel-preset-env#browserslist-integration" rel="noopener noreferrer"&gt;used by Babel&lt;/a&gt; to transpile various modern JS features to their safer alternatives according to &lt;a href="https://github.com/browserslist/browserslist" rel="noopener noreferrer"&gt;browsers usage&lt;/a&gt;, and we needed to explicitly &lt;strong&gt;exclude IE11 support&lt;/strong&gt; to compile our JS code to ES6 and the error disappeared.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  "browserslist": [
&lt;span class="gd"&gt;-    "defaults"
&lt;/span&gt;&lt;span class="gi"&gt;+    "defaults",
+    "not IE 11"
&lt;/span&gt;  ],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the way, targeting our JavaScript code to ES6 alone &lt;strong&gt;decreased our production bundle size by about 15%&lt;/strong&gt; (unzipped). Nice!&lt;/p&gt;

&lt;p&gt;We also quickly checked with &lt;a href="https://caniuse.com/es6" rel="noopener noreferrer"&gt;Can I Use&lt;/a&gt; that we are OK with ES6 considering our browser usage pattern, and yes, sure:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8gnr7r3rghfzc6azhqs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8gnr7r3rghfzc6azhqs.png" alt="Can I use ES6 for our site" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, as we recently &lt;a href="https://dev.to/nejremeslnici/migrating-selenium-system-tests-to-cuprite-42ah#ajax-fetch-issues-due-to-cuprite-being-too-fast"&gt;added&lt;/a&gt; the &lt;a href="https://github.com/stimulus-use/stimulus-use" rel="noopener noreferrer"&gt;Stimulus-Use library&lt;/a&gt; to our project, we made sure to upgrade it to current beta which supports Stimulus 3. &lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Our tests show that everything works nicely under Stimulus 3. We enjoy the lovely debug mode and other new features. Stimulus has grown to a mature framework, perfectly usable in &lt;a href="https://twitter.com/jaredcwhite/status/1450281146139348995" rel="noopener noreferrer"&gt;HTML-first application&lt;/a&gt; stacks.&lt;/p&gt;

&lt;p&gt;While for the few remaining IE11 users it will be increasingly difficult to use our site, we are quite OK with it as we have tried to reduce the harm before and continue to do so to some (lesser and lesser) extent. You can’t stop progress. Bye IE! 👋&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you like reading stuff like this, you might want to &lt;a href="https://twitter.com/boramacz" rel="noopener noreferrer"&gt;follow us&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stimulus</category>
      <category>rails</category>
      <category>javascript</category>
      <category>ie11</category>
    </item>
    <item>
      <title>Migrating Selenium system tests to Cuprite</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Mon, 04 Oct 2021 20:58:51 +0000</pubDate>
      <link>https://dev.to/nejremeslnici/migrating-selenium-system-tests-to-cuprite-42ah</link>
      <guid>https://dev.to/nejremeslnici/migrating-selenium-system-tests-to-cuprite-42ah</guid>
      <description>&lt;p&gt;In our project, we’ve been running system tests (then called rather "Feature tests") since around 2016. &lt;a href="https://guides.rubyonrails.org/testing.html#system-testing" rel="noopener noreferrer"&gt;System tests&lt;/a&gt; use a real browser in the background and test all layers of a Rails application at once: from the database all the way up to the nuances of JavaScript loaded together with the web pages. Back then, we wrote our system tests using &lt;a href="https://github.com/teamcapybara/capybara" rel="noopener noreferrer"&gt;Capybara&lt;/a&gt; with &lt;a href="https://github.com/teampoltergeist/poltergeist" rel="noopener noreferrer"&gt;Poltergeist&lt;/a&gt;, a driver that ran a headless &lt;a href="https://phantomjs.org/" rel="noopener noreferrer"&gt;Phantom JS&lt;/a&gt; browser. Since this browser  stopped being actively developed, we migrated our test suite to the &lt;a href="https://github.com/SeleniumHQ/selenium" rel="noopener noreferrer"&gt;Selenium / Webdriver&lt;/a&gt; wrapper around Chrome browser around ~2018. Chrome was itself fine for tests automation but the Selenium API was quite limited and we had to rewrite several Poltergeist features using 3rd party gems and tools.&lt;/p&gt;

&lt;p&gt;That is why we were happy to find out that a new ruby testing driver approach is being developed. &lt;strong&gt;It is called &lt;a href="https://github.com/rubycdp/cuprite" rel="noopener noreferrer"&gt;Cuprite&lt;/a&gt;&lt;/strong&gt;, it runs the &lt;a href="https://github.com/rubycdp/ferrum" rel="noopener noreferrer"&gt;Ferrum library&lt;/a&gt; under the hood which, in turn, is an API that directly instruments the Chrome browser using the &lt;a href="https://chromedevtools.github.io/devtools-protocol/" rel="noopener noreferrer"&gt;Chrome DevTools Protocol&lt;/a&gt; (CDP). About a week ago, we finally made a serious attempt to make our system test suite run on Cuprite, with especially two questions in our minds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;would the tests run faster?&lt;/li&gt;
&lt;li&gt;would the Cuprite API be easier to use?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a little spoiler we are glad to say that &lt;strong&gt;both points turned true for us and we kind of fell in love with these wonderful pieces of software, Cuprite and Ferrum&lt;/strong&gt;. If you’d like to hear more details, read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration
&lt;/h2&gt;

&lt;p&gt;All important parts of the basic installation process are shown in the &lt;a href="https://github.com/rubycdp/cuprite#install" rel="noopener noreferrer"&gt;Cuprite README&lt;/a&gt; and in the &lt;a href="https://github.com/rubycdp/ferrum#customization" rel="noopener noreferrer"&gt;customization section&lt;/a&gt; of the Ferrum README. Great resources and tips can also be found in &lt;a href="https://evilmartians.com/chronicles/system-of-a-test-setting-up-end-to-end-rails-testing" rel="noopener noreferrer"&gt;this article by Evil Martians&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The very lovely thing about Cuprite is that &lt;strong&gt;it very much resembles the old but good Poltergeist API&lt;/strong&gt;. The CDP protocol is much more versatile than Selenium driver and thus Cuprite allows e.g. the following things which were hard or even impossible with Selenium:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/rubycdp/cuprite#url-blacklisting--whitelisting" rel="noopener noreferrer"&gt;blocking / allowing requests&lt;/a&gt; to external domains and URLs,&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/rubycdp/cuprite#manipulating-cookies" rel="noopener noreferrer"&gt;setting cookies&lt;/a&gt;, even before visiting the given page,&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/rubycdp/cuprite#request-headers" rel="noopener noreferrer"&gt;setting request headers&lt;/a&gt;,&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/rubycdp/cuprite#debugging" rel="noopener noreferrer"&gt;opening the Chrome DevTools&lt;/a&gt; with a single line of code,&lt;/li&gt;
&lt;li&gt;inspecting and/or logging all communication between the test and the Chrome browser (all CDP messages).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a lot of these features, we previously had to adopt various 3rd party gems, such as the &lt;a href="https://github.com/oesmith/puffing-billy" rel="noopener noreferrer"&gt;Puffing Billy&lt;/a&gt; proxy (for blocking domains), the &lt;a href="https://github.com/titusfortner/webdrivers" rel="noopener noreferrer"&gt;webdrivers gem&lt;/a&gt; (for auto-updating the Chrome drivers), etc. and although they certainly did a good job for us, now we were able to finally rip them off the project completely:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtf8n81pu42fmfbxontz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtf8n81pu42fmfbxontz.png" alt="Migration of Cuprite-related gems in the Gemfile" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cuprite speed-up is real and can be helped even more
&lt;/h2&gt;

&lt;p&gt;OK, let’s talk numbers. We have ~140 system tests in our project, covering the most important use cases in our web application. Several of the test cases go through some very complex scenarios, slowing down the whole test suite run time considerably. Overall, our system tests used to run approximately 12 minutes on Selenium, while the same suite finishes in ~7 minutes under Cuprite. &lt;strong&gt;That is approximately a 40% speed-up 😲!&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9u3ob49wgngiobpduj2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9u3ob49wgngiobpduj2.png" alt="Cuprite tests are ~40% faster" width="800" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not all of this can be attributed to Cuprite speed alone though as, in the end, we configured the new driver slightly differently. For example we used whitelisting of specific domains instead of blocking the others as we did on Selenium. It is now a much stronger and stricter setup that probably blocks more domains than before, speeding up the page loads. Still, &lt;strong&gt;the speed up was clear&lt;/strong&gt; and apparent since the first run of Cuprite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Faster sign-in in tests
&lt;/h2&gt;

&lt;p&gt;And we added a few more tricks. We rewrote our sign-in helper method in a more efficient way. This was possible because &lt;strong&gt;Cuprite allows setting a cookie&lt;/strong&gt; (i.e. the session cookie) &lt;strong&gt;even prior to visiting a page&lt;/strong&gt;, unlike Selenium. Thus, we could manually generate a session token and store it both to our back-end session store as well as the session cookie. We just needed to make sure the session cookie had the same options as the real session cookie.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login_via_cookie_as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;public_session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"session_test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                         &lt;span class="n"&gt;public_session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                         &lt;span class="ss"&gt;domain: &lt;/span&gt;&lt;span class="s2"&gt;".example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                         &lt;span class="ss"&gt;sameSite: :Lax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                         &lt;span class="ss"&gt;secure: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                         &lt;span class="ss"&gt;httpOnly: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;private_session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Session&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SessionId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;public_session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                               &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;private_id&lt;/span&gt;
  &lt;span class="no"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;session_id: &lt;/span&gt;&lt;span class="n"&gt;private_session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                  &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;user_id: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lead to another noticeable speed-up of the tests suite run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test fixes needed
&lt;/h2&gt;

&lt;p&gt;Initially, about 30 tests (~20%) that were OK under Selenium, failed under Cuprite. Some of the failures were easy to fix, others were more puzzling. Overall, we came to a feeling that the &lt;strong&gt;Cuprite driver was less forgiving than Selenium&lt;/strong&gt;, forcing us to be a bit more precise in our tests. &lt;/p&gt;

&lt;p&gt;For example, we filled a value of &lt;code&gt;"10 000"&lt;/code&gt; into a number input field in a test (note the whitespace). This works without issues inside Selenium but fails under Cuprite. Now, let’s show a few more types of fixes that we had to deal with.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scrolling and clicking issues
&lt;/h3&gt;

&lt;p&gt;A lot of tests failed because Cuprite tried to click an element that was covered by another element on the page. Cuprite seems to &lt;strong&gt;scroll and center the element a bit less&lt;/strong&gt; (compared to Selenium) prior to clicking it.&lt;/p&gt;

&lt;p&gt;Here is a typical example – the test was trying to click on the button covered by the sticky header, as we could easily see by saving the &lt;a href="https://github.com/teamcapybara/capybara#debugging" rel="noopener noreferrer"&gt;page screenshot&lt;/a&gt; upon test failure:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcm1d1eo2rxdot30fjpi4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcm1d1eo2rxdot30fjpi4.png" alt="Click failure due to covered button" width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The failure log message would show a &lt;code&gt;Capybara::Cuprite::MouseEventFailed&lt;/code&gt; error with details about which element was at the same position as the clicked-on element.&lt;/p&gt;

&lt;p&gt;We had to manually scroll to an element in a few tests. To further mitigate this issue in a more generic way, we also overloaded the &lt;code&gt;click_button&lt;/code&gt; method from Capybara &lt;strong&gt;to scroll and center the button on the page before clicking it&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;click_button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scroll_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;align: :center&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  File uploads needed absolute file paths
&lt;/h3&gt;

&lt;p&gt;We use &lt;a href="https://github.com/dropzone/dropzone" rel="noopener noreferrer"&gt;Dropzone JS&lt;/a&gt; to support uploading files. Under Cuprite, uploading stopped working and an &lt;strong&gt;&lt;code&gt;ERR_ACCESS_DENIED&lt;/code&gt; error&lt;/strong&gt; was shown in the JavaScript console each time a test attempted to upload a file.&lt;/p&gt;

&lt;p&gt;It took a while to debug this but in the end the issue was quite prosaic – Chrome needed absolute paths when simulating the file upload in the test. So, the fix was just along the following lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- attach_file("file-input", 
-             "./app/assets/images/backgrounds/brown-wood-bg_512.png")
&lt;/span&gt;&lt;span class="gi"&gt;+ attach_file("file-input", 
+             Rails.root.join("app/assets/images/backgrounds/brown-wood-bg_512.png").to_s)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are not sure if this issue is only when using Dropzone or rather related to generic file uploads in system tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  AJAX / Fetch issues due to Cuprite being ”too fast“
&lt;/h3&gt;

&lt;p&gt;Surprisingly, some more tests started failing randomly. Soon it turned out that all of them deal somehow with JavaScript sending requests to the back-end via AJAX or Fetch. Again, these tests were rather stable under Selenium and – as we investigated – the issue was that under some circumstances the Cuprite driver generated multiple Fetch requests and sent them too fast.&lt;/p&gt;

&lt;p&gt;For example, we have a few ”live search“ fields, backed by back-end Fetch requests, on some pages. The live search function was usually triggered by the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/keyup_event" rel="noopener noreferrer"&gt;keyup event&lt;/a&gt; and Cuprite was such a fast typewriter that it frequently sent multiple requests almost at once. If some of the responses got a bit late or out of sync, the front-end JavaScript code began hitting issues. We solved this by &lt;strong&gt;adopting a technique called &lt;a href="https://www.freecodecamp.org/news/javascript-debounce-example/" rel="noopener noreferrer"&gt;debouncing&lt;/a&gt;&lt;/strong&gt; and, frankly, we should have done this since the beginning. By the way, we used the &lt;a href="https://github.com/stimulus-use/stimulus-use/blob/main/docs/use-debounce.md" rel="noopener noreferrer"&gt;&lt;code&gt;useDebounce&lt;/code&gt; module&lt;/a&gt; from the marvelous &lt;a href="https://github.com/stimulus-use/stimulus-use" rel="noopener noreferrer"&gt;Stimulus-use library&lt;/a&gt; to achieve this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Cuprite logger
&lt;/h2&gt;

&lt;p&gt;A lot of our migration effort went to developing a logger for some of the events that Cuprite / Ferrum handles when talking to the browser. In general, &lt;strong&gt;Cuprite offers a stream of all CDP messages&lt;/strong&gt; exchanged between the driver and the browser. To use it, one has to filter out the events that he or she is interested in.&lt;/p&gt;

&lt;p&gt;We used this feature to track two kinds of data in the log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript errors printed in the JS console in the Chrome browser,&lt;/li&gt;
&lt;li&gt;details about the requests and responses sent to/from the server as this information sometimes greatly helps debugging tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Usually, &lt;strong&gt;we let the test fail when a JavaScript error occurs&lt;/strong&gt;. Ferrum has a &lt;a href="https://github.com/rubycdp/ferrum#customization" rel="noopener noreferrer"&gt;&lt;code&gt;js_errors&lt;/code&gt; option&lt;/a&gt; in the driver configuration to do just that.  It works nice but we used a custom solution instead because we wanted some of the JavaScript errors to actually be ignored and we didn’t want a test failure then. In the end, we made a helper class (similar to &lt;a href="https://github.com/rubycdp/cuprite/issues/113#issuecomment-753598305" rel="noopener noreferrer"&gt;this one&lt;/a&gt;) that collected all JS errors during a test run and checked this array of errors in the &lt;code&gt;after&lt;/code&gt; block, allowing for ignoring preconfigured types of errors. Note that care must be taken about cleaning-up the state in RSpec as triggering an expectation error in the &lt;code&gt;after&lt;/code&gt; block otherwise skips all later code in the block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;catch_javascript_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignored_js_errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;log_records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;

  &lt;span class="n"&gt;aggregate_failures&lt;/span&gt; &lt;span class="s2"&gt;"javascript errors"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;log_records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ignored_js_error?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignored_js_errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be_nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Error caught in JS console:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# this is run after each test&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;catch_javascript_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error_logs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                            &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:ignored_js_errors&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;ensure&lt;/span&gt;
    &lt;span class="c1"&gt;# truncate the collected JS errors&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;truncate&lt;/span&gt;
    &lt;span class="c1"&gt;# clean up networking&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_network_idle&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other CDP protocol events (namely &lt;code&gt;Network.requestWillBeSent&lt;/code&gt;, &lt;code&gt;Network.responseReceived&lt;/code&gt; and &lt;code&gt;Network.requestServedFromCache&lt;/code&gt;) served as the basis for logging all requests and their responses. We chose a custom log format that enables us to better understand what’s going on – network wise – in each test and if you’re curious, it looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnu5iy9e46pzjzxoeaenw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnu5iy9e46pzjzxoeaenw.png" alt="Browser requests log in the system tests" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;We are indeed very happy about the migration to Cuprite.&lt;/strong&gt; Our tests are much faster, the API to handle them is simpler and the migration forced us to take a closer care while handling some special situations, benefiting not only the tests but the users visiting our site, too. Overall this feels like a great move and we heartily recommend anyone to do the same! 🤞&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you like reading stuff like this, you might want to &lt;a href="https://twitter.com/boramacz" rel="noopener noreferrer"&gt;follow us&lt;/a&gt; on Twitter.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cuprite</category>
      <category>selenium</category>
      <category>rails</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
