<?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: Matouš Borák</title>
    <description>The latest articles on DEV Community by Matouš Borák (@borama).</description>
    <link>https://dev.to/borama</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%2Fuser%2Fprofile_image%2F190621%2Fc2f062c6-120f-452b-9695-3b5607cb9122.jpg</url>
      <title>DEV Community: Matouš Borák</title>
      <link>https://dev.to/borama</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/borama"/>
    <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>Building a syntax highlighting extension for VS Code</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Fri, 01 Mar 2024 04:28:55 +0000</pubDate>
      <link>https://dev.to/borama/building-a-syntax-highlighting-extension-for-vs-code-594</link>
      <guid>https://dev.to/borama/building-a-syntax-highlighting-extension-for-vs-code-594</guid>
      <description>&lt;p&gt;I spent a few days of my spare time building a &lt;a href="https://marketplace.visualstudio.com/items?itemName=borama.ruby-slim" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt; that would bring better syntax highlighting for the &lt;a href="https://github.com/slim-template/slim" rel="noopener noreferrer"&gt;Slim template language&lt;/a&gt; to the editor. I quite enjoyed most of the process so I’d like to share what I learned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why?
&lt;/h3&gt;

&lt;p&gt;First of all, &lt;strong&gt;I like Slim&lt;/strong&gt;. I like the beauty and cleanness of Slim templates, to me they are way more readable than regular ERB templates and I think they fit in the ruby/Rails ecosystem very well. Slim is a close cousin to &lt;a href="https://haml.info/" rel="noopener noreferrer"&gt;Haml&lt;/a&gt;, without the ugly percent characters, haha. I've used Slim exclusively in my projects since about 2016.&lt;/p&gt;

&lt;p&gt;But why bother building a new syntax highlighting extension for VS Code especially when there &lt;a href="https://marketplace.visualstudio.com/items?itemName=sianglim.slim" rel="noopener noreferrer"&gt;already is&lt;/a&gt; a quite popular one? To answer that I need to add a bit more of a personal context (short, I promise)… As a long-time and mostly happy user of the &lt;a href="https://www.jetbrains.com/ruby/" rel="noopener noreferrer"&gt;RubyMine IDE&lt;/a&gt; from Jetbrains I recently noticed I somehow got used to this subdued, passive role: when I encountered a bug or a missing feature of the editor, I could of course file an &lt;a href="https://youtrack.jetbrains.com/issues/RUBY" rel="noopener noreferrer"&gt;issue in the tracker&lt;/a&gt; but – that was about it. As RubyMine is closed source code and the &lt;a href="https://plugins.jetbrains.com/docs/intellij/developing-plugins.html" rel="noopener noreferrer"&gt;plugins ecosystem&lt;/a&gt; was hard to grasp for me, I never felt the courage to deep dive into building a custom plugin (actually, I did try hard once – and failed – many years ago, and I can see that things might be a bit easier now with Kotlin, the Gradle toolkit and finally the recent &lt;a href="https://blog.jetbrains.com/ruby/2023/07/the-rubymine-2023-2-beta-updated-ai-assistant-lsp-api-for-plugin-developers-and-more/#lsp-support-for-plugin-developers" rel="noopener noreferrer"&gt;LSP support&lt;/a&gt;). So, I waited patiently for the developers to notice the issue and fix it. And sometimes they did! But more often not which is quite understandable for a niche bug or language like Slim.&lt;/p&gt;

&lt;p&gt;Now, fast forward to last year's &lt;a href="https://rubyonrails.org/world" rel="noopener noreferrer"&gt;Rails World conference&lt;/a&gt; that I was a lucky attendee of. What a breeze of fresh air! Among the many many inspiring people, talks and presentations, I noticed one thing: most people use VS Code, some use Vim but – more importantly – &lt;strong&gt;a lot of &lt;a href="https://youtu.be/iRjei4nj41o?feature=shared&amp;amp;t=1261" rel="noopener noreferrer"&gt;people&lt;/a&gt; &lt;a href="https://youtu.be/GnqRMQ0iQTg?feature=shared&amp;amp;t=2019" rel="noopener noreferrer"&gt;tweak&lt;/a&gt; their editor / IDE&lt;/strong&gt; almost as routinely as they tweak the code they work on professionally! And I thought: I want that too, how come I've lost this mindset here? I’ve taken for granted that I can tweak every imaginable aspect of my Linux OS as well as the Gnome environment so why not my IDE – the program that I literary spend most hours a day in? That was the final nudge for me to try to switch to something – anything really – that would be feasible for me to tweak and that’s how I ended up in VS Code. I’m not saying this will be my final IDE destination (looking at you &lt;a href="https://zed.dev/" rel="noopener noreferrer"&gt;Zed&lt;/a&gt;, &lt;a href="https://www.jetbrains.com/fleet/" rel="noopener noreferrer"&gt;Fleet&lt;/a&gt; or perhaps even Vim) but I know I want to stay closer to where a more active developer community around the editor is.&lt;/p&gt;

&lt;h3&gt;
  
  
  OK, but why another Slim syntax extension?
&lt;/h3&gt;

&lt;p&gt;After the switch, an opportunity for a small initial tweak came almost immediately: I didn't like the syntax highlighting of the Slim templates in our project. It almost resembled the old days when RubyMine highlighted &lt;a href="https://tailwindcss.com/docs/width#percentage-widths" rel="noopener noreferrer"&gt;slashes&lt;/a&gt; in some of the Tailwind classes as ugly errors (to be fair, they have fixed it a long time ago):&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%2Fk97r1e03zudvy4x7lbkv.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%2Fk97r1e03zudvy4x7lbkv.png" alt="An old version of RubyMine marking a Tailwind class with a slash as an error" width="800" height="108"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=sianglim.slim" rel="noopener noreferrer"&gt;The only Slim extension&lt;/a&gt; available for VS Code at that time had somewhat similar problems: it couldn’t recognize slashes in class names, it did not understand multi-line comments, attribute values and expressions, support for some embedded language blocks such as Markdown or SASS was missing.&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%2Fblvrmru9s846wb2yx82c.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%2Fblvrmru9s846wb2yx82c.png" alt="Slim extension having problems with a multi-line attribute" width="800" height="67"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I said to myself: &lt;em&gt;it couldn’t be that hard to fix, could it?&lt;/em&gt; Well… When I looked at &lt;a href="https://github.com/sianglim/vscode-slim" rel="noopener noreferrer"&gt;the repository&lt;/a&gt;, I soon found out that it wouldn’t be that easy: the grammar syntax was in some hardly comprehensible XML format (&lt;a href="https://en.wikipedia.org/wiki/Property_list" rel="noopener noreferrer"&gt;PList&lt;/a&gt;) and looked like being a bare copy from somewhere else, there were no comments or helpful tips anywhere and, above all, the whole thing was basically unmaintained: last changes from several years ago, no issues resolved, no PRs merged. I’m not judging here (of course I made several ”zombie projects“ myself, too), I just hope it’s clear now that I &lt;em&gt;had to&lt;/em&gt; go down the rabbit hole and rebuild the extension from the bottom up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Down the rabbit hole
&lt;/h3&gt;

&lt;p&gt;The official &lt;a href="https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide" rel="noopener noreferrer"&gt;Syntax Highlighting Guide&lt;/a&gt; was very nice and helpful. It was not hard to create a blank grammar extension. What turned out much harder was the grammar syntax itself, partly because there was no formal specification available. The official guide showed a &lt;a href="https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide#contributing-a-basic-grammar" rel="noopener noreferrer"&gt;basic grammar example&lt;/a&gt; but mostly linked to a few articles such as the &lt;a href="https://macromates.com/manual/en/language_grammars" rel="noopener noreferrer"&gt;Textmate grammars&lt;/a&gt; from which the VS Code ones originate. &lt;/p&gt;

&lt;p&gt;The most helpful resource for me, in the end, was &lt;a href="https://www.apeth.com/nonblog/stories/textmatebundle.html" rel="noopener noreferrer"&gt;this post by Matt Neuburg&lt;/a&gt;. It was a long one and I loved how sincere it was when describing the time involvement needed to deep dive into writing language grammars: &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%2F1453lbs7k0k30syiqvwe.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%2F1453lbs7k0k30syiqvwe.png" alt="Excerpt from Matt Neuburg’s article about grammars" width="800" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Strangely enough, it got me hooked, it looked like a fun thing to try! 🙂&lt;/p&gt;

&lt;h4&gt;
  
  
  Regular expressions
&lt;/h4&gt;

&lt;p&gt;I quickly learned that I needed to invest myself in one more thing before trying to actually edit the files: &lt;strong&gt;regular expressions&lt;/strong&gt;. All grammar matching is based on them. I refreshed the basics and re-read thoroughly the &lt;a href="https://www.regextutorial.org/positive-and-negative-lookahead-assertions.php" rel="noopener noreferrer"&gt;lookahead&lt;/a&gt; and &lt;a href="https://www.regextutorial.org/positive-and-negative-lookbehind-assertions.php" rel="noopener noreferrer"&gt;lookbehind&lt;/a&gt; chapters which turned out to be tremendously useful. &lt;/p&gt;

&lt;p&gt;As a ruby developer, I was happy to find that VS Code / TextMate grammar files use the same regular expression engine called &lt;a href="https://github.com/kkos/oniguruma" rel="noopener noreferrer"&gt;Oniguruma&lt;/a&gt; as ruby itself. Thus, I could be sure that when trying my regular expressions in my favorite online regex tool, &lt;a href="https://rubular.com/" rel="noopener noreferrer"&gt;rubular.com&lt;/a&gt;, there would be no inconsistencies due to the engine inner workings.&lt;/p&gt;

&lt;h4&gt;
  
  
  Unit-testing the beast
&lt;/h4&gt;

&lt;p&gt;Oh, one more thing – and actually the most important one – &lt;strong&gt;unit tests&lt;/strong&gt;! I would never be able to finish a working extension without having them at hand, I don’t even understand how did people make language grammars without unit-testing them, it seems just like a too complex task to me. I am no &lt;a href="https://en.wikipedia.org/wiki/Test-driven_development" rel="noopener noreferrer"&gt;TDD&lt;/a&gt;-ist but it was unit tests that gave me the necessary confidence and guided me throughout the process.&lt;/p&gt;

&lt;p&gt;I spent some time looking for ways to unit-test the grammar. The official VS Code docs didn’t say a word about that and their &lt;a href="https://code.visualstudio.com/api/working-with-extensions/testing-extension" rel="noopener noreferrer"&gt;Testing Extensions chapter&lt;/a&gt; dealt rather with integration tests for the extension itself. I needed something at a lower level.&lt;/p&gt;

&lt;p&gt;Luckily, there is a project that has fitted my needs perfectly: &lt;strong&gt;&lt;a href="https://github.com/PanAeon/vscode-tmgrammar-test" rel="noopener noreferrer"&gt;&lt;code&gt;vscode-grammar-test&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;. It’s a command line tool that builds on the VS Code &lt;a href="https://github.com/microsoft/vscode-oniguruma" rel="noopener noreferrer"&gt;regex engine&lt;/a&gt; and &lt;a href="https://github.com/microsoft/vscode-textmate" rel="noopener noreferrer"&gt;grammar file parser&lt;/a&gt; and allows to run unit tests directly against a given grammar file. &lt;/p&gt;

&lt;p&gt;The format of the test files themselves is inspired by the relatively new initiative by the &lt;a href="https://www.sublimetext.com/" rel="noopener noreferrer"&gt;Sublime Text&lt;/a&gt; team when they &lt;a href="https://www.sublimetext.com/blog/articles/sublime-text-3-build-3103" rel="noopener noreferrer"&gt;introduced&lt;/a&gt; a new grammar file format called &lt;a href="https://www.sublimetext.com/docs/syntax.html" rel="noopener noreferrer"&gt;Sublime Syntax&lt;/a&gt; and – more importantly – &lt;strong&gt;a way to &lt;a href="https://www.sublimetext.com/docs/syntax.html#testing" rel="noopener noreferrer"&gt;unit test&lt;/a&gt; grammars&lt;/strong&gt;. It’s using some lovely human-friendly magic comments that allow to specify what &lt;a href="https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide#textmate-tokens-and-scopes" rel="noopener noreferrer"&gt;scopes&lt;/a&gt; should the grammar file produce for a given position on a given line. &lt;/p&gt;

&lt;p&gt;For example, the following Slim line is tested using asserts in the comment lines below it: the &lt;code&gt;&amp;lt;-&lt;/code&gt; ”operator“ targets the first character on the tested line and the &lt;code&gt;^&lt;/code&gt;s target the ”underlined“ part of the tested line:&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;/ unit_tests.slim (i.e. this is a Slim file with comments)&lt;/span&gt;

&lt;span class="p"&gt;'&lt;/span&gt; Verbatim &lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&lt;/span&gt; without processing.
&lt;span class="c"&gt;/ &amp;lt;- punctuation.section.verbatim.slim&lt;/span&gt;
&lt;span class="c"&gt;/ ^^^^^^^^^^^^ text.html.embedded.slim&lt;/span&gt;
&lt;span class="c"&gt;/          ^^^ meta.tag.inline.b.start.html&lt;/span&gt;
&lt;span class="c"&gt;/              ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is cool. Even cooler is the fact that the Sublime Text team also provide their own official and full-featured &lt;a href="https://github.com/SublimeText/Slim/tree/master" rel="noopener noreferrer"&gt;grammar for Slim templates&lt;/a&gt; as well as unit tests! So, in the end, I just grabbed the &lt;a href="https://github.com/SublimeText/Slim/blob/master/tests/syntax_test_slim.slim" rel="noopener noreferrer"&gt;test file&lt;/a&gt; and used it as a basis for all my development of the VS Code extension grammar file. ❤️&lt;/p&gt;

&lt;h4&gt;
  
  
  TL;DR recap
&lt;/h4&gt;

&lt;p&gt;Chances are that your head is already exploding so let’s recap the dependencies and inspiration sources for building a VS Code syntax grammar file: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VS Code syntax highlighting works with TextMate editor grammar files,&lt;/li&gt;
&lt;li&gt;they are written in an old but well-thought-out specification based on some clever regexp matching&lt;/li&gt;
&lt;li&gt;for VS Code they can be written in PList, JSON or YAML formats,&lt;/li&gt;
&lt;li&gt;the Sublime Text team introduced a newer format for the same task (which is not directly usable in VS Code) and another one for testing the grammars,&lt;/li&gt;
&lt;li&gt;they also created grammar files for various languages, they are usually well-maintained and include the test files,&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;vscode-grammar-test&lt;/code&gt; tool combines the Sublime Text test files with VS Code / TextMate grammar files and thus allows unit testing the VS Code syntax grammars.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building the grammar
&lt;/h3&gt;

&lt;p&gt;With all the tools and some basic theoretical knowledge of the problem at hand, I began building the grammar file. Of course I didn’t start from scratch though, I used the official TextMate grammar file (the &lt;a href="https://github.com/slim-template/ruby-slim.tmbundle/blob/master/Syntaxes/Ruby%20Slim.YAML-tmLanguage" rel="noopener noreferrer"&gt;one in the YAML format&lt;/a&gt;) from the &lt;a href="https://github.com/slim-template/ruby-slim.tmbundle" rel="noopener noreferrer"&gt;Slim language repository&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;I opened up the grammar file and the unit test file in my editor and started working &lt;strong&gt;iteratively&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;I converted my YAML grammar file to the JSON format using the &lt;code&gt;js-yaml&lt;/code&gt; tool (see my &lt;a href="https://github.com/borama/vscode-ruby-slim/wiki/Development-hints" rel="noopener noreferrer"&gt;wiki&lt;/a&gt; for more instructions).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I &lt;a href="https://github.com/borama/vscode-ruby-slim/wiki/Development-hints#tests" rel="noopener noreferrer"&gt;ran the tests&lt;/a&gt; with the output redirected to a &lt;code&gt;results.txt&lt;/code&gt; log file. This flooded the results log with hundreds of assertion errors. I opened up the log file and scrolled to the first error, for example similar to this one:&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✖ tests/unit_tests.slim failed
  at [tests/unit_tests.slim:5:1:2]:
  5: /Comment
     ^
  missing required scopes: comment.block.code.slim punctuation.definition.comment.slim
  actual: source.slim comment.block.slim comment.line.slash.slim punctuation.definition.comment.slim
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Next, I tried to understand what the error was about: was this a real error or a missing feature in the grammar file? Or, was this just a mismatch between what scopes the original grammar file produced vs. what scopes the – much younger – Sublime Text unit test file expected? What scopes does VS Code need anyway? While it was hard to asses these things in the beginning, I slowly began to follow… (I will share some hints below.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;BTW, I found the &lt;a href="https://dev.to/github/understand-your-code-using-github-copilot-5375"&gt;GitHub Copilot: Explain This&lt;/a&gt; feature in VS Code quite useful. It provided me with explanations that helped me initially, even though I still had to do the hard part – deciding what the error really was about – myself.&lt;br&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%2Flfo7guh2b0ynakg0k2ol.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%2Flfo7guh2b0ynakg0k2ol.png" alt="Copilot explaining a grammar test for me" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Having an idea about the cause of the error I edited the grammar file and/or the test file and jumped back to #1. And that was all there was to it! 😄&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It may sound scary but &lt;strong&gt;it was actually quite fun&lt;/strong&gt;! Although trying to untangle some of the problems definitely &lt;em&gt;was&lt;/em&gt; frustrating, the &lt;strong&gt;very fast feedback loop&lt;/strong&gt; that this TDD-ish workflow provided was very satisfying and kept me reassured and oriented along the path. The most pleasant were the situations when I fixed a single thing in the grammar file and then watched tens or even hundreds of errors magically disappear!&lt;/p&gt;

&lt;h4&gt;
  
  
  Selected grammar file quirks
&lt;/h4&gt;

&lt;p&gt;I will not try to explain how the grammar file works, it would be useless to repeat stuff from other great materials such as the &lt;a href="https://www.apeth.com/nonblog/stories/textmatebundle.html" rel="noopener noreferrer"&gt;Matt’s post&lt;/a&gt;. Instead, I want to briefly mention a few of the peculiarities that one likely hits when playing with the VS Code grammar files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The order of patterns in the grammar file is important - &lt;strong&gt;first patterns are tried first&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The regexps can and often &lt;strong&gt;should use groups&lt;/strong&gt; (either numbered or named groups). They can then be targeted by the &lt;code&gt;capture&lt;/code&gt; keys mapping the groups to the actual &lt;a href="https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide#textmate-tokens-and-scopes" rel="noopener noreferrer"&gt;VS Code scopes&lt;/a&gt; that the given language pattern should produce.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Although the regexps in the grammar can match new lines &lt;code&gt;\n&lt;/code&gt;, they will &lt;strong&gt;never match multiple lines of text&lt;/strong&gt; even though they are written like they should. In other words, regexps in grammars are forced to always match a single line of text only.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;end&lt;/code&gt; key of a pattern, which specifies the regexp that the matched language pattern ends with, often acts in a surprising way that one has to keep in mind: if you match some &lt;em&gt;nested&lt;/em&gt; patterns inside this one, its &lt;strong&gt;&lt;code&gt;end&lt;/code&gt; regexp will become a ”floating“ one&lt;/strong&gt;. It will match the (final) part of the language pattern &lt;em&gt;after all nested patterns have finished their matching&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;So, for example, if the &lt;code&gt;end&lt;/code&gt; regexp matches a new line but patterns nested inside the main one also match some new lines, the main pattern &lt;code&gt;end&lt;/code&gt; will match a new line only after all the nested patterns are satisfied. In other words, what the &lt;code&gt;end&lt;/code&gt; regexp effectively means is that it’s the &lt;em&gt;first&lt;/em&gt; match the pattern &lt;em&gt;can have&lt;/em&gt; if no other nested patterns prolong the whole pattern.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;end&lt;/code&gt; regexp can also &lt;strong&gt;&lt;a href="https://www.regular-expressions.info/backref.html" rel="noopener noreferrer"&gt;backreference&lt;/a&gt; groups from the &lt;code&gt;begin&lt;/code&gt; regexp&lt;/strong&gt;. This is very helpful in Slim templates because it allows the &lt;code&gt;end&lt;/code&gt; regexp to match lines that are nested out of the &lt;code&gt;begin&lt;/code&gt; line.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The grammar syntax also allows defining a &lt;strong&gt;repository of named patterns&lt;/strong&gt;. The repository very conveniently cleans up the grammar file and allows reusing of patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When working with &lt;strong&gt;embedded languages&lt;/strong&gt;, the appropriate language grammar file for the embedded language is needed to better understand how embedding works and to run tests that target embedded language scopes (see the &lt;code&gt;-g&lt;/code&gt; option of the &lt;code&gt;vscode-tmgrammar-test&lt;/code&gt; tool). It should not be packed into the extension, though, this is a dev-only dependency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;VS Code has a &lt;a href="https://macromates.com/manual/en/language_grammars#naming_conventions" rel="noopener noreferrer"&gt;convention for scopes naming&lt;/a&gt;. &lt;strong&gt;Sometimes this convention differs&lt;/strong&gt; from conventions in other editors, notable those used in the new grammar files in Sublime Text which I used as a basis for the Slim extension. Thus, I had to rename some of the scopes to match the VS Code ones.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Debugging and testing the extension
&lt;/h3&gt;

&lt;p&gt;I used a few ways to try and test the syntax highlighting extension I was working on. First of all, the most important one were the &lt;strong&gt;unit tests&lt;/strong&gt;. Once I got used to the error messages the testing tool reported, I was able to quickly pinpoint the problematic location in the grammar file and could start playing with the matching rules there. To verify that they work, I again ran the tests and watched the changes in the output log.&lt;/p&gt;

&lt;p&gt;In more complicated cases, &lt;strong&gt;I used the &lt;a href="https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide#scope-inspector" rel="noopener noreferrer"&gt;Scope inspector&lt;/a&gt;&lt;/strong&gt;. I created a &lt;a href="https://github.com/borama/vscode-ruby-slim/blob/main/.vscode/launch.json" rel="noopener noreferrer"&gt;launch configuration&lt;/a&gt; to run the extension in the &lt;a href="https://code.visualstudio.com/api/advanced-topics/extension-host" rel="noopener noreferrer"&gt;VS Code Extension host&lt;/a&gt;. Then I could run the extension any time, open up a Slim template and start the Scope inspector via the ”Editor: Inspect Editor Tokens and Scopes“ quick action (for which I soon learned the shortcut key). This way I was able to inspect the behavior of the extension in a live document. &lt;/p&gt;

&lt;p&gt;By the way, I extracted the official &lt;a href="https://github.com/slim-template/slim/blob/main/test/literate/TESTS.md" rel="noopener noreferrer"&gt;Literate tests&lt;/a&gt; from the Slim repository into a &lt;a href="https://github.com/borama/vscode-ruby-slim/blob/main/tests/test_suite.slim" rel="noopener noreferrer"&gt;Slim template&lt;/a&gt; that would be always ready for some manual testing.&lt;/p&gt;

&lt;p&gt;From time to time I also &lt;strong&gt;&lt;a href="https://github.com/borama/vscode-ruby-slim/wiki/Development-hints#pack-extension" rel="noopener noreferrer"&gt;packed&lt;/a&gt; the extension&lt;/strong&gt; into a &lt;code&gt;.vsix&lt;/code&gt; file and installed it locally into my VS Code (using the ”Extension: Install from VSIX…“ quick action) so that I could watch it highlighting some real code during my regular working days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final notes
&lt;/h3&gt;

&lt;p&gt;So here we are, the &lt;a href="https://marketplace.visualstudio.com/items?itemName=borama.ruby-slim" rel="noopener noreferrer"&gt;Slim highlighting extension&lt;/a&gt; is ready for you to try. I mostly enjoyed the process of building it, although at times frustrating and having an arcane feel. Thanks to the fast feedback loop, very clear problem boundaries and a good notion of progress it was fun most of the time!&lt;/p&gt;

&lt;p&gt;I now realize I quickly made most of the extension four months ago and it was only after I finished covering the grammar file by the unit tests that I somehow lost interest. I had the extension installed locally and it was working well, so I didn’t feel the push to finish the last 5% and actually publish the thing. Until now, luckily. 😅&lt;/p&gt;

&lt;p&gt;So, enjoy the extension if you work in Slim, and I encourage you to try to build your own language grammar if you like working with a vintage, sometimes weird but apparently well-thought-out piece of technology.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Want more stuff 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>vscode</category>
      <category>slim</category>
      <category>rails</category>
      <category>regex</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>TUXEDO InfinityBook Pro 14: The Linux Laptop</title>
      <dc:creator>Matouš Borák</dc:creator>
      <pubDate>Sun, 17 Dec 2023 11:08:12 +0000</pubDate>
      <link>https://dev.to/borama/tuxedo-infinitybook-pro-14-the-linux-laptop-1026</link>
      <guid>https://dev.to/borama/tuxedo-infinitybook-pro-14-the-linux-laptop-1026</guid>
      <description>&lt;p&gt;After almost seven years, I decided that the time for an upgrade is up! As a life-long Linux user, I spent some time looking for good alternative manufacturers of professional laptops that would be as Linux-friendly as possible. And I was happy to see that there are quite a few to choose from! But let me add some context first which will help clarify my preferences.&lt;/p&gt;

&lt;h3&gt;
  
  
  My preferences and previous experience
&lt;/h3&gt;

&lt;p&gt;As a senior web developer working mostly in Ruby on Rails, I usually just edit text files and browse the web, nothing too demanding in terms of hardware power. I don’t even play games much (this might change now, actually 😅). That said, I &lt;em&gt;hate&lt;/em&gt; being slowed down by a lagging computer, peripheral, operating system, program or whatever so since I could afford it I always looked for higher-end devices.&lt;/p&gt;

&lt;p&gt;Although I usually work remotely from home, I've always opted for &lt;strong&gt;a laptop&lt;/strong&gt;. That’s because when I &lt;em&gt;do&lt;/em&gt; work outside, in the office or while commuting, I &lt;em&gt;absolutely need&lt;/em&gt; to have the same setup as usually. I hate having to take care of maintaining and syncing multiple devices that I expect to &lt;em&gt;just work&lt;/em&gt;. Over time, I fell in love with the convenience, lightness and beauty of ultra-portable laptops and the HiDPI displays that often come with them so am not looking elsewhere.&lt;/p&gt;

&lt;p&gt;Previously, I used to have all sorts of laptops from various manufacturers, from some Hewlett-Packards, to a Fujitsu, to a Dell XPS 13 which was actually quite a nice device that I’ve used until now. (It had its quirks though and I got to replace almost everything I could in it, from the wifi module, to the motherboard or the &lt;a href="https://twitter.com/boramacz/status/1299696896223608834" rel="noopener noreferrer"&gt;palm rest assembly&lt;/a&gt; multiple times due to its notoriously loosening and breaking screw holders.)&lt;/p&gt;

&lt;p&gt;All of them were running Linux, all having various problems with it, luckily less and less over time though, as Linux matured and manufacturers turned to cooperating with the kernel developers more often. After having used Red Hat, Ubuntu or Gentoo I jumped into the Arch Linux waters and never looked back. Generally, &lt;em&gt;I like to tweak&lt;/em&gt; every aspect of the OS and the desktop environment so my full OS re-installs require a lot of effort and must not be needed more often than – essentially – when buying a new hardware…&lt;/p&gt;

&lt;p&gt;So, overall, this time I was looking for a &lt;strong&gt;thin, ultra-portable, powerful laptop, with good display&lt;/strong&gt;, that would be &lt;strong&gt;able to run Linux nicely&lt;/strong&gt;. I didn’t care that much about multimedia peripherals quality, battery capacity or gaming power.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Linux laptop market is finally a reality
&lt;/h3&gt;

&lt;p&gt;At first I – very briefly – considered the standard laptop brands such as the latest Dell XPS 13 but always hated something essential about each and every one of them (such as having an awful &lt;a href="https://www.theverge.com/2022/1/4/22865547/dell-xps-13-plus-2022-hands-on-new-design-features" rel="noopener noreferrer"&gt;keyboard with a touch bar&lt;/a&gt;, omg) or did not believe enough in the stated Linux compatibility.&lt;/p&gt;

&lt;p&gt;I again realized how unhappy I was regarding the usual laptop market due to them still ignoring Linux. Fed up with paying thousands of bucks for a device running perhaps perfectly on Windows but being a &lt;em&gt;pain in the arse&lt;/em&gt; to configure and/or use on Linux. Annoyed by binary drivers, &lt;a href="https://github.com/torvalds/linux/blob/master/Documentation/nvme/feature-and-quirk-policy.rst" rel="noopener noreferrer"&gt;quirks&lt;/a&gt; or having to pay for software licenses I wouldn’t use. What I wanted instead was a laptop that was carefully thought through specifically in terms of Linux compatibility and well tested under various distributions. A laptop, that would just work and work well.&lt;/p&gt;

&lt;p&gt;And I was pleasantly surprised how many such companies there are in the market nowadays! There are some interesting brands, &lt;a href="https://system76.com/laptops-ultraportables" rel="noopener noreferrer"&gt;System76&lt;/a&gt;, &lt;a href="https://puri.sm/products/librem-14/" rel="noopener noreferrer"&gt;Purism&lt;/a&gt; or the absolutely lovely concept of &lt;a href="https://frame.work/marketplace/laptops" rel="noopener noreferrer"&gt;Framework laptops&lt;/a&gt;. I liked some of the devices but kept on searching because all of these brands were US-based and I wanted to prefer a European company, if possible.&lt;/p&gt;

&lt;p&gt;Of course, there are a few of them, too! I particularly noticed &lt;a href="https://slimbook.com/en/" rel="noopener noreferrer"&gt;Slimbook&lt;/a&gt; and &lt;a href="https://www.tuxedocomputers.com/" rel="noopener noreferrer"&gt;TUXEDO Computers&lt;/a&gt;. After some more hesitation and &lt;a href="https://www.youtube.com/results?search_query=tuxedo+infinitybook+pro+14" rel="noopener noreferrer"&gt;reviews watching&lt;/a&gt;, I chose the &lt;strong&gt;TUXEDO&lt;/strong&gt; (yes, it’s written in all-caps) &lt;strong&gt;&lt;a href="https://www.tuxedocomputers.com/en/TUXEDO-InfinityBook-Pro-14-Gen8.tuxedo" rel="noopener noreferrer"&gt;InfinityBook Pro 14&lt;/a&gt;&lt;/strong&gt; (8th generation). I maxed-out most of the &lt;a href="https://www.tuxedocomputers.com/en/TUXEDO-InfinityBook-Pro-14-Gen8.tuxedo#configurator" rel="noopener noreferrer"&gt;specs&lt;/a&gt;, fought the urge to add a custom logo and placed an order!&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%2Fw49pv1emyhbb9qzfssj7.jpg" 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%2Fw49pv1emyhbb9qzfssj7.jpg" alt="The new TUXEDO" width="800" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  First checks and a Linux re-installation
&lt;/h3&gt;

&lt;p&gt;The package came within two weeks. For the next few hours, I had the preinstalled &lt;a href="https://www.tuxedocomputers.com/en/TUXEDO-OS_1.tuxedo" rel="noopener noreferrer"&gt;TUXEDO OS&lt;/a&gt; booted up to check that all hardware is working nicely, including all function keys and even that little LED switch on the touchpad that allows to temporarily lock it. &lt;/p&gt;

&lt;p&gt;All seemed OK so I wiped the disk and proceeded with Arch installation. That was straightforward, too, the only part I was fighting was the whole disk encryption (but I always do until I re-read the docs properly). So, soon after, I was running my favorite Linux distro with Gnome, Wayland, Firefox and everything else and could finally start comparing my impressions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Things I like
&lt;/h3&gt;

&lt;p&gt;I’ll try to keep this short while there are many things to like…&lt;/p&gt;

&lt;h4&gt;
  
  
  That beast is fast
&lt;/h4&gt;

&lt;p&gt;This is the thing I noticed at the very first moment. It felt fast right after installation but at that time I thought &lt;em&gt;maybe it’s just that it’s still basically empty&lt;/em&gt; but it stayed equally snappy after I loaded it with all my programs and data. Sure, it’s probably nowhere near the computing power of – say – the latest Apple “M” processors (well, in fact, &lt;a href="https://browser.geekbench.com/v6/cpu/compare/4011200?baseline=3981540" rel="noopener noreferrer"&gt;it’s not that bad&lt;/a&gt;) but I’m not talking about benchmarks here but about the subjective feel of overall responsiveness during my usual workload. &lt;/p&gt;

&lt;p&gt;At last I don’t mind if someone sends me a link to Slack, Miro, Figma, Notion or any other complex modern SPA web, as they open much, &lt;em&gt;much&lt;/em&gt; faster than I was used to. I don’t mind doing this even during a Meet call (Meet being an infamous CPU eater) while having a ton of other programs open. Starting the Rails server for our not-so-little project is 40% faster for me than before which probably crossed some psychological perceptual boundary because subjectively it feels &lt;em&gt;so much&lt;/em&gt; faster and not blocking me any more now.&lt;/p&gt;

&lt;p&gt;During the couple of weeks that I have the TUXEDO now, &lt;strong&gt;I’ve never experienced any lag, sluggishness or hiccup&lt;/strong&gt;, none that I’m aware of, at least. Contributing to this must be the 20 CPU threads and also the 64 gigabytes of RAM that I’m very happy to have added – the ”used memory“ chart rarely reaches 30% and I’ve yet to see it cross 50%. (Did I ditch the swap file? Of course I did!).&lt;/p&gt;

&lt;h4&gt;
  
  
  It just works
&lt;/h4&gt;

&lt;p&gt;I know I’ve already said this but for me it’s such an essential point that I’ll shamelessly repeat: &lt;strong&gt;all hardware just works out of the box&lt;/strong&gt;. The only thing I had to explicitly install driver for was that little LED switch on the touchpad that I mentioned earlier (and luckily Arch has a package for it so even that was a non issue). Everything else, from the WiFi to all graphics modes to sleep mode to Bluetooth to webcam or HDMI, it all just works. I didn’t bother with the infra-red camera though (the one that can be used for logging in), the OS seems to see it correctly but I had no interest yet.&lt;/p&gt;

&lt;p&gt;I still can’t get fully used to this convenience. I haven’t had to solve any surprises after rebooting. All peripherals connected to the laptop via a USB-C docking station have so far worked seamlessly, too. Of course, maybe I was just lucky. Or maybe not, just compare the &lt;a href="https://wiki.archlinux.org/title/TUXEDO_InfinityBook_14_v6" rel="noopener noreferrer"&gt;Arch wiki page&lt;/a&gt; for (an older) TUXEDO versus the &lt;a href="https://wiki.archlinux.org/title/Dell_XPS_13_Plus_(9320)" rel="noopener noreferrer"&gt;new XPS&lt;/a&gt;…&lt;/p&gt;

&lt;h4&gt;
  
  
  It’s light
&lt;/h4&gt;

&lt;p&gt;Compared to my old XPS 13, the TUXEDO feels noticeably lighter, although, interestingly, the weight is about the same in specs (ok, I weighted them both now and TUXEDO is 75g lighter). It’s so light that one of my colleagues thought that it has a plastic case (while it’s some magnesium alloy). It’s probably a bit less sturdy than my old Dell aluminum chassis but still solid enough for daily carrying over and I just love the lightness, I even sometimes forget that I have the laptop in my back pack…&lt;/p&gt;

&lt;h4&gt;
  
  
  The display is fine
&lt;/h4&gt;

&lt;p&gt;The Dell display, with its extreme QHD resolution, set the bar really high and got me used to very sharp visuals and text. The TUXEDO display has a bit fewer pixels but you know what? Only horizontally. The vertical resolution is the same in both devices, which means that they both can show the same amount of text or code but text on the TUXEDO feels a bit larger as the display is taller. And I actually like that! I run my Gnome at 200% happily and after tweaking a few font sizes I reached a sweet spot. (Oh, actually I still use &lt;a href="https://extensions.gnome.org/extension/5669/compact-top-bar/" rel="noopener noreferrer"&gt;an extension&lt;/a&gt; to make the top Gnome panel a few pixels smaller but that’s just a touch up.)&lt;/p&gt;

&lt;p&gt;What I also like about the display is that it’s anti-glare matte finish actually does the job. The brightness is fine too, sometimes I only miss that it can’t be turned a little bit further down. The 90Hz frequency is also nice especially when scrolling and animations feel so smooth.&lt;/p&gt;

&lt;h4&gt;
  
  
  The TUXEDO Control Center
&lt;/h4&gt;

&lt;p&gt;I love how TUXEDO not only makes laptops but tries to take care of the whole experience. They have their own Linux distro that I don’t use but they also maintain a &lt;a href="https://www.tuxedocomputers.com/en/TUXEDO-Control-Center.tuxedo" rel="noopener noreferrer"&gt;Control Center&lt;/a&gt; application that allows to monitor and configure various advanced aspects of the hardware. I use it to set the battery charging profile (in hope that it will last more than the ~3 years I was used to) or the power control profiles. Nice!&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%2Fzatem1btwjetlyc6tkf4.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%2Fzatem1btwjetlyc6tkf4.png" alt="TUXEDO Control Center" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Things I like less…
&lt;/h3&gt;

&lt;p&gt;There are certainly a few – rather minor – things that I dislike or am in the process of forced getting used to…&lt;/p&gt;

&lt;h4&gt;
  
  
  Keyboard issues
&lt;/h4&gt;

&lt;p&gt;The first thing I noticed negatively about this laptop was the &lt;strong&gt;Delete key position&lt;/strong&gt;: right next to the power button and – more importantly – not in the top right-hand corner of the keyboard any more. It felt wrong, even more when I was used to smashing ”that corner area“ with my finger and now had to re-learn my muscle memory to be more precise. The power button has a delay so, luckily, putting the laptop to sleep mode by accident never actually happened to me but I still hated the missed Delete key presses. Interestingly, I must have been successful at re-learning as this is not an issue for me any more, I now just vaguely remember how disturbing it was at the beginning.&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%2Fxc2rqqu7dfd94vhfvxgp.jpg" 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%2Fxc2rqqu7dfd94vhfvxgp.jpg" alt="The Delete key" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I still regret today is that I ordered a &lt;strong&gt;&lt;a href="https://freepages.rootsweb.com/~elainetmaddox/genealogy/czech_keyboard.jpg" rel="noopener noreferrer"&gt;Czech keyboard layout&lt;/a&gt;&lt;/strong&gt; for the notebook. I wanted to be able to see the top row of accented characters, yeah, this is nice, but I didn’t realize that all other special character keys are in such unusual positions and combinations that almost all non-alphanumeric key caps are misleading now. To clarify, I don’t use the Czech keyboard layout in my OS, I actually use a slightly customized &lt;a href="https://normanlayout.info/" rel="noopener noreferrer"&gt;Norman layout&lt;/a&gt; with the Czech characters added so that I can equally easily type Czech accents as well as all the special characters needed for programming.&lt;/p&gt;

&lt;p&gt;I queried the TUXEDO support whether they could send me a few of the US layout key caps and to my surprise they offered me only replacing the whole keyboard. Which was kind of them but required sending the laptop back to their office and would not really solve my issue as I ideally want a fully customized layout. They said they had some bad experience with people damaging their keyboards while trying to shuffle keys on them which is funny because that was the first thing I did with the new laptop - I swapped the keys to match my layout and luckily it all works perfectly 😅. Anyway, this whole layout thing is not a TUXEDO issue but rather my own problem, I guess.&lt;/p&gt;

&lt;h4&gt;
  
  
  Touchpad issues
&lt;/h4&gt;

&lt;p&gt;The touchpad is &lt;strong&gt;huge&lt;/strong&gt;. It so big that I had to relearn where to put my left hand while typing or resting on the palm-rest assembly. Even so, I accidentally and &lt;strong&gt;unconsciously touched the pad&lt;/strong&gt; several times, only to realize that my cursor moved somewhere I didn’t want and changed the currently focused app. I even unknowingly locked the touchpad a few times, nice that I got the LED working and now I can at least easily notice this situation. That said, I think I got used to this somehow as I don’t remember hitting this issue from a more recent past.&lt;/p&gt;

&lt;p&gt;Another weird issue I have with the touchpad is that it &lt;strong&gt;seems to have problems with some of my fingers&lt;/strong&gt;. When scrolling, I usually use fingers 3 and 4 (counting from the thumb) and on the new laptop this leads to some barely noticeable (but to me still sometimes disturbing) jerkiness in the scrolling. I have no idea what the root cause might be here: do I have the fingers too close to each other? is the conductivity of the skin on one of my fingers weird? Who knows. What I learned though is that when I use some other fingers, like 2 and 4 or 2 and 3, the problem goes away. And again, I must have forced my brain to do so as I don’t think I hit the issue any more. When writing this I even thought that the problem got resolved with some setting or update but no, it is still there if I try hard enough.&lt;/p&gt;

&lt;h4&gt;
  
  
  Fans and heat issues
&lt;/h4&gt;

&lt;p&gt;Especially in the beginning when my senses regarding the laptop were alert and perception sharp could I notice the next issue: &lt;strong&gt;the fans are almost never totally calm&lt;/strong&gt;. Not that I could usually hear them while being focused on working but when I turn the laptop upside down, yeah, they rotate, almost all the time. The TUXEDO monitoring app says the fans are usually at 20% of their max speed. I could probably manually move the trigger point for fans to start rotating a bit higher on the CPU heat curve but I have that in mind as a backup in case the problem bothers me more. Which it does not yet. I guess in general this is the high price for the enormous laptop power that we have to pay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: this was actually a BIOS setup issue. After I enabled the "silent" fans mode in BIOS, they keep calm much more often.&lt;/p&gt;

&lt;p&gt;Another unexpected thing still surprises me time to time. There is heat coming out from behind the keyboard keys. It’s not a vague, fuzzy heat warming up the whole keyboard (that I experienced on my old Dell), it’s rather a &lt;strong&gt;focused ”blow“ of hot air attacking your fingers&lt;/strong&gt; while the rest of the keyboard stays cool. It’s not an issue I struggle with often, but sometimes, especially when I rest my right thumb in the tiny area between the space bar and the touchpad, my brain triggers a ”too hot!“ alert and makes the finger move away before I realize it.&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%2Fvdu5qhm99i3pucs2f1zp.jpg" 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%2Fvdu5qhm99i3pucs2f1zp.jpg" alt="The place with occasional heat blow" width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Coil whine etc
&lt;/h4&gt;

&lt;p&gt;Yeah, it’s there but I only noticed it yesterday for the first time ever. It’s funny because my old Dell was &lt;a href="https://www.youtube.com/watch?v=ATxR9FyBrVw" rel="noopener noreferrer"&gt;very infamous&lt;/a&gt; for coil whine and here it is again. On the other hand, it’s much less of a problem on the TUXEDO laptop and it can only be noticed in extremely calm environment. The fact that I haven’t noticed it for several weeks makes it more than good enough for me.&lt;/p&gt;

&lt;p&gt;I could also mention some things that might be relevant for other people, like a not particularly excellent quality of the webcam but you know what? &lt;strong&gt;I don’t care.&lt;/strong&gt; It’s good enough for my needs and I have an external webcam at home anyway. So I’ll leave the rest of potential complaints to others. I am not aware of anything else that would bother me.&lt;/p&gt;

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

&lt;p&gt;Should this still not be obvious from the above text, &lt;strong&gt;I love my new TUXEDO&lt;/strong&gt; and hope that it will last for another seven years. It’s powerful as hell for my needs and it – almost boringly – &lt;strong&gt;just works in Linux&lt;/strong&gt;. My productivity instantly jumped to unprecedented levels, the overall experience is very smooth, nothing gets in the way when I’m working with the applications that I need. &lt;/p&gt;

&lt;p&gt;In a way this enhances a &lt;strong&gt;similar feel when programming in Ruby&lt;/strong&gt; – I can make the program (or computer) do things in about the same pace as my intentions and thoughts come to my brain. I don’t have to wait till the interrupted flow is restored, I don’t struggle. And that is so nice and addictive. So thanks, TUXEDO!&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%2F10lk9vgnw4hfq7zpwabx.jpg" 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%2F10lk9vgnw4hfq7zpwabx.jpg" alt="TUXEDO" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Followup (spring 2024)
&lt;/h3&gt;

&lt;p&gt;I started experiencing some random hard reboots, so I contacted the TUXEDO support and they recommended adding &lt;code&gt;i915.enable_guc=2&lt;/code&gt; to the kernel boot parameters, which indeed fixed the issue immediately and I'm happy again. :)&lt;/p&gt;

&lt;h3&gt;
  
  
  Winter 2024 (after 1st year of usage)
&lt;/h3&gt;

&lt;p&gt;All good, the beast just works! I mean recently I had a small weird issue with the keyboard underlight – out of the blue it started flashing about once per second and it couldn’t be controlled in any way. Once I figured out that the problem was deeper than in the OS (the flashing started even before booting Linux), I upgraded the BIOS and this fixed the issue. Since then, all good again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: it just happened again and it turns out that the problem is already known and the &lt;a href="https://www.reddit.com/r/tuxedocomputers/comments/1eijzol/comment/li1kexo/" rel="noopener noreferrer"&gt;solution&lt;/a&gt; is to keep the Power button pressed for 10-20s. It is somehow related to USB-c charging and I’m actually happy that I do not have to update BIOS again!&lt;/p&gt;

</description>
      <category>tuxedo</category>
      <category>hardware</category>
      <category>linux</category>
      <category>productivity</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>
  </channel>
</rss>
