<?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: Alberto Hernandez Cerezo</title>
    <description>The latest articles on DEV Community by Alberto Hernandez Cerezo (@pascualtalcual).</description>
    <link>https://dev.to/pascualtalcual</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%2F971808%2Fe18acdb1-21d9-450e-b34f-92cd56b16900.png</url>
      <title>DEV Community: Alberto Hernandez Cerezo</title>
      <link>https://dev.to/pascualtalcual</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pascualtalcual"/>
    <language>en</language>
    <item>
      <title>Configure Stimulus with esbuild and Babel — Rails &amp; Javascript</title>
      <dc:creator>Alberto Hernandez Cerezo</dc:creator>
      <pubDate>Sun, 26 Feb 2023 16:18:08 +0000</pubDate>
      <link>https://dev.to/pascualtalcual/configure-stimulus-with-esbuild-and-babel-rails-javascript-2jgf</link>
      <guid>https://dev.to/pascualtalcual/configure-stimulus-with-esbuild-and-babel-rails-javascript-2jgf</guid>
      <description>&lt;h2&gt;
  
  
  What will you learn?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Understand how Javascript works in Rails. Learn to configure a Rails project with Stimulus, using esbuild to bundle your code efficiently and effectively. Set up a simple script to automatically load your Stimulus controllers and bundle your Javascript code when editing your code.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Rails&lt;/code&gt; &lt;strong&gt;server-side &lt;code&gt;HTML&lt;/code&gt; rendering enables the creation of powerful web interfaces without using javascript&lt;/strong&gt;. For more complex cases which require it, Rails provides Stimulus, a minimalist framework to embed javascript in your views.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both &lt;code&gt;Rails&lt;/code&gt; and &lt;code&gt;Stimulus&lt;/code&gt; aim to minimize your project's javascript code&lt;/strong&gt;. This is desired since javascript significantly increases maintenance costs due to its web browser execution environment.&lt;/p&gt;

&lt;p&gt;With multiple web browsers in the market, each with multiple versions, running in numerous OS of different devices, &lt;strong&gt;ensuring a javascript application works as expected on each platform is challenging&lt;/strong&gt;. Compared to a Rails server, running on a specific OS on specific machines where developers can observe and diagnose errors, the javascript run environment is out of the developers' control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To mitigate this problem, the javascript ecosystem provides tools to transpile and bundle code to maximize code compatibility and optimization&lt;/strong&gt;. We are talking about bundlers such as esbuild and transpilers like Babel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;However, the more we dig into these solutions, the more we overload our stack with javascript code&lt;/strong&gt;. On top of that, these tools are complex and require a good understanding of their purpose, how they work, and how to configure them properly. Testimony of this is the length of the present post.&lt;/p&gt;

&lt;p&gt;This document is designed with newcomers in mind. To properly configure your javascript stack, it is important to understand its functioning and parts. Because of that, we will spend a significant part of the post explaining all these agents in an easy-to-understand way; to then put the theory into practice with the real configuration of a Rails project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are already familiar with bundlers and are just seeking a solution to integrate &lt;code&gt;Stimulus&lt;/code&gt; and &lt;code&gt;esbuild&lt;/code&gt;in your project, I recommend you to jump to the solution section or even to the PR links where the final configuration is available and can be easily integrated into any other &lt;code&gt;Rails&lt;/code&gt; project&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basics
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How does Javascript work in Rails?
&lt;/h2&gt;

&lt;p&gt;A Rails web application is an application with a web-based interface. Web interfaces are rendered in web browsers.&lt;/p&gt;

&lt;p&gt;When we open a Rails application in our web browser, &lt;strong&gt;the navigator performs &lt;code&gt;HTTP&lt;/code&gt; requests to &lt;code&gt;Rails&lt;/code&gt; backend server in &lt;code&gt;HTML&lt;/code&gt; format. In response, the server returns an &lt;code&gt;HTTP&lt;/code&gt; response in &lt;code&gt;HTML&lt;/code&gt; format&lt;/strong&gt;. This response is rendered by the web browser, displaying the application interface.&lt;/p&gt;

&lt;p&gt;The server generates the HTTP response content dynamically based on the request parameters. It consists of a standard HTML document. &lt;strong&gt;This document comprises a &lt;code&gt;body&lt;/code&gt; section, which defines the layout of the interface (what we see on the screen), and a &lt;code&gt;head&lt;/code&gt; section&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F8380%2F1%2ABTFK_IjVbYR-z3Hzo5dBFQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F8380%2F1%2ABTFK_IjVbYR-z3Hzo5dBFQ.png" alt="How HTML format HTTP requests work in Rails"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;head&lt;/code&gt; contains document meta information (name, viewport configuration, etc.) and &lt;code&gt;links&lt;/code&gt;. These links point to static assets the application requires to render the document correctly. For example, the &lt;code&gt;.css&lt;/code&gt; code to style our HTML or, more relevant for us, the javascript code to run our front-end business logic. Per each link present, the browser will perform an additional &lt;code&gt;HTTP&lt;/code&gt; request to download each asset and load it. Javascript assets (&lt;code&gt;.js&lt;/code&gt; files) are loaded by executing their inner code in a similar way we could do manually by copying the file content and pasting it into the javascript console of the web browser.&lt;/p&gt;

&lt;p&gt;Unlike server &lt;code&gt;HTML&lt;/code&gt; responses, &lt;strong&gt;asset requests are static: they always return the same content&lt;/strong&gt;. This allows us to generate our application asset responses (the asset files) once and use them in all asset requests (this is why asset files are precompiled, generated, before launching a Rails application).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F8610%2F1%2AUDzOAVjMGe4XnyV4xQxqQw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F8610%2F1%2AUDzOAVjMGe4XnyV4xQxqQw.png" alt="how web browser load Rails assets"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;Rails&lt;/code&gt; asset pipeline generates the assets&lt;/strong&gt;. The process is simple: the pipeline processes all the asset files in the assets folder, generates the final asset files, and places them under the public folder. This folder is, as the name says, public. Web browsers can access the resources available via the link tags in the HTTP responses' head.&lt;/p&gt;

&lt;p&gt;The Javascript code of a Rails application is located in two folders: the &lt;code&gt;app/javascript&lt;/code&gt; folder and the &lt;code&gt;node_modules&lt;/code&gt; folder, which contains our javascript project dependencies. &lt;strong&gt;To serve our javascript code as a &lt;code&gt;.js&lt;/code&gt; asset, we must pack all this code in a set of self-contained files that the asset pipeline can process and the web browsers load&lt;/strong&gt;. We need to bundle our javascript code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Javascript bundling in Rails
&lt;/h2&gt;

&lt;p&gt;Quoting the definition from &lt;a href="https://nextjs.org/learn/foundations/how-nextjs-works/bundling" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt;: &lt;em&gt;Bundling is the process of resolving the web of dependencies and merging (or ‘packaging’) the files (or modules) into optimized bundles for the browser&lt;/em&gt;. &lt;strong&gt;We can bundle our javascript projects with javascript bundlers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F9276%2F1%2AgO3KWritZVyOMEHOzKFr2A.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F9276%2F1%2AgO3KWritZVyOMEHOzKFr2A.png" alt="javascript digest flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most of the bundlers work similarly. &lt;strong&gt;They provided an API with methods to generate bundles and optional parameters for customized outputs&lt;/strong&gt;. Depending on the tool you use, these sets of parameters might vary. Most of them can also extend their capabilities with additional plugins.&lt;/p&gt;

&lt;p&gt;When selecting a bundler for your project, there are two key aspects to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Speed&lt;/strong&gt;: &lt;strong&gt;the faster the bundler bundles your code, the better&lt;/strong&gt;. In production environments, where the code does not change, speed is irrelevant (you bundle your code once). In development, however, you want to test changes in your code as fast as possible. Since you need to re-bundle all javascript code on each change for it, having a fast bundler is critical for a smooth development experience.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Optimization&lt;/strong&gt;: &lt;strong&gt;bundlers use multiple optimization techniques to keep your bundle small&lt;/strong&gt; and to take the most advantage of web browser caching capabilities. The bundle is an asset your application will serve in every HTTP request: smaller bundles and the use of caching lead to faster load times and smoother end-user experiences per request.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rails &lt;strong&gt;applications are &lt;em&gt;bundler-agnostic&lt;/em&gt;. They do not care how you bundle your javascript code&lt;/strong&gt;. It just expects whatever comes from the bundler to be placed under app/assets, so the asset pipeline processes it. We can see this in the official &lt;a href="https://github.com/rails/jsbundling-rails" rel="noopener noreferrer"&gt;jsbundling-rails&lt;/a&gt; gem, which consists of scripts to install different bundlers and configure a default npm build command to generate our bundles—no interaction whatsoever with the Rails configuration. &lt;strong&gt;This &lt;em&gt;black-box&lt;/em&gt; bundler logic allows us to change and update our bundler system without tuning any other aspect of our &lt;code&gt;Rails&lt;/code&gt; application&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;to bundle our javascript code in &lt;code&gt;Rails&lt;/code&gt; we need to install a bundler, run it, and ensure the resulting bundle is placed under any of the asset paths configured in our application so that the asset pipeline can digest it&lt;/strong&gt;. Understanding this, the question is now, what do we bundle?&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundling Stimulus
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://stimulus.hotwired.dev/" rel="noopener noreferrer"&gt;Stimulus&lt;/a&gt; is a minimalist Javascript framework&lt;/strong&gt;. It can be classified in the same category as other frameworks, such as React, Vue, or Ember. These ones are complex tools with multiple modules and utilities to build complex web applications: components, routers, HTML template engines, services, etc. Stimulus omits all these framework parts and &lt;strong&gt;relies on already rendered HTML (server-side HTML), and HTML data attributes to attach simple javascript logic to it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works with &lt;code&gt;controllers&lt;/code&gt;, javascript classes that can be attached to any part of our page&lt;/strong&gt;. When a Stimulus application is launched, controller classes must be registered with an &lt;code&gt;identifier&lt;/code&gt;. The application will observe changes in the page HTML. When an element includes a &lt;code&gt;data-controller&lt;/code&gt; attribute with a specific &lt;code&gt;identifier&lt;/code&gt;, the application will identify the corresponding controller class and generate an instance of it attached to the element. This approach makes it significantly easy to reuse our javascript logic in our views since it is decoupled from our HTML (in comparison with traditional component-oriented approaches from other frameworks).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Since &lt;code&gt;Rails&lt;/code&gt; renders &lt;code&gt;HTML&lt;/code&gt; in the backend, &lt;code&gt;Stimulus&lt;/code&gt; is the best solution to sprinkle our views with simple yet powerful javascript code&lt;/strong&gt;. Controllers are placed under the &lt;code&gt;app/javascript/controllers&lt;/code&gt; folder. To run the Stimulus application, we define a javascript initialization script. This is usually done under &lt;code&gt;app/javascript/application.js&lt;/code&gt;. This script creates an instance of a Stimulus application and then registers a list of imported controllers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We must pass this &lt;code&gt;application.js&lt;/code&gt; file to our bundler to bundle a Stimulus application since it contains the reference to our controllers and the logic of how the application can be launched&lt;/strong&gt;. The bundler will go from there through all the imported controllers and their specific dependencies to generate a bundle containing all the required code to run the application in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  And let’s not forget about the transpiler!
&lt;/h2&gt;

&lt;p&gt;We have already introduced all the key concepts to start working with javascript in our Rails application. However, there is still a missing piece in our puzzle to bundle ready-for-production javascript code in our applications: the transpiler.&lt;/p&gt;

&lt;p&gt;Many &lt;code&gt;Rails&lt;/code&gt; developers consider that the less code you have in your &lt;code&gt;Rails&lt;/code&gt; applications, the better. The main reason to think this is that javascript can be hard to test.&lt;/p&gt;

&lt;p&gt;Javascript web-browser execution environment requires browser-makers to keep their browsers updated with the latest &lt;code&gt;ECMAScript&lt;/code&gt; specification releases. This means that newer (and not so new) released javascript language features might not be compatible with some commercial web browsers or even behave differently from the specification. As a result, developing javascript code that runs in all web browsers can be challenging. Imagine going one by one testing your code not just in all commercial web browsers but in different versions of each one (which might have different implementations of the javascript language).&lt;/p&gt;

&lt;p&gt;To solve part of this problem, we can use transpilers. A &lt;strong&gt;transpiler rewrites javascript code targeting a specific &lt;code&gt;ECMAScript&lt;/code&gt; version&lt;/strong&gt;. By doing so, we can take advantage of the latest javascript features without compromising the browser compatibility of our code. &lt;strong&gt;Transpilers use polyfills, which specify how new javascript features can be translated into javascript code compatible for older versions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In our case, we will use &lt;code&gt;Babel&lt;/code&gt; with &lt;code&gt;CoreJS&lt;/code&gt; to transpile our code. This transpiler can be attached to most of the bundlers, running the transpiling process in parallel with the bundling, generating both highly optimized and compatible javascript code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Configure a Rails application with Stimulus, esbuild and babel.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;h2&gt;
  
  
  esbuild
&lt;/h2&gt;

&lt;p&gt;We will use &lt;code&gt;esbuild&lt;/code&gt; in our project. &lt;code&gt;esbuild&lt;/code&gt; &lt;strong&gt;is a bundler that stands for its speed&lt;/strong&gt;. It does not have the same level of maturity or popularity as other available bundlers, such as &lt;code&gt;webpacker&lt;/code&gt;, and it still misses some useful optimization options (such as code splitting, which is still in the project roadmap at the time I write this post). Still, its growing popularity and blazing-fast bundling times are reasons enough to choose it as our preferred option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2800%2F1%2AXYqcxMD_cZt7AhP7Q1LWEA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2800%2F1%2AXYqcxMD_cZt7AhP7Q1LWEA.png" alt="esbuild benchmark"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start by installing &lt;code&gt;esbuild&lt;/code&gt; in your project. Open a terminal window, navigate to your &lt;code&gt;Rails&lt;/code&gt; project, and install the &lt;code&gt;esbuild&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add esbuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, the &lt;code&gt;esbuild&lt;/code&gt; CLI should be available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In your terminal window running this command:&lt;/span&gt;
esbuild &lt;span class="nt"&gt;--version&lt;/span&gt;                              
&lt;span class="c"&gt;# Should return the installed esbuild version number&lt;/span&gt;
0.15.10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can bundle javascript code with &lt;code&gt;esbuild --bundle&lt;/code&gt;. It requires two parameters: &lt;code&gt;entryPoints&lt;/code&gt;, the list of javascript files we want to bundle, and &lt;code&gt;outdir&lt;/code&gt;, the directory path where the bundler outcome will be placed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Produces dist/entry_point.js and dist/entry_point.js.map&lt;/span&gt;
esbuild &lt;span class="nt"&gt;--bundle&lt;/span&gt; entry_point.js &lt;span class="nt"&gt;--outdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command has multiple optional parameters to optimize the resulting bundle to your needs. You can find the entire list in the &lt;a href="https://esbuild.github.io/api/" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;. To properly configure the bundle with the right options, it is important to understand how each customization option works and what your needs are.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;minify&lt;/code&gt; optimizes your bundle size using different compression techniques, like removing whitespaces from code. In production environments, the &lt;code&gt;minify&lt;/code&gt; option is a must-use; we want to reduce the weight of our assets as much as possible. In development, however, removing whitespace leads to code that is harder to read and thus more difficult to debug, so you should not use this option.&lt;/p&gt;

&lt;p&gt;A good way to learn about the different options and their outcomes is also by experimenting with them. Let’s start by bundling something simple. We will set up the javascript entry point for our application, consisting of a simple script that prints the current date-time in an infinite loop in the console:&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="c1"&gt;// Entrypoint for our Javascript code. It might already exists in your project.&lt;/span&gt;
&lt;span class="c1"&gt;// This is just a simple example to show how bundling works.&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Loading Javascript asset&lt;/span&gt;&lt;span class="dl"&gt;'&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;printTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;())&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;setTimeLogger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setInterval&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;printTime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Starting script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;setTimeLogger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now, let’s bundle it. We will point the bundle directory to the app/assets folder, as the Rails asset pipeline requires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;esbuild &lt;span class="nt"&gt;--bundle&lt;/span&gt; app/javascript/application.js &lt;span class="nt"&gt;--outdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app/assets/builds &lt;span class="nt"&gt;--minify&lt;/span&gt;

  app/assets/builds/application.js      200b 
  app/assets/builds/application.js.map  536b 

⚡ Done &lt;span class="k"&gt;in &lt;/span&gt;14ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting bundle looks like this:&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="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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading Javascript asset&lt;/span&gt;&lt;span class="dl"&gt;"&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;o&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&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;e&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;&lt;span class="nf"&gt;setInterval&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;o&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e3&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Starting script&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="nf"&gt;e&lt;/span&gt;&lt;span class="p"&gt;();})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;minify&lt;/code&gt; option removes all line breaks and whitespace and renames functions and variables with shorter names. This results in a significantly smaller bundle than the one generated without the minification option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;esbuild &lt;span class="nt"&gt;--bundle&lt;/span&gt; app/javascript/application.js &lt;span class="nt"&gt;--outdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app/assets/builds

  app/assets/builds/application.js  278b 

⚡ Done &lt;span class="k"&gt;in &lt;/span&gt;4ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the same time, this refactoring makes it extremely hard to read the code and map variables and methods from the bundle to the actual code. If there are bugs in our code, error messages will be hard to understand and trace back in our code. Remember, in case of doubt, to play with the different &lt;code&gt;esbuild&lt;/code&gt; parameters and inspect the resultant bundle when exploring new bundle optimizations in your project.&lt;/p&gt;

&lt;p&gt;The bundle generated is ready to be processed by the pipeline and loaded by the web browser. If you want to test how it works on the browser, you can always copy its code and paste it into the browser console:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2864%2F1%2AtowaqRiMo1XWyewMCPSLag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2864%2F1%2AtowaqRiMo1XWyewMCPSLag.png" alt="Result of pasting our bundle in the web browser console. The script runs, printing the current timestamp in an infinite loop"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To automate bundling, we can define a &lt;code&gt;npm&lt;/code&gt; &lt;code&gt;build&lt;/code&gt; command that runs &lt;code&gt;esbuild&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build:esbuild"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"esbuild --bundle app/javascript/application.js --outdir=app/assets/builds --minify --sourcemap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it possible to bundle our code by running the command &lt;code&gt;yarn build:esbuild&lt;/code&gt; in our terminal, which is way better than typing the previous &lt;code&gt;esbuild&lt;/code&gt; command. Still, this bundling system is not optimal. The CLI approach can lead to very long bundle commands, hard to read and maintain. Instead, we can use the &lt;code&gt;esbuild&lt;/code&gt; javascript API to define our bundle script in javascript, way easier to work with:&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;// config/esbuild.mjs&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;esbuild&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;esbuild&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rails&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;esbuild-rails&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;babel&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;esbuild-plugin-babel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;esbuild&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Path to application.js folder&lt;/span&gt;
    &lt;span class="na"&gt;absWorkingDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app/javascript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// Application.js file, used by Rails to bundle all JS Rails code&lt;/span&gt;
    &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;// Destination of JS bundle, points to the Rails JS Asset folder&lt;/span&gt;
    &lt;span class="na"&gt;outdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app/assets/builds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// Enables watch option. Will regenerate JS bundle if files are changed&lt;/span&gt;
    &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--watch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// Split option is disabled, only needed when using multiple input files&lt;/span&gt;
    &lt;span class="c1"&gt;// More information: https://esbuild.github.io/api/#splitting (change it if using multiple inputs)&lt;/span&gt;
    &lt;span class="na"&gt;splitting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;chunkNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chunks/[name]-[hash]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Remove unused JS methods&lt;/span&gt;
    &lt;span class="na"&gt;treeShaking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Adds mapping information so web browser console can map bundle errors to the corresponding&lt;/span&gt;
    &lt;span class="c1"&gt;// code line and column in the real code&lt;/span&gt;
    &lt;span class="c1"&gt;// More information: https://esbuild.github.io/api/#sourcemap&lt;/span&gt;
    &lt;span class="na"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// Compresses bundle&lt;/span&gt;
    &lt;span class="c1"&gt;// More information: https://esbuild.github.io/api/#minify&lt;/span&gt;
    &lt;span class="na"&gt;minify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// Removes all console lines from bundle&lt;/span&gt;
    &lt;span class="c1"&gt;// More information: https://esbuild.github.io/api/#drop&lt;/span&gt;
    &lt;span class="na"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--production&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;console&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="p"&gt;[],&lt;/span&gt;
    &lt;span class="c1"&gt;// Build command log output: https://esbuild.github.io/api/#log-level&lt;/span&gt;
    &lt;span class="na"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Set of ESLint plugins&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="c1"&gt;// Plugin to easily import Rails JS files, such as Stimulus controllers and channels&lt;/span&gt;
      &lt;span class="c1"&gt;// https://github.com/excid3/esbuild-rails&lt;/span&gt;
      &lt;span class="nf"&gt;rails&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="c1"&gt;// Configures bundle with Babel. Babel configuration defined in babel.config.js&lt;/span&gt;
      &lt;span class="c1"&gt;// Babel translates JS code to make it compatible with older JS versions.&lt;/span&gt;
      &lt;span class="c1"&gt;// https://github.com/nativew/esbuild-plugin-babel&lt;/span&gt;
      &lt;span class="nf"&gt;babel&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;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an example of our project’s &lt;code&gt;esbuild&lt;/code&gt; bundle configuration. As you can see, we can use javascript to add comments and keep the bundle options better organized. Via &lt;code&gt;process.argv&lt;/code&gt; we can use different option values based on custom build parameters. This can be useful to define multiple configurations in the same code (for example, enabling or disabling minification in development or production). Please make sure you define your bundle configuration in a &lt;code&gt;.mjs&lt;/code&gt; file. This is required since the script uses the import method to require the &lt;code&gt;esbuild&lt;/code&gt; dependencies.&lt;/p&gt;

&lt;p&gt;With the javascript code set, we can now update our build script in our &lt;code&gt;package.json&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build:esbuild"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node config/esbuild.config.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now run &lt;code&gt;yarn build:esbuild&lt;/code&gt; again to check the configuration works properly and the code is bundled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Babel
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Babel&lt;/code&gt; is the most popular Javascript transpiler. The transpiler refactors our bundle code to maximize its compatibility with web browsers. To install &lt;code&gt;Babel&lt;/code&gt;, go to your project folder, open the terminal and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# in your terminal window:&lt;/span&gt;
yarn add &lt;span class="nt"&gt;--dev&lt;/span&gt; core-js @babel/core @babel/preset-env esbuild-plugin-babel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We listed multiple packages here. Let’s quickly describe their purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@babel/core&lt;/code&gt;: the official Babel npm package.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;esbuild-plugin-babel&lt;/code&gt;: this package allows us to integrate the &lt;code&gt;Babel&lt;/code&gt; transpiling process in the &lt;code&gt;esbuild&lt;/code&gt; pipeline. We already imported and used this package in our &lt;code&gt;esbuild&lt;/code&gt; configuration file:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// config/esbuild.mjs&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nx"&gt;plugins&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="c1"&gt;// Configures bundle with Babel. Babel configuration defined in babel.config.js&lt;/span&gt;
        &lt;span class="c1"&gt;// Babel translates JS code to make it compatible with older JS versions.&lt;/span&gt;
        &lt;span class="c1"&gt;// https://github.com/nativew/esbuild-plugin-babel&lt;/span&gt;
        &lt;span class="nf"&gt;babel&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;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@babel/preset-env&lt;/code&gt;: &lt;code&gt;Babel&lt;/code&gt; uses repositories of plugins and configuration options to transpile javascript code. These repositories are called presets. The preset-env module is an official &lt;a href="https://babeljs.io/docs/babel-preset-env" rel="noopener noreferrer"&gt;Babel preset for compiling ES2015+ syntax&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;core-js&lt;/code&gt;: this is a special kind of preset, a polyfill. The polyfill contains a library of javascript features and defines the code refactor needed for each one to make it compatible with older javascript versions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since we already told &lt;code&gt;esbuild&lt;/code&gt; to use &lt;code&gt;Babel&lt;/code&gt;, we need to configure &lt;code&gt;Babel&lt;/code&gt;. To do so, create a &lt;code&gt;babel.config.js&lt;/code&gt; file in your project root and import the defined presets:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;presets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// Javascript 2015+ syntax transpiler&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@babel/preset-env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Javascript Polyfill&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;useBuiltIns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;usage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Make sure version number matches the Core.js version installed in your project&lt;/span&gt;
      &lt;span class="na"&gt;corejs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3.28.0&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="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="nx"&gt;presets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// Fixes Core-JS $ issue: https://github.com/zloirock/core-js/issues/912&lt;/span&gt;
  &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./node_modules&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Presets can be configured with multiple optional parameters. Similar to what we mentioned for &lt;code&gt;esbuild&lt;/code&gt;, we encourage you to read the docs and find the setup that adapts best to your needs.&lt;/p&gt;

&lt;p&gt;To finish the configuration, we must tell &lt;code&gt;Babel&lt;/code&gt; the web browsers we want our code to be compatible with. Create a new file called &lt;code&gt;.browserslist.rc&lt;/code&gt; and specify your desired browser compatibility. &lt;a href="https://github.com/browserslist/browserslist#queries" rel="noopener noreferrer"&gt;You can find all the possible ways to specify the list of compatible browsers in the official docs&lt;/a&gt;. We will use a simple configuration for our project:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .browserslist.rc

# Babel Preset configuration
# --------------------------
# Defines web-browser compatibility parameters for Babel to transpile your JS code.
# This configuration is used by babel.config.js.
# More information in here.
# https://github.com/browserslist/browserslist

# Support browsers with a market share higher than 5%
&amp;gt;10%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;With everything set, you can run esbuild and see how Babel transpiles your code. To do a little experiment, we will update our script with a new method that uses a technique called destructuring assignment to define its parameter. Its syntax is not available in some older javascript versions:&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="p"&gt;...&lt;/span&gt;
&lt;span class="c1"&gt;// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;destructuringAssignment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;param&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="nx"&gt;param&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;Now compiling our bundle, we can see how the method is refactored. The new code does the same &lt;code&gt;destructuringAssigment&lt;/code&gt; does. However, its syntax is now compatible with all web browsers which do not implement this kind of assignment:&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="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="p"&gt;...&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;destructuringAssignment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;param&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_ref&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;param&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;span class="c1"&gt;//# sourceMappingURL=application.js.map&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Stimulus
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Rails&lt;/code&gt; provides multiple commands and generators to create applications with Stimulus pre-installed or install the framework from scratch. In this post, we will configure stimulus from scratch without using those helpers.&lt;/p&gt;

&lt;p&gt;Let’s start installing the &lt;code&gt;stimulus&lt;/code&gt; package. Open a terminal window in your project folder and execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add @hotwired/stimulus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s add a sample controller. &lt;a href="https://stimulus.hotwired.dev/handbook/hello-stimulus" rel="noopener noreferrer"&gt;We will use the example from the official Stimulus documentation&lt;/a&gt;. In a Rails application, Stimulus controllers are placed under the app/javascript/controllers folder. Each controller file must be suffixed with&lt;code&gt;_controller.js&lt;/code&gt; (it is a convention).&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/controllers/hello_controller.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World!&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we need to create the launch script. This script is responsible for creating a Stimulus application instance, registering all our project controllers, and launching the application in the web browser. The official Stimulus documentation also provides an example of how this script should look like. Since the script launches the application, we should place the code in &lt;code&gt;application.js&lt;/code&gt;:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Application&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;HelloController&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./controllers/hello_controller&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// Creates &amp;amp; launches a Stimulus application instance&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stimulus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Registers controller in Stimulus application instance&lt;/span&gt;
&lt;span class="nx"&gt;Stimulus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HelloController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Each controller in our project must be registered in the Stimulus application instance with an &lt;code&gt;identifier&lt;/code&gt;&lt;/strong&gt;. We chose the &lt;code&gt;identifier&lt;/code&gt; of a controller by its file and path name, excluding the &lt;code&gt;_controller.js&lt;/code&gt; filename suffix.&lt;/p&gt;

&lt;p&gt;This script has multiple problems. &lt;strong&gt;Registering our controllers one by one can be tedious&lt;/strong&gt;: any time a new controller is created, deleted or renamed, we need to update the file. On top of that, &lt;strong&gt;the idea of manually writing the &lt;code&gt;identifiers&lt;/code&gt; can lead to multiple problems: what happens if someone makes a type in the &lt;code&gt;identifier&lt;/code&gt;? What happens if different developers use different naming conventions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can solve this problem by automating the controller registering process with a script: the script could look into the controllers folder, get the list of controller files, import the respective classes defined, and register them with the identifier corresponding to the file name. Luckily, the &lt;code&gt;esbuild-rails&lt;/code&gt; package already provides the resources to implement this script.&lt;/p&gt;

&lt;p&gt;Installing the package first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add esbuild-rails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then using its esbuild plugin in our bundle configuration:&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;// config/esbuild.mjs&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="nx"&gt;plugins&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="c1"&gt;// Plugin to easily import Rails JS files, such as Stimulus controllers and channels&lt;/span&gt;
      &lt;span class="c1"&gt;// https://github.com/excid3/esbuild-rails&lt;/span&gt;
      &lt;span class="nf"&gt;rails&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;We can now define our script to import our controllers automatically:&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;// Entry point for the build script in your package.json&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hotwired/turbo-rails&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Application&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// General Controllers&lt;/span&gt;
&lt;span class="c1"&gt;// -------------------&lt;/span&gt;
&lt;span class="c1"&gt;// import all Stimulus controller files under the controllers folder&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;controllers&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./**/*_controller.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Auxiliary Methods&lt;/span&gt;
&lt;span class="c1"&gt;// -----------------&lt;/span&gt;
&lt;span class="c1"&gt;// Infer Stimulus controller name from its file&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;controllerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;defaultName&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;namespaces&lt;/span&gt; &lt;span class="o"&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;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;defaultName&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--&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="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;controllers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ns&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;return&lt;/span&gt; &lt;span class="nx"&gt;namespaces&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;application&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Set flag to true to get debbug information in the web browser console&lt;/span&gt;
&lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stimulus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;application&lt;/span&gt;

&lt;span class="nx"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;controller&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="nx"&gt;Stimulus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;controllerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;controller&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="k"&gt;default&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;Bundling our javascript code, we can see how the controllers are attached to the bundle, indicating we successfully integrated Stimulus in our Rails application:&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/assets/builds/application.js&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="c1"&gt;// We can see our controllers &amp;amp; the initialization script bundled&lt;/span&gt;
  &lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stimulus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;controller_default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;controller&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="nx"&gt;Stimulus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;controllerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;controller&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="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;controller_default2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;controller&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="nx"&gt;Stimulus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;componentControllerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;controller&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="k"&gt;default&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;Finally, copying the bundle file content into our web browser console and with the &lt;code&gt;application.debug&lt;/code&gt; flag set to true; we can see how the Stimulus application is launched. If then, we type &lt;code&gt;window.Stimulus&lt;/code&gt; in the console, we can access the application instance object:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2848%2F1%2A6LwmJ9HEwgbgbe3Kn70NrA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2848%2F1%2A6LwmJ9HEwgbgbe3Kn70NrA.png" alt="Pasting the bundle code in our web browser displays the Stimulus application start up messages. With window.Stimulus we access the Stimulus application object."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Last touches
&lt;/h2&gt;

&lt;p&gt;We already explained how to bundle our javascript code. But we are missing the last part: linking it in our &lt;code&gt;HTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To attach a link to our &lt;code&gt;application.js&lt;/code&gt; asset we need to use the &lt;code&gt;javascript_include_tag&lt;/code&gt; helper in &lt;code&gt;app/views/layout/application.html&lt;/code&gt;. This file defines a wrapper for all the views in our application. By placing the javascript helper in the head section, we make sure every HTML formatted HTTP response includes in its head a link to the application javascript asset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;app/views/layout/application.html.haml

head
  ...
  = javascript_include_tag("application", media: "all", "data-turbolinks-track" =&amp;gt; true)
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The helper renders a link tag pointing to the corresponding &lt;code&gt;.js&lt;/code&gt; asset. By setting the name parameter to &lt;code&gt;application&lt;/code&gt;, we point to our generated bundle.&lt;/p&gt;

&lt;p&gt;Once this is set, you can start developing and testing your javascript logic. Remember to run the &lt;code&gt;yarn build:esbuild&lt;/code&gt; command with the &lt;code&gt;--watch&lt;/code&gt; option. This will enable the &lt;code&gt;esbuild&lt;/code&gt; watch flag, which triggers automatic bundles when you change your controller files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn build &lt;span class="nt"&gt;--watch&lt;/span&gt; &lt;span class="nt"&gt;--development&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To deploy your application, you need to run the asset pipeline before launching your Rails application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In a terminal window&lt;/span&gt;
bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rails assets:precompile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command will try to bundle the javascript code by running the &lt;code&gt;build&lt;/code&gt; script. Since we only defined the script for development, we need to add the missing one for production, so the pipeline can generate the bundle properly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn build:esbuild --production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the pipeline, we will see the command being executed, the bundle being generated and placed under the public folder, from which web browsers will request the asset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rails assets:precompile
yarn &lt;span class="nb"&gt;install &lt;/span&gt;v1.22.19
&lt;span class="o"&gt;[&lt;/span&gt;1/4] 🔍  Resolving packages...
success Already up-to-date.
✨  Done &lt;span class="k"&gt;in &lt;/span&gt;0.29s.
yarn run v1.22.19
&lt;span class="nv"&gt;$ &lt;/span&gt;yarn build:esbuild &lt;span class="nt"&gt;--production&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;node esbuild.config.mjs &lt;span class="nt"&gt;--production&lt;/span&gt;
Dynamic import can only be supported when transforming ES modules to AMD, CommonJS or SystemJS. Only the parser plugin will be enabled.

  app/assets/builds/application.js  150.7kb

✨  Done &lt;span class="k"&gt;in &lt;/span&gt;1.75s.
yarn &lt;span class="nb"&gt;install &lt;/span&gt;v1.22.19
&lt;span class="o"&gt;[&lt;/span&gt;1/4] 🔍  Resolving packages...
success Already up-to-date.
✨  Done &lt;span class="k"&gt;in &lt;/span&gt;0.34s.
yarn run v1.22.19
&lt;span class="nv"&gt;$ &lt;/span&gt;yarn build:tailwind-config-viewer &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; yarn build:tailwind
&lt;span class="nv"&gt;$ &lt;/span&gt;tailwind-config-viewer &lt;span class="nb"&gt;export&lt;/span&gt; ./public/tailwind-config-viewer &lt;span class="nt"&gt;-c&lt;/span&gt; ./config/tailwind.config.js
&lt;span class="nv"&gt;$ &lt;/span&gt;npx tailwindcss &lt;span class="nt"&gt;--postcss&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ./app/assets/stylesheets/application.tailwind.scss &lt;span class="nt"&gt;-o&lt;/span&gt; ./app/assets/builds/application.css &lt;span class="nt"&gt;-c&lt;/span&gt; ./config/tailwind.config.js &lt;span class="nt"&gt;--minify&lt;/span&gt;

Rebuilding...

Done &lt;span class="k"&gt;in &lt;/span&gt;838ms.
✨  Done &lt;span class="k"&gt;in &lt;/span&gt;3.07s.
I, &lt;span class="o"&gt;[&lt;/span&gt;2023-02-25T23:36:12.163440 &lt;span class="c"&gt;#5705]  INFO -- : Writing /Users/alberto-mac/Desktop/Projects/Albert/Albert/public/assets/application-dd70b2d6bf41af42627f148cd9a084b6c0a0b0961780833472ec6faf19b199e7.js&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2023-02-25T23:36:12.163717 &lt;span class="c"&gt;#5705]  INFO -- : Writing /Users/alberto-mac/Desktop/Projects/Albert/Albert/public/assets/application-dd70b2d6bf41af42627f148cd9a084b6c0a0b0961780833472ec6faf19b199e7.js.gz&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Let’s try to wrap up all the lessons of this post. Javascript code is embedded in &lt;code&gt;Rails&lt;/code&gt; views as assets via link tags in &lt;code&gt;HTML&lt;/code&gt; response heads. To turn our javascript code into assets, we need to bundle it first, so &lt;code&gt;Rails&lt;/code&gt; can process the resulting bundle in the &lt;code&gt;asset pipeline&lt;/code&gt; and serve it to the web browser.&lt;/p&gt;

&lt;p&gt;We can bundle our code with javascript bundlers such as &lt;code&gt;esbuild&lt;/code&gt;. The bundler merges all our code with its dependencies in optimized files. These optimizations can be configured via different bundling parameters and third-party plugins, allowing us to shrink the size of our bundles and maximize their compatibility with web browsers, among other possibilities.&lt;/p&gt;

&lt;p&gt;Finally, with &lt;code&gt;Stimulus&lt;/code&gt;, &lt;code&gt;Rails&lt;/code&gt; developers can write powerful javascript code with a simple framework that relies heavily on Rails server-side rendering to keep the entire javascript stack of your project as minimal as possible.&lt;/p&gt;

&lt;p&gt;Remember always to keep your javascript code to the minimum possible and rely on &lt;code&gt;CSS&lt;/code&gt; capabilities for simple UI interfaces. The disparity of javascript implementations present on the different web browsers in the market can make it difficult to build and test code that works in all possible scenarios.&lt;/p&gt;

&lt;p&gt;And last but not least: read, experiment, and get familiar with the bundlers, transpilers, and other additional tools you find on your way to configuring your Rails application. It can be an overwhelming task due to all the different technologies involved and how complex they are, but definitely, the effort will pay off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code in Github
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Albert-Markdown-Editor/Albert/pull/166/commits/763700c5e8f15fda382e543e79a010c3127e3953" rel="noopener noreferrer"&gt;152 basic application layout by AlbertoHdezCerezo · Pull Request #166 · Albert-Markdown-Editor/Albert&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://esbuild.github.io/" rel="noopener noreferrer"&gt;esbuild — An extremely fast bundler for the web&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://babeljs.io/docs/" rel="noopener noreferrer"&gt;What is Babel? · Babel&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://stimulus.hotwired.dev/" rel="noopener noreferrer"&gt;Stimulus: A modest JavaScript framework for the HTML you already have.&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://guides.rubyonrails.org/asset_pipeline.html" rel="noopener noreferrer"&gt;The Asset Pipeline — Ruby on Rails Guides&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Any Questions?
&lt;/h2&gt;

&lt;p&gt;If you have reached this far, thank you so much for reading my article. It has been quite challenging to wrap up so many concepts, tools, and ideas in a single post, and I am pretty sure there is still much I can improve. This is why I keep writing more and more (apart from the fun, of course).&lt;/p&gt;

&lt;p&gt;Did you find the post clear enough? Or did you get lost or stuck at any point? As always, feel free to write down in the comments or reach me for any support you need to set up your own bundling process. I will be happy to give a hand if possible.&lt;/p&gt;

&lt;p&gt;Best regards, and see you in the next post!&lt;/p&gt;

</description>
      <category>rails</category>
      <category>esbuild</category>
      <category>babel</category>
      <category>stimulus</category>
    </item>
    <item>
      <title>Multistep Form (Wizard) — Rails Recipes</title>
      <dc:creator>Alberto Hernandez Cerezo</dc:creator>
      <pubDate>Sat, 11 Feb 2023 13:37:48 +0000</pubDate>
      <link>https://dev.to/pascualtalcual/multistep-form-wizard-rails-recipes-3emn</link>
      <guid>https://dev.to/pascualtalcual/multistep-form-wizard-rails-recipes-3emn</guid>
      <description>&lt;h2&gt;
  
  
  Multistep Form (Wizard) — Rails Recipes
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Anatomy of a Multistep Form
&lt;/h2&gt;

&lt;p&gt;A multistep form is a variant of a wizard. &lt;strong&gt;A wizard is a usability tool. It represents a task as a set of steps arranged in a prescribed order&lt;/strong&gt;. Users must complete each step in the given order to achieve its goal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multistep forms split complex forms into smaller parts, grouping their fields by different criteria&lt;/strong&gt;. This results in a less daunting form-filling experience for end users. &lt;strong&gt;When combined with instructions, multistep forms significantly increase the ease of use of our applications&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0actmjot1rt4h7usecws.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%2F0actmjot1rt4h7usecws.png" alt="Comparison of a regular form with a multistep form" width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Their main drawback is efficiency&lt;/strong&gt;. Filling a multistep form usually takes more time than filling a single form (if you are familiar with the process). This is due to the navigation between steps, which does not exist in standard forms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How you implement this navigation can significantly impact the user experience&lt;/strong&gt;: will you allow users to navigate back and forth to any step in the wizard or just to the next and previous steps? is previous step information visible in the current step? &lt;strong&gt;Bad design choices can lead to slow and frustrating form-filling experiences&lt;/strong&gt; that discourage users from filling out our form and even using our system.&lt;/p&gt;

&lt;p&gt;This recipe will teach you to create reusable and customizable multistep forms in Rails.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;In our application we manage Projects. A project is defined by a name and a description. A Project can have multiple Books associated to it. A Book is defined by a title and a description. We want to implement a multistep form to create Projects. The first step will require the project basic information, and the second, optional, information about a Book associated to the Project.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Recipe
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;(Required) Rails **ViewComponent **gem installed&lt;/strong&gt;: ViewComponent wraps Rails views into objects. This makes it easier to test our views and provides additional methods for a better developer experience working with them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;(Optional) *&lt;em&gt;TailwindCSS *&lt;/em&gt;&lt;/strong&gt;PostCSS &lt;strong&gt;plugin installed&lt;/strong&gt;: this CSS framework removes the need to write CSS in our code by providing a powerful set of CSS classes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Model
&lt;/h2&gt;

&lt;p&gt;In Rails, we use forms to &lt;em&gt;create&lt;/em&gt; and &lt;em&gt;update&lt;/em&gt; records. These operations are performed via POST, PUT, and PATCH HTTP requests submitted from a single form. Requests are processed following the same process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Rails Controller layer parses form request data and asks the Model layer to perform the requested operation with the extracted information.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rails Model layer processes request data, validates it, and performs the requested operation if it is valid. The result of the operation is sent back from the Model to the Controller.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rails Controller layer requests the View layer to render a response based on the operation outcome. Successful operations render a view of the operation results. Unsuccessful operations render the submitted form with the form request data and error messages.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;In a multistep form, operations only succeed when all steps are completed. Unsuccessful operations will re-render the multistep form, displaying the next step to complete&lt;/strong&gt;. Once the last step is completed and submitted, the operation is then performed.&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%2Flos2pmt436g4ajolefge.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%2Flos2pmt436g4ajolefge.png" alt="Business logic of a multistep form" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our first task is to implement the logic to validate whether all steps in a multistep form are completed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rails ActiveRecord::Validations &lt;a href="https://guides.rubyonrails.org/active_record_validations.html" rel="noopener noreferrer"&gt;API&lt;/a&gt;, included in ApplicationRecord Model classes, defines the valid? method. This method is executed on every &lt;em&gt;creation *and *update&lt;/em&gt; operation. It evaluates all Model validation conditions, returning true when all validations pass. Only valid? operations can persist information in the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The validation for our multistep form must check the current step index in the form request corresponds to its last step&lt;/strong&gt;. We need two attributes to track these variables: current_step to store the current step index and total_steps to store the number of steps in a multistep form. Since both the validation and the attributes are the same for all models, we can encapsulate them in a concern that can be reused across our Model classes:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/models/concerns/multistep_form_model.rb
​
module MultistepFormModel
  ERROR_ATTRIBUTE = :current_step
  ERROR_DETAIL = :incompleted_multistep_form
​
  extend ActiveSupport::Concern
​
  included do
    # Defines two new attributes in including Model class
    attr_accessor :current_step, :total_steps
​
    # Adds new validation rule in including Model class
    validate :all_multistep_form_steps_completed
  end
​
  private
​
  def all_multistep_form_steps_completed? = current_step == total_steps
​
  def all_multistep_form_steps_completed
    errors.add(ERROR_ATTRIBUTE, ERROR_DETAIL) unless all_multistep_form_steps_completed?
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The all_multistep_form_steps_completed validation will be checked in all creation and update operations. &lt;strong&gt;Since there might be scenarios where you want to update records without using a multistep form, we need to ensure the validation is only checked when requests are submitted from a multistep form&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/models/concerns/multistep_form_model.rb
​
module MultistepFormModel
  ...
  included do
    # Adds new validation rule in including Model class
    validate :all_multistep_form_steps_completed,
      if: -&amp;gt; { current_step.present? &amp;amp;&amp;amp; total_steps.present? }
  end
  ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Adding this if statement, we ensure the validation is executed only when both current_step and total_steps attributes have a value (which will happen when using multistep forms only).&lt;/p&gt;

&lt;p&gt;Finally, we can update our models by including our multistep form concern so that we can use them with our multistep form:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/models/project.rb
​
class Project &amp;lt; ApplicationRecord
  ...
  # Now our Project class has current_step and total_steps attributes, and the multistep validation rule
  include MultistepFormModel
  ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Controller
&lt;/h2&gt;

&lt;p&gt;Controllers are responsible for parsing form request data and passing it down to the Model class. To work with multistep forms, &lt;strong&gt;we need to update the controller’s strong parameters to ensure the **curret_step **and **total_steps **parameters are parsed&lt;/strong&gt; and passed to the Model class.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/controllers/projects_controller.rb

class ProjectsController &amp;lt; ApplicationController
  ...
  private
  ...
  def project_params
    params
      .require(:project)
      .permit(:name, :description, :total_steps, :current_step, books_attributes: [:title, :description])
  end
  ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  View
&lt;/h2&gt;

&lt;p&gt;Let’s decompose a multistep form in ViewComponents. A multistep form is a form split into multiple parts (steps). In terms of components, this can be modeled as a MultistepFormComponent whose content is composed of multiple StepComponents.&lt;/p&gt;

&lt;p&gt;Each step contains a set of form fields. A step is completed when all step-required fields are filled with valid data. &lt;strong&gt;A **StepComponent **renders the fields corresponding to a multistep form step and provides a simple interface to know, given the form data, if the step it represents is completed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We previously talked about the valid? method and how it determines whether form data is valid. In addition to this behavior, *&lt;strong&gt;*valid? **also updates the **Model **object with a &lt;a href="https://api.rubyonrails.org/classes/ActiveModel/Errors.html" rel="noopener noreferrer"&gt;dictionary containing validation errors&lt;/a&gt;&lt;/strong&gt;. It can be accessed via model.errors, which returns an ActiveModel::Errors object.&lt;/p&gt;

&lt;p&gt;To determine if a StepComponent is completed, we can use the form.object.errors object. Given a step with a set of input fields, we can confirm the step is completed if the form.object.errors object does not include any error associated with the step fields.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# How to check whether a step is completed.
def completed?(form_object)
  input_attributes.none? { |attr| form_object.errors.key?(attr) }
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This method also reveals the information a step needs to be rendered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;An ActionView::Helpers::FormBuilder object to render the step fields and fetch the form data via the .object method.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A list of the input_attributes fields it renders.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Not revealed from the method but obvious, we also need to know the index of the step in the multistep form.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With this in mind, we can write down our StepComponent class:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/components/common/multistep_form_component/step_component.rb

module Common
  class MultistepFormComponent::StepComponent &amp;lt; ApplicationComponent
    class &amp;lt;&amp;lt; self
      def title = raise "StepComponent #{name} does not define title method"

      def input_attributes = raise "StepComponent #{name} does not define list of input attributes"

      def completed?(form_object)
        input_attributes.none? { |attr| form_object.errors.key?(attr) }
      end
    end

    attr_reader :multistep_component, :index, :form

    def initialize(multistep_component:, index:, html_attributes: {})
      @index = index
      @form = multistep_component.form
      @multistep_component = multistep_component
      super(html_attributes:)
    end

    def call
      raise "
        #{self.class.name} is a StepComponent. StepComponents are abstract by default.
        Components inheriting from it must define its own template or call method.
      "
    end

    def current_step? = index == multistep_component.current_step

    def completed? = self.class.completed?(multistep_component.form.object)
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We slightly changed our previous specification and required a MultistepFormComponent instance instead of a FormBuilder object to initialize the component. This will allow our StepComponents to access additional information from the parent component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The **StepComponent **is an abstract component&lt;/strong&gt;. It is not designed to be rendered but extended by other StepComponent classes. For our Project form steps, we will define two new components, each one for each steps we want to render:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/components/common/projects/form_component/project_details_component.rb

module Common
  class Projects::FormComponent::ProjectDetailsComponent &amp;lt; Common::MultistepFormComponent::StepComponent
    class &amp;lt;&amp;lt; self
      def title = "Project Details"

      def input_attributes = %w[name]
    end
    ...
  end
end

# app/components/common/projects/form_component/project_deliverables_component.rb

module Common
  class Projects::FormComponent::ProjectDeliverablesComponent &amp;lt; Common::MultistepFormComponent::StepComponent
    class &amp;lt;&amp;lt; self
      def title = "Project Deliverables"

      # Example of a step component with nested attributes / nested form fields
      def input_attributes = %w[books.title]
    end
    ...
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The **MultistepFormComponent **is also an abstract component&lt;/strong&gt;. It defines the list of StepComponents which compose the multistep form and is responsible for rendering them in the given order. Internally the component renders a form in which steps are placed. To generate the form, the component requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The URL where the form data will be submitted.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And the form data itself. This data represents the record we want to create or update. In each iteration of the multistep form cycle, the data will represent the information provided by the user in the previous form submission.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The multistep form must also render hidden fields for the current_step and total_step attributes, ensuring these attributes are present in the submission and triggering the model validation logic:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/components/common/multistep_form_component.rb

module Common
  class MultistepFormComponent &amp;lt; ApplicationComponent
    class &amp;lt;&amp;lt; self
      def steps = raise "#{name} MultistepFormComponent does not define steps class model"
    end

    delegate :steps, to: :class

    attr_reader :form_url, :back_url, :model, :form

    def initialize(form_url:, back_url:, model:, html_attributes: {})
      @form_url = form_url
      @back_url = back_url
      @model = model
      super(html_attributes:)
    end

    def call
      form_with(url:, model: model) do |form|
        @form = form

        concat(form.hidden_field(:total_steps, value: steps.count))
        concat(form.hidden_field(:current_step, value: current_step_index))

        steps.each_with_index do |step, index|
          concat(render step.new(multistep_form: self, index:))
        end
      end
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In the code above, the call method renders all steps StepComponents in the given order. When the form is rendered, we store a reference to the form builder object in the &lt;a class="mentioned-user" href="https://dev.to/form"&gt;@form&lt;/a&gt; attribute. This will allow each step component instance to access the builder object to render their form fields.&lt;/p&gt;

&lt;p&gt;We can now define our project FormComponent by extending the class and specifying the step of StepComponent classes that define its form:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/components/common/projects/form_component.rb

module Common
  class Projects::FormComponent &amp;lt; Common::MultistepFormComponent
    class &amp;lt;&amp;lt; self
      def steps = [ProjectDetailsComponent, ProjectDeliverablesComponent]
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Let’s now define the logic for navigating between steps. Our current component displays all form steps at once. The goal is to show one step at a time and be able to show and hide others’ steps when navigating back and forth in the form.&lt;/p&gt;

&lt;p&gt;There are multiple ways to navigate between steps: some only allow navigation forward until all steps are completed; in others, it is possible to navigate back and forth one step and a time, and others even allow displaying multiple previous steps at once. &lt;strong&gt;Our multistep form should be flexible enough to implement all possible navigation strategies&lt;/strong&gt;. However, it should not implement any navigation strategy. We will delegate it to the components extending the MultistepFormComponent class.&lt;/p&gt;

&lt;p&gt;We can hide and show steps using simple checkbox and radio HTML inputs. A checkbox is a simple box that can be checked. A radio is a group of checkboxes in which only one can be checked. CSS can target the checked status of these elements via the :checked selector. This allows styling HTML elements based on the status of a checkbox or radio.&lt;/p&gt;

&lt;p&gt;We can assign a radio and checkbox input to each step in our form and style the step according to its state so it is only visible when the corresponding input is checked. We will define a new method to wrap each step instance within a tag containing a radio and checkbox to achieve it.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/components/common/multistep_form_component.rb

...
  def call
    ...
      steps.each_with_index do |step, index|
        concat(step_wrapper(step, index) { render step.new(multistep_form: self, index:) })
      end
    ...
  end

  def radio_id(step_index) = "#{object_id}_radio_#{step_index}"

  def checkbox_id(step_index) = "#{object_id}_radio_#{step_index}"

  def step_wrapper(step, index, html_attributes: {}, &amp;amp;content)
    tag.div(**html_attributes) do
      concat(
        radio_button_tag(
          radio_id(nil),
          1,
          current_step_index == index,
          id: radio_id(index),
          class: "hidden peer/radio"
        )
      )
      concat(
        check_box_tag(
          checkbox_id(index),
          1,
          current_step_index == index,
          id: checkbox_id(index),
          class: "hidden peer/checkbox"
        )
      )
      concat(capture(&amp;amp;content))
    end
  end
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The step_wrapper method yields the steps in a div tag together with a checkbox and radio element. Via two new id methods, we assign an id and name to them. This allows targeting the inputs from buttons that can check and uncheck the input they point to when clicked. Finally, we use the tailwind hidden class to hide the inputs and the peer class to style the sibling step container based on the input checked status.&lt;/p&gt;

&lt;p&gt;Component extending the MultistepFormComponent can update this method defining common elements present in all steps and using the peers to show and hide steps based on the checked status.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/components/common/projects/form_component.rb

...
def step_wrapper(step, index, html_attributes: {}, &amp;amp;content)
  selected = index == current_step_index

  options = (selected ? { "aria-current": "step" } : {})

  super(step, index, html_attributes: options) do
    concat(
      tag.label(
        for: index &amp;lt;= current_step_index ? radio_id(index) : "",
        class: "
          w-full inline-flex items-center px-4 py-2 gap-4 border-2 text-zinc-500 border-zinc-500
          peer-checked/radio:bg-zinc-500 peer-checked/radio:text-white
        "
      ) do
        concat(tag.p(index + 1, class: "h-6 w-6 text-center rounded-full text-white bg-zinc-500"))
        concat(tag.p(step.title))
      end
    )
    concat(
      tag.div(
        class: "
          hidden px-4 py-6
          peer-checked/radio:block
        "
      ) do
        concat(capture(&amp;amp;content))
        concat(
          tag.div(class: "mt-5 w-full inline-flex gap-2 items-center justify-end") do
            concat(link_to("cancel", back_url, class: "px-4 py-2 bg-zinc-500 text-white")) if index == 0
            concat(tag.label("previous", for: radio_id(index - 1), class: "px-4 py-2 bg-zinc-500 text-white")) if index &amp;gt; 0
            concat(form.submit("next", class: "cursor-pointer px-4 py-2 bg-zinc-500 text-white")) if index &amp;lt; (steps.count - 1)
            concat(form.submit("create project", class: "cursor-pointer px-4 py-2 bg-zinc-500 text-white")) if index == (steps.count - 1)
          end
        )
      end
    )
  end
end
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The project FormComponent extends the step_wrapper method to wrap our steps with a header and a set of navigation buttons. In lines 13 and 24 we can see some peer-checked/radio prefixed CSS classes. They style the step wrapper when the radio checkboxes are checked, revealing it. Our steps are hidden by default and only revealed when the sibling radio checkbox is checked. We use the radio to display one step at a time. If we want to display multiple steps simultaneously, we should target the checkbox instead.&lt;/p&gt;

&lt;p&gt;In line 31 there is the logic to navigate a step back in the form. We use the MultistepFormComponent radio_id method to map the label to the radio input from the previous step. When we click the label, the radio button will be checked. This will hide the current step and reveal the previous one.&lt;/p&gt;

&lt;p&gt;To navigate forward, we always need to submit the form. If, after form submission, all previous steps are completed, the multistep will render the next step. Otherwise, the form will step back to the incompleted step.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/components/common/multistep_form_component.rb
...
def first_incompleted_step_index
  steps.find_index { |s| !s.completed?(model) }
end

def current_step_index
  previous_step = model.current_step&amp;amp;.to_i

  @current_step_index =
    if model.current_step.nil?
      0
    else
      next_step = previous_step + (steps[previous_step].completed?(model) ? 1 : 0)
      [next_step, first_incompleted_step_index].compact.min
    end
end
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Updating the MultistepFormComponent with these methods provides a way for steps to determine which is the current step in the form. If a specific multistep form defines a different, forward navigation logic, the current_step_index can be overwritten to implement that specific navigation logic. The MultistepFormComponent can use this method to set the status of the radio and checkboxes:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/components/common/multistep_form_component.rb
...
def step_wrapper(step, index, html_attributes: {}, &amp;amp;content)
  tag.div(**html_attributes) do
    concat(
      radio_button_tag(
        ...
        # If true radio is checked
        current_step_index == index,
        ...
      )
    )
    concat(
      check_box_tag(
        ...
        # If true checkbox is checked
        current_step_index == index,
        ...
      )
    )
    concat(capture(&amp;amp;content))
  end
end
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Finally, we need to handle errors. &lt;strong&gt;Every time the form is submitted, all validations are checked. This can lead to errors associated with form fields in steps the user did not visit&lt;/strong&gt;, resulting in error messages displayed before filling out the form. On top of that, the current_step error is always registered. We use this error to trigger the rendering of new steps, but we must avoid displaying its associated error message (the user should be unaware of it).&lt;/p&gt;

&lt;p&gt;We will add a new method responsible for removing all error messages related to steps the user did not visit and another to delete the current_step error. To achieve this, we will add a third attribute responsible for tracking the latest step in the form the user filled in so we can determine which steps remain unvisited.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/components/common/multistep_form_component.rb
...
def call
    ...
    concat(form.hidden_field(:total_steps, value: steps.count))
    concat(form.hidden_field(:latest_step, value: latest_step_index))
    concat(form.hidden_field(:current_step, value: current_step_index))
    ...
  end
end
...
def latest_step_index
  @latest_step_index =
    if model.latest_step.nil?
      0
    else
      [current_step_index, model.latest_step].max
    end
end
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Tests
&lt;/h2&gt;

&lt;p&gt;There are two key elements to test for our multistep form: our Model concern and the MultistepFormComponent. Additional unit and system tests for the controllers, project components, and the whole user flow of creating projects are also essential. Still, they do not involve any special configurations as these others do.&lt;/p&gt;

&lt;p&gt;To test the MultistepFormModel concern, we might be tempted to test the current_step validations via the Project model. This is not a good practice since it could lead to flaky tests: we must not test concerns via Models that might not include or extend them in the future.&lt;/p&gt;

&lt;p&gt;Instead, we can define an auxiliary Model class with the only purpose of testing the concern, and then run the tests with it. This approach has two benefits: testing the concern without relying on Model classes from our business logic and documenting how the concern can be used in our Model classes.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class MultistepFormModelTest &amp;lt; ActiveSupport::TestCase
  class ValidMultistepFormMode
    include ActiveModel::Model
    include MultistepFormModel
  end

  def setup
    @model = ValidMultistepFormMode.new
  end

  # Validations
  # -----------
  test "MultistepFormModel does not apply step validation if total and current attibutes are not specified" do
    assert_nil @model.current_step
    assert_nil @model.total_steps
    assert @model.valid?
  end

  test "MultistepFormModel is valid only when total and current steps are the same" do
    @model.current_step = 1
    @model.total_steps = 2
    refute @model.valid?

    @model.current_step = 3
    @model.total_steps = 3
    assert @model.valid?
  end

  test "MultistepFormModel sets custom error attribute and detail when total and current step attributes are invalid" do
    @model.current_step = 1
    @model.total_steps = 2
    refute @model.valid?

    assert_equal @model.errors.first.attribute, MultistepFormModel::ERROR_ATTRIBUTE
    assert_equal @model.errors.first.detail, MultistepFormModel::ERROR_DETAIL
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;To test the MultistepFormModel we can follow the same approach, defining a set of test components that inherit from the MultistepFormComponent and StepComponent classes just for testing. Here we will check the component sets the correct current_step according to our logic by passing different Model data. Since we do not need to interact with the form (click buttons, run javascript code, etc), we can use unit tests instead of slower system tests.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Common::MultistepFormComponentTest &amp;lt; ViewComponent::TestCase&lt;br&gt;
  class SampleModel&lt;br&gt;
    include ActiveModel::Model&lt;br&gt;
    include MultistepFormModel
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;attr_accessor :attribute_1, :attribute_2, :attribute_3

validates_presence_of :attribute_1
validates_presence_of :attribute_2
validates_presence_of :attribute_3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;end&lt;/p&gt;

&lt;p&gt;class TestMultistepFormComponent &amp;lt; Common::MultistepFormComponent&lt;br&gt;
    class &amp;lt;&amp;lt; self&lt;br&gt;
      def steps = [FirstTestStepComponent, SecondTestStepComponent, ThirdTestStepComponent]&lt;br&gt;
    end&lt;br&gt;
  end&lt;/p&gt;

&lt;p&gt;class FirstTestStepComponent &amp;lt; Common::MultistepFormComponent::StepComponent&lt;br&gt;
    class &amp;lt;&amp;lt; self&lt;br&gt;
      def title = "First Step"&lt;br&gt;
      def input_attributes = %i[attribute_1]&lt;br&gt;
    end&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def call
  tag.div(**wrapper_attributes)
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;end&lt;/p&gt;

&lt;p&gt;class SecondTestStepComponent &amp;lt; Common::MultistepFormComponent::StepComponent&lt;br&gt;
    class &amp;lt;&amp;lt; self&lt;br&gt;
      def title = "Second Step"&lt;br&gt;
      def input_attributes = %i[attribute_2]&lt;br&gt;
    end&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def call
  tag.div(**wrapper_attributes)
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;end&lt;/p&gt;

&lt;p&gt;class ThirdTestStepComponent &amp;lt; Common::MultistepFormComponent::StepComponent&lt;br&gt;
    class &amp;lt;&amp;lt; self&lt;br&gt;
      def title = "Third Step"&lt;br&gt;
      def input_attributes = %i[attribute_3]&lt;br&gt;
    end&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def call
  tag.div(**wrapper_attributes)
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;end&lt;/p&gt;

&lt;p&gt;def setup&lt;br&gt;
    @component = TestMultistepFormComponent&lt;br&gt;
  end&lt;/p&gt;

&lt;p&gt;test "Renders multistep form" do&lt;br&gt;
    render_inline(@component.new(form_url: "test", back_url: "test", model: SampleModel.new))&lt;br&gt;
    assert_selector(".#{@component.component_class}")&lt;br&gt;
  end&lt;/p&gt;

&lt;p&gt;test "Renders multistep form steps" do&lt;br&gt;
    render_inline(@component.new(form_url: "test", back_url: "test", model: SampleModel.new))&lt;br&gt;
    assert_selector(".#{FirstTestStepComponent.component_class}", visible: :all)&lt;br&gt;
    assert_selector(".#{SecondTestStepComponent.component_class}", visible: :all)&lt;br&gt;
    assert_selector(".#{ThirdTestStepComponent.component_class}", visible: :all)&lt;br&gt;
  end&lt;/p&gt;

&lt;p&gt;test "Selects first step as current step if current_step value is nil" do&lt;br&gt;
    model = SampleModel.new&lt;br&gt;
    model.valid?&lt;br&gt;
    result = @component.new(form_url: "test", back_url: "test", model:).current_step_index&lt;br&gt;
    assert_equal 0, result&lt;br&gt;
  end&lt;br&gt;
  ...&lt;br&gt;
end&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Conclusions&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;Multisteps in Rails are tightly coupled to the controller and model layers. To keep our business logic as separate as possible from this component, we rely on concerns and abstract components. These tools work as a plug &amp;amp; play logic that makes it extremely easy to use multisteps in any part of our application.&lt;/p&gt;

&lt;p&gt;The abstract nature of the MultistepFormComponent simplifies the task of implementing forms with different designs and navigation logics, by delegating the implementation of this logic to the components that extend it.&lt;/p&gt;

&lt;p&gt;Finally, by writing some generic tests that rely on agnostic test Model and Component classes, we ensure the correct functioning of the multisteps across all our applications and provide a detailed and well-documented example of how developers can make use of the multistep logic in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code in Github
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Albert-Markdown-Editor/Albert/pull/150" rel="noopener noreferrer"&gt;140 multistep form by AlbertoHdezCerezo · Pull Request #150 · Albert-Markdown-Editor/Albert&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.a11yproject.com/posts/how-to-write-accessible-forms/" rel="noopener noreferrer"&gt;How-to: Create accessible forms — The A11Y Project&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.accede-web.com/en/guidelines/html-css/complementary-guidelines/identify-the-current-step-in-multiple-steps-forms-using-aria-currentstep/" rel="noopener noreferrer"&gt;In forms with multiple steps, identify the current step using aria-current&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Any Questions?
&lt;/h2&gt;

&lt;p&gt;If you find yourself lost at any point in the article or try to replicate my solution with no result, you can reach me in the comments section. I will be glad to provide additional information and try to clarify open questions. Soon I will implement additional channels for sharing feedback and questions.&lt;/p&gt;

&lt;p&gt;If you have reached this point, thank you for your time and patience. I hope you found my tutorial helpful!&lt;/p&gt;

</description>
      <category>vue</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Database Views &amp; Rails Active Record: defining new Model classes out of views</title>
      <dc:creator>Alberto Hernandez Cerezo</dc:creator>
      <pubDate>Mon, 23 Jan 2023 19:01:25 +0000</pubDate>
      <link>https://dev.to/pascualtalcual/database-views-rails-active-record-defining-new-model-classes-out-of-views-44m3</link>
      <guid>https://dev.to/pascualtalcual/database-views-rails-active-record-defining-new-model-classes-out-of-views-44m3</guid>
      <description>&lt;h2&gt;
  
  
  Database Views &amp;amp; Rails Active Record: defining new Model classes out of views
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Learn about database views, their use, benefits and how to integrate them in your Rails application. Through a simple scenario you will understand their capability to bring new concepts to your data model based on information already present in it.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A Database &lt;code&gt;view&lt;/code&gt; is a persistent SQL query that other SQL queries can reference&lt;/strong&gt;. &lt;code&gt;views&lt;/code&gt; make queries reusable, reducing redundancy and complexity in our business logic. &lt;strong&gt;Its persistent nature implies their result tables behave as “virtual tables“&lt;/strong&gt;: they are not persisted in the database but can be referenced anytime from the view. From a conceptual point of view, &lt;code&gt;views&lt;/code&gt; &lt;strong&gt;can reveal hidden relations in our data model&lt;/strong&gt;, defined from other existing models and represented by these result tables.&lt;/p&gt;

&lt;p&gt;In Rails, database tables are mapped to &lt;code&gt;Model&lt;/code&gt; classes. With this in mind, we can define new &lt;code&gt;Model&lt;/code&gt; classes out of &lt;code&gt;views&lt;/code&gt; virtual tables, consisting of the combination of attributes from multiple models. This allows &lt;strong&gt;working with different models simultaneously to perform certain operations more effectively and efficiently&lt;/strong&gt; than using them separately.&lt;/p&gt;

&lt;p&gt;In this post, you will learn about database &lt;code&gt;views&lt;/code&gt;, their integration with Rails applications, and how they can help you enhance your data model via virtual tables and their respective &lt;code&gt;read-only&lt;/code&gt; &lt;code&gt;Model&lt;/code&gt; classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basics
&lt;/h2&gt;

&lt;h2&gt;
  
  
  SQL queries, subqueries, and views
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;We use the Structured Query Language (SQL) to perform CRUD operations (Create / Read / Update / Delete) in relational databases&lt;/strong&gt;. Relational databases are made of tables (also called relations).&lt;/p&gt;

&lt;p&gt;The SQL language is extremely powerful and allows developers to fetch any data they need from the database. &lt;strong&gt;Read operations are performed via &lt;code&gt;SELECT&lt;/code&gt; statement queries, and return result tables&lt;/strong&gt;. Result tables are composed of columns from other table columns in the database (or new ones defined for the specific query) and filled with records from these tables, which also match the set of conditions specified by other SQL statements in the query (&lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;, …).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5052%2F1%2A5WgflfKZFabkwsV6SBkQVQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5052%2F1%2A5WgflfKZFabkwsV6SBkQVQ.png" alt="Example of Result Table, contributions, generated from two other database tables."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sometimes, &lt;strong&gt;queries for fetching complex information become too long and difficult to handle&lt;/strong&gt;. The language provides two powerful tools to design simpler queries that can return the same results: &lt;code&gt;subqueries&lt;/code&gt; and &lt;code&gt;views&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A &lt;code&gt;subquery&lt;/code&gt; is a SQL query nested inside of another query&lt;/strong&gt;. They split long queries into smaller ones, which are easier to handle. One of the limitations of subqueries is that they only exist in the SQL query in which they are declared. &lt;strong&gt;They are “one use“ only&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A view is a reusable SQL &lt;code&gt;query&lt;/code&gt;&lt;/strong&gt;. &lt;code&gt;views&lt;/code&gt; are persistent and can be referenced from other SQL queries to build more complex queries, behaving similarly to a &lt;code&gt;subquery&lt;/code&gt;. They remove redundancy from our SQL code via reusable encapsulated queries. views definitions (their SQL queries) are persisted within the database, and their result tables are generated each time a query that includes the view is executed. A &lt;code&gt;view&lt;/code&gt; variant, &lt;code&gt;materialized views&lt;/code&gt;, stores views result tables in the database, acting as a cache, which we will ignore to keep this article simple.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F7514%2F1%2AND6Gy96kG9BpQiF8kexeJg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F7514%2F1%2AND6Gy96kG9BpQiF8kexeJg.png" alt="SQL subquery VS SQL view"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;views&lt;/code&gt; are a powerful tool. Besides their reusability benefits, they also bring the possibility of discovering new hidden concepts in our data model. &lt;strong&gt;While result tables out of SQL queries are ephemeral, &lt;code&gt;views&lt;/code&gt; result tables can be accessed anywhere from the database as if they were persistent tables&lt;/strong&gt;. This opens the possibility of creating new “virtual“ tables and enriching our data model with new concepts resulting from synergies between existing ones.&lt;/p&gt;

&lt;p&gt;What are the consequences of this? Let’s briefly explore how the Rails Model layer works first!&lt;/p&gt;

&lt;h2&gt;
  
  
  Rails, Active Record &amp;amp; SQL requests
&lt;/h2&gt;

&lt;p&gt;Rails applications present a Model-View-Controller architecture. The Model layer is responsible for handling the data exchange with the database. For that, the framework counts with ActiveRecord, Rails ORM implementation. &lt;strong&gt;Object-Relational Mapping (ORM) maps connect our business logic classes with their respective database tables representation. For &lt;code&gt;ActiveRecord&lt;/code&gt;, this means generating &lt;code&gt;Model&lt;/code&gt; class instances out of database result table records and vice versa&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ActiveRecord&lt;/code&gt; classes include the &lt;code&gt;QueryInterface&lt;/code&gt; module. It consists of high-level methods to fetch information from the database (&lt;code&gt;find&lt;/code&gt;, &lt;code&gt;where&lt;/code&gt;, &lt;code&gt;includes&lt;/code&gt;, &lt;code&gt;joins&lt;/code&gt;, etc). We use these methods to build SQL queries that fetch the records we need for our business logic. When we invoke a &lt;code&gt;QueryInterface&lt;/code&gt; method, we get a &lt;code&gt;Relation&lt;/code&gt; object in return. These objects are composed of an SQL &lt;code&gt;query&lt;/code&gt; and the &lt;code&gt;QueryInterface&lt;/code&gt; methods. We can see this by calling the &lt;code&gt;.to_sql&lt;/code&gt; method, which returns the SQL &lt;code&gt;query&lt;/code&gt; associated with the method invoked. Since &lt;code&gt;Relation&lt;/code&gt; objects also have &lt;code&gt;QueryInterface&lt;/code&gt; methods, we concatenate them with additional methods to assemble more complex SQL queries.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;irb(main):002:0&amp;gt; Project.joins(:deliverables).where(name: "My project").to_sql
=&amp;gt; "SELECT \"projects\".* FROM \"projects\" INNER JOIN \"project_deliverables\" ON \"project_deliverables\".\"project_id\" = \"projects\".\"id\" WHERE \"projects\".\"name\" = 'My project'"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;ActiveRecord&lt;/code&gt; processes the database result table when the SQL query is performed and parses the data to one or more &lt;code&gt;Model&lt;/code&gt; class records of the class or object from which we initially invoked the query methods. Rails convention maps the database table to its equivalent &lt;code&gt;Model&lt;/code&gt; class via their names.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;irb(main):004:0&amp;gt; relation_object = Project.where.not(name: nil)
  Project Load (0.4ms)  SELECT "projects".* FROM "projects" WHERE "projects"."name" IS NOT NULL
=&amp;gt;                                                                                                          
[#&amp;lt;Project:0x000000010c0764f0                                                                               
...    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;views **are persistent SQL queries, and result tables from these queries can be seen as “virtual tables“. Rails acknowledge this and allow us to map **Model **classes to **views result tables by their respective &lt;code&gt;view&lt;/code&gt; name. As a result, we can create new classes out of existing ones. Since they are generated from &lt;code&gt;views&lt;/code&gt;, these classes will be &lt;code&gt;read-only&lt;/code&gt;, so no creation, update, or deletion operations can be performed on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;view&lt;/code&gt;-based &lt;code&gt;Model&lt;/code&gt; classes matter?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Model&lt;/code&gt; classes mapped from &lt;code&gt;views&lt;/code&gt; are &lt;code&gt;read-only&lt;/code&gt;, and their attributes are defined from other Model class attributes. This means &lt;code&gt;view&lt;/code&gt;-based &lt;code&gt;Models&lt;/code&gt; are useful for handling data from multiple models in single requests. How is it useful for our applications?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There are scenarios where we want to perform a certain operation over a heterogeneous data set&lt;/strong&gt;. For example, we want to display a list of objects from different &lt;code&gt;Model&lt;/code&gt; classes. In these situations, the convention one table one class Rails uses by default is ineffective since it requires us to fetch each set of &lt;code&gt;Model&lt;/code&gt; class data separately. Things get even more complicated if we want to extend these lists with new features: how do we filter and sort these lists? what if we want to implement pagination? This would require implementing quite complex ad-hoc logic in our business model logic.&lt;/p&gt;

&lt;p&gt;views can lower this burden. &lt;strong&gt;By defining a &lt;code&gt;view&lt;/code&gt;-based &lt;code&gt;Model&lt;/code&gt;class from the different &lt;code&gt;Model&lt;/code&gt;classes we want to include in the list, we could fetch all the list items in one request&lt;/strong&gt;. On top of that, additional operations such as those mentioned above could also be implemented within the fetch SQL request, resulting in a more efficient and simple list logic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F6250%2F1%2A22d9z-yWmxCDUZxEFDgjFg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F6250%2F1%2A22d9z-yWmxCDUZxEFDgjFg.png" alt="Proposed integration of database views in our Rails model"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In conclusion, features requiring read operations over data sets including instances of different &lt;code&gt;Model&lt;/code&gt; classes are potential candidates for using &lt;code&gt;views&lt;/code&gt;. They significantly simplify the logic of handling records and provide an efficient way to do so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The only additional cost of using &lt;code&gt;views&lt;/code&gt; is their maintenance costs&lt;/strong&gt;. &lt;code&gt;views&lt;/code&gt; are persisted in the database, and as such, they require some maintenance. If the data model changes and tables associated with the view modify its composition, we might need to update our views. This will also require versioning our views and handling these updated versions via migrations, as we do with the rest of the database changes. This is a minor trade-off from &lt;code&gt;views&lt;/code&gt; which is still important to mention before working with them.&lt;/p&gt;

&lt;p&gt;Let’s now look at an example of how to implement views in Rails.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;We have an application that manages &lt;code&gt;Projects&lt;/code&gt;. A &lt;code&gt;Project&lt;/code&gt; can contain multiple &lt;code&gt;Deliverables&lt;/code&gt;. &lt;code&gt;Deliverables&lt;/code&gt; can be any sort of text based document: a &lt;code&gt;Book&lt;/code&gt;, a &lt;code&gt;BlogPost&lt;/code&gt;, etc. We want to create a list that displays all &lt;code&gt;Deliverables&lt;/code&gt; of a &lt;code&gt;Project&lt;/code&gt;, with the possibility to filter and sort them by multiple criteria.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Model
&lt;/h2&gt;

&lt;p&gt;This problem has two key concepts: the &lt;code&gt;Project&lt;/code&gt; and the &lt;code&gt;Deliverable&lt;/code&gt;. A &lt;code&gt;Project&lt;/code&gt; contains multiple &lt;code&gt;Deliverables&lt;/code&gt;. And what is a Deliverable? It can be anything. Right now, we identified &lt;code&gt;Books&lt;/code&gt; and &lt;code&gt;BlogPosts&lt;/code&gt; as deliverables, but what if we discover new kinds of deliverables in the future? We should be able to use them as &lt;code&gt;Deliverables&lt;/code&gt; too. Each deliverable can be arbitrarily different from the rest. A &lt;code&gt;Book&lt;/code&gt; can have pages, an index, different sections, etc. A &lt;code&gt;BlogPost&lt;/code&gt; is way simpler, with its content, maybe the URL where it will be available, and not much more. Both of them might share some common attributes, like a title. So how do we define a &lt;code&gt;Deliverable&lt;/code&gt; in our data model?&lt;/p&gt;

&lt;p&gt;The answer is: we don’t! The &lt;code&gt;Deliverable&lt;/code&gt; is just the concept of an artifact associated with a Project, which multiple classes can embody. The data of a Deliverable is its underlying &lt;code&gt;Model&lt;/code&gt; class. It does not have specific data that would require us to define a new table in our data model. It is a model composed of data from other models, a perfect candidate to be represented by a view.&lt;/p&gt;

&lt;p&gt;Let’s start generating our new &lt;code&gt;Book&lt;/code&gt; and &lt;code&gt;BlogPost&lt;/code&gt; model classes. We will add some fixtures for testing purposes and update the existing &lt;code&gt;Project&lt;/code&gt; class with the corresponding associations.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Rails generator commands run in terminal to generate new models
bundle exec rails g model BlogPost title:string summary:string release_date:date project:references
bundle exec rails g model Book title:string summary:string release_date:date project:references

# app/models/project.rb

class Project &amp;lt; ApplicationRecord
  ...
  # Associations
  has_many :blog_posts, dependent: :nullify
  has_many :books, dependent: :nullify
  ...
end

# app/models/book.rb

class Book &amp;lt; ApplicationRecord
  belongs_to :project, optional: true
end

# app/models/blog_post.rb

class BlogPost &amp;lt; ApplicationRecord
  belongs_to :project, optional: true
end

# app/test/fixtures/books.yml

# Book Fixture
recipes_book_project_book_1:
  title: How to cook pizza without burning your kitchen
  summary: Learn how make the best pizza in simple and secure steps
  release_date: &amp;lt;%= rand(10..360).days.from_now %&amp;gt;
  project: recipes_book

# app/test/fixtures/blog_posts.yml

# BlogPost Fixture
recipes_book_project_blog_post_1:
  title: "Introducting my new book: How to cook pizza without burning your kitchen"
  summary: Promo post to announce my new awesome book
  release_date: &amp;lt;%= rand(10..360).days.from_now %&amp;gt;
  project: recipes_book

# app/test/fixtures/projects.yml

# Project Fixture
recipes_book:
  title: My Recipes Book
  description: This is the best book about recipes you have ever read
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;To model our &lt;code&gt;Deliverable&lt;/code&gt; class, we will need a &lt;code&gt;view&lt;/code&gt;. &lt;a href="https://github.com/scenic-views/scenic" rel="noopener noreferrer"&gt;We will use the popular scenic gem&lt;/a&gt;, which provides some useful generators for creating views with their respective migrations, and utilities to handle &lt;code&gt;views&lt;/code&gt; versioning.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# gemfile
...
# DB
# --
# Database View Manager
gem "scenic"
...

# Running this command in the console will generate our Deliverables view
bundle exec rails g scenic:view project_deliverables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;view&lt;/code&gt; SQL query must return all &lt;code&gt;Book&lt;/code&gt; and &lt;code&gt;BlogPost&lt;/code&gt; records in our database. It must also handle commonly named attributes, returning the correct value for each data table record. Since it will return a result table as a response, we might also need a way to identify which table records are &lt;code&gt;Books&lt;/code&gt; and which ones are &lt;code&gt;BlogPosts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All this can be easily achieved via a &lt;code&gt;NATURAL OUTER JOIN&lt;/code&gt; and the &lt;code&gt;COALESCE&lt;/code&gt; method. The &lt;code&gt;JOIN&lt;/code&gt; method will merge both tables into a single one with the combined columns of each. We can then select the relevant columns for our final result table via the &lt;code&gt;SELECT&lt;/code&gt; statement.&lt;/p&gt;

&lt;p&gt;Since some columns share the same name, we can use the &lt;code&gt;COALESCE&lt;/code&gt; method to select the corresponding column value for &lt;code&gt;Book&lt;/code&gt; and &lt;code&gt;BlogPost&lt;/code&gt; records. &lt;code&gt;COALESCE&lt;/code&gt; admits an indefinite number of parameters, returning the first one which is not &lt;code&gt;NULL&lt;/code&gt;. Joint table &lt;code&gt;Book&lt;/code&gt; records will have &lt;code&gt;BlogPost&lt;/code&gt; columns set to &lt;code&gt;NULL&lt;/code&gt; and vice versa.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/db/views/project_deliverables_v01.sql

SELECT
    CASE
        WHEN books.id IS NOT NULL THEN 'Book'
        WHEN blog_posts.id IS NOT NULL THEN 'BlogPost'
    END as kind,
    COALESCE(books.id, blog_posts.id) as id,
    COALESCE(books.title, blog_posts.title) as title,
    COALESCE(books.summary, blog_posts.summary) as summary,
    COALESCE(books.release_date, blog_posts.release_date) as release_date,
    COALESCE(books.project_id, blog_posts.project_id) as project_id
FROM books NATURAL FULL OUTER JOIN blog_posts;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;With our SQL &lt;code&gt;view&lt;/code&gt; ready, we can now define the associated &lt;code&gt;ActiveRecord&lt;/code&gt; &lt;code&gt;Model&lt;/code&gt; class. Following the Rails conventions, it must have the same name as the &lt;code&gt;view&lt;/code&gt;. And since &lt;code&gt;views&lt;/code&gt; are &lt;code&gt;read-only&lt;/code&gt;, our class must be &lt;code&gt;read-only&lt;/code&gt; too. We will mark &lt;code&gt;Model&lt;/code&gt; classes as &lt;code&gt;read only&lt;/code&gt; via the &lt;code&gt;readonly?&lt;/code&gt; method. Since we will likely define new &lt;code&gt;view&lt;/code&gt;-based Model classes in the future, we will encapsulate this logic in a concern that we can later reuse for other new classes to make them &lt;code&gt;view&lt;/code&gt;-based.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/models/concerns/view_based_model.rb

module ViewBasedModel
  extend ActiveSupport::Concern

  # View Models are made out of database views, so they are read only
  def readonly? = true
end

# app/models/project_deliverable.rb

class ProjectDeliverable &amp;lt; ApplicationRecord
  include ViewBasedModel

  # Ensures only deliverables with valid kinds are created
  enum kind: %w[Book BlogPost]
end

# app/models/project.rb

class Project &amp;lt; ApplicationRecord
  ...
  # Associations
  has_many :deliverables, class_name: "ProjectDeliverable"
  ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;All set; we can now test how the &lt;code&gt;Project&lt;/code&gt; class instances can fetch all &lt;code&gt;ProjectDeliverable&lt;/code&gt; instances associated with it. If we open the Rails console in the terminal (&lt;code&gt;bundle exec rails c&lt;/code&gt;), we can see:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;irb(main):001:0&amp;gt; Project.first.deliverables
  Project Load (1.1ms)  SELECT "projects".* FROM "projects" ORDER BY "projects"."id" ASC LIMIT $1  [["LIMIT", 1]]
  ProjectDeliverable Load (2.2ms)  SELECT "project_deliverables".* FROM "project_deliverables" WHERE "project_deliverables"."project_id" = $1  [["project_id", "f920b63a-a941-565f-92c4-6af6b0f107ad"]]
=&amp;gt; 
[#&amp;lt;ProjectDeliverable:0x00000001063e4188 kind: "Book", id: "ffbcc7d5-66cb-5095-b0d8-c3e5d2a16e29", title: "How to cook pizza without burning your kitchen", summary: "Learn how make the best pizza in simple and secure steps", release_date: Wed, 13 Sep 2023, project_id: "f920b63a-a941-565f-92c4-6af6b0f107ad"&amp;gt;,
 #&amp;lt;ProjectDeliverable:0x00000001065b7730 kind: "BlogPost", id: "6acf2090-67dc-5d76-9dcc-c082ab0bc84b", title: "Introducting my new book: How to cook pizza without burning your kitchen", summary: "Promo post to announce my new awesome book", release_date: Sun, 06 Aug 2023, project_id: "f920b63a-a941-565f-92c4-6af6b0f107ad"&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The class returns all the &lt;code&gt;Book&lt;/code&gt; and &lt;code&gt;BlogPost&lt;/code&gt; records as &lt;code&gt;ProjectDeliverable&lt;/code&gt; instances. Each instance has the attributes corresponding to its respective data table record. We can also take a look at the underlying SQL query and see how small it is in comparison to the &lt;code&gt;view&lt;/code&gt; request, illustrating once more how &lt;code&gt;views&lt;/code&gt; can simplify queries:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;irb(main):004:0&amp;gt; Project.first.deliverables.to_sql
  Project Load (0.4ms)  SELECT "projects".* FROM "projects" ORDER BY "projects"."id" ASC LIMIT $1  [["LIMIT", 1]]
=&amp;gt; "SELECT \"project_deliverables\".* FROM \"project_deliverables\" WHERE \"project_deliverables\".\"project_id\" = 'f920b63a-a941-565f-92c4-6af6b0f107ad'"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now, if we want to filter or sort &lt;code&gt;ProjectDeliverables&lt;/code&gt; we can easily do it by just concatenating the &lt;code&gt;.deliverable&lt;/code&gt; method invocation with the respective &lt;code&gt;QueryInterface&lt;/code&gt; methods. For example, for sorting, we could use the following:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;irb(main):007:0&amp;gt; Project.first.deliverables.order(name: :desc)
  Project Load (0.4ms)  SELECT "projects".* FROM "projects" ORDER BY "projects"."id" ASC LIMIT $1  [["LIMIT", 1]]
  ProjectDeliverable Load (0.4ms)  SELECT "project_deliverables".* FROM "project_deliverables" WHERE "project_deliverables"."project_id" = $1 ORDER BY "project_deliverables"."name" DESC  [["project_id", "f920b63a-a941-565f-92c4-6af6b0f107ad"]]
=&amp;gt;                                                                                                                 
[#&amp;lt;ProjectDeliverable:0x000000010816eeb0, kind: "BlogPost", id: "6acf2090-67dc-5d76-9dcc-c082ab0bc84b", name: "Introducting my new book: How to cook pizza without burning your kitchen", summary: "Promo post to announce my new awesome book", release_date: Sun, 06 Aug 2023, project_id: "f920b63a-a941-565f-92c4-6af6b0f107ad"&amp;gt;,                                                             
 #&amp;lt;ProjectDeliverable:0x000000010816ed48 kind: "Book", id: "ffbcc7d5-66cb-5095-b0d8-c3e5d2a16e29", name: "How to cook pizza without burning your kitchen", summary: "Learn how make the best pizza in simple and secure steps", release_date: Wed, 13 Sep 2023, project_id: "f920b63a-a941-565f-92c4-6af6b0f107ad"&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Filtering is a more complex problem, but luckily I already wrote a detailed post about how to implement filters in Rails that you can follow to add filters to your application, and &lt;a href="https://github.com/Albert-Markdown-Editor/Albert/pull/138" rel="noopener noreferrer"&gt;you can find the solution to it in the PR reference available here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As a final touch, let’s rework our &lt;code&gt;ViewBasedModel&lt;/code&gt; concern. Having an enum defining the &lt;code&gt;Model&lt;/code&gt; compatible kinds is a pattern we want to do in all our &lt;code&gt;view&lt;/code&gt;-based model classes. To document and enforce following this convention, we can extend our concern to define the enum for us and require developers to define a list of compatible kind classes per model. When you have to implement new views in the future, it will be easier to remember how to configure your models.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/models/concerns/view_based_model.rb

module ViewBasedModel
  extend ActiveSupport::Concern

  class_methods do
    def compatible_kinds
      raise "
        Model #{name} does not define `compatible_kinds` class method.
        Define method returning an array of strings corresponding to the model
        class names of the tables the view record is composed of. Check example
        in Project and ProjectDeliverable classes.
      "
    end
  end

  # View Models are made out of database views, they are read only
  def readonly? = true

  included do
    enum kind: compatible_kinds.index_with(&amp;amp;:to_s)
  end
end

# app/models/project_deliverable.rb

class ProjectDeliverable &amp;lt; ApplicationRecord
  class &amp;lt;&amp;lt; self
    def compatible_kinds = %w[Book BlogPost]
  end

  include ViewBasedModel
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;As we can see, we ended up with a very simple, sustainable, and efficient solution to handle multiple &lt;code&gt;Model&lt;/code&gt; classes at once. If, in the future, we want to add new deliverables to our &lt;code&gt;Projects&lt;/code&gt;; we need to update the view SQL query with a new version and then set the proper model associations. If our existing &lt;code&gt;ProjectDeliverable&lt;/code&gt; models change, we might need to update the query if any changes affect its fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests
&lt;/h2&gt;

&lt;p&gt;To make sure the new feature works, we need to check &lt;code&gt;Project&lt;/code&gt; instance deliverables method returns all associated deliverable records in it (&lt;code&gt;books&lt;/code&gt; and &lt;code&gt;blog_posts&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Rather than just using the &lt;code&gt;Project&lt;/code&gt; fixtures to test &lt;code&gt;.deliverables&lt;/code&gt; returns the same records as &lt;code&gt;.books&lt;/code&gt; and &lt;code&gt;.blog_posts&lt;/code&gt; together, we will use the &lt;code&gt;ProductDeliverable&lt;/code&gt; &lt;code&gt;.kinds&lt;/code&gt; method to get the list of available &lt;code&gt;ProjectDeliverable&lt;/code&gt; kinds. Then we can use it to fetch all deliverable associations and compare them with the tested method. If new kinds are added in the future, the test will still be valid.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/test/models/project_test.rb

class ProjectTest &amp;lt; ModelTestHelper
  def setup
    @sample_project = projects(:recipes_book)
  end

  test ".deliverables return all project deliverables" do
    project_kinds = ProjectDeliverable.kinds.keys.map(&amp;amp;:underscore).map(&amp;amp;:pluralize)
    project_deliverables = project_kinds.map { |k| @sample_project.send(k) }.flatten.map { |d| d.id }.sort

    result = @sample_project.deliverables.pluck(:id).sort

    assert_equal project_deliverables, result
  end
  ...
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Additional tests can be set to check that each deliverable kind is properly mapped to the &lt;code&gt;ProjectDeliverable&lt;/code&gt; instance, but tests carry maintenance costs. The current test is good enough to ensure the system is working and very easy to maintain when new kinds are added to the view. If in the future we detect further issues in the logic, we can always add more tests to grant the correct functioning of it.&lt;/p&gt;

&lt;p&gt;Finally, let’s add a test to our new concern. It will serve as a documented example of how to create a &lt;code&gt;view&lt;/code&gt;-based model for us in the future:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/test/model/concerns/view_based_model_test.rb

&lt;p&gt;require "test_helper"&lt;/p&gt;

&lt;p&gt;class ViewBasedModelTest &amp;lt; ActiveSupport::TestCase&lt;br&gt;
  class ValidViewBasedModel &amp;lt; ActiveRecord::Base&lt;br&gt;
    def self.compatible_kinds = %w[Test]&lt;br&gt;
  end&lt;/p&gt;

&lt;p&gt;class InvalidViewBasedModel &amp;lt; ActiveRecord::Base&lt;br&gt;
  end&lt;/p&gt;

&lt;p&gt;test "ViewBasedModel concern requires included classes to define compatible_kinds class method" do&lt;br&gt;
    assert_nothing_raised do&lt;br&gt;
      ValidViewBasedModel.include(ViewBasedModel)&lt;br&gt;
    end&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assert_raises do
  InvalidViewBasedModel.include(ViewBasedModel)
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;end&lt;/p&gt;

&lt;p&gt;test "ViewBasedModel concerns defines kinds enum in included class" do&lt;br&gt;
    ValidViewBasedModel.include(ViewBasedModel)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assert ValidViewBasedModel.defined_enums.key?("kind")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;end&lt;/p&gt;

&lt;p&gt;test "ViewBasedModel concerns kinds enum includes the respective compatible_kind values" do&lt;br&gt;
    ValidViewBasedModel.include(ViewBasedModel)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assert_equal ValidViewBasedModel.defined_enums.values.first.values.sort, ValidViewBasedModel.compatible_kinds.sort
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;end&lt;br&gt;
end&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Conclusion&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;views &lt;strong&gt;are a powerful tool for handling information from multiple relations in our database&lt;/strong&gt;. Its persistent nature allows us to use them as “virtual tables“. As a result, we can model new result tables out of them to add new concepts to our data model without changing our database structure.&lt;/p&gt;

&lt;p&gt;When working with Rails applications, &lt;code&gt;QueryInterface&lt;/code&gt; methods are limited to fetching instances of one Model class at a time. &lt;strong&gt;With &lt;code&gt;views&lt;/code&gt;, we can create new virtual models out of their respective result tables and define new &lt;code&gt;Model&lt;/code&gt; classes from them&lt;/strong&gt;. These classes can contain information from multiple classes, allowing us to overcome the limitation of one &lt;code&gt;Model&lt;/code&gt; class per request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This leads to more efficient use of database queries and a sustainable logic in our data model, which can be easily extended to include new kinds anytime needed&lt;/strong&gt;. Considering the only additional cost of views is their maintenance (which should be zero if the data model is properly defined), this is one of the key tools to rely on when working with databases and Rails applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code in Github
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;In response to the feedback gathered from my previous post, and to provide complementary material for better understanding my future post entries, I decided to share all the code associated to the work for each post in simple PR requests.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Albert-Markdown-Editor/Albert/pull/138" rel="noopener noreferrer"&gt;133 create a list with filter options for project items by AlbertoHdezCerezo · Pull Request #138 · Albert-Markdown-Editor/Albert&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://pganalyze.com/blog/materialized-views-ruby-rails" rel="noopener noreferrer"&gt;Effectively Using Materialized Views in Ruby on Rails&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/tutorial-views.html" rel="noopener noreferrer"&gt;PostgreSQL Views&lt;/a&gt; Official Documentation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://thoughtbot.com/blog/announcing-scenic--versioned-database-views-for-rails" rel="noopener noreferrer"&gt;Announcing Scenic — Versioned Database Views for Rails&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rails</category>
      <category>database</category>
      <category>views</category>
      <category>activerecord</category>
    </item>
    <item>
      <title>Filters in Rails with Query Object Pattern</title>
      <dc:creator>Alberto Hernandez Cerezo</dc:creator>
      <pubDate>Wed, 16 Nov 2022 10:51:58 +0000</pubDate>
      <link>https://dev.to/pascualtalcual/filters-in-rails-with-query-object-pattern-3j9f</link>
      <guid>https://dev.to/pascualtalcual/filters-in-rails-with-query-object-pattern-3j9f</guid>
      <description>&lt;h2&gt;
  
  
  What will you learn?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the Query Object Pattern to implement a sustainable filter logic for your models and controllers. Connect your filter logic to your views by building a reusable filter component.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Filtering is the process of refining a data set by excluding data according to certain criteria&lt;/strong&gt;. Filters are frequently used to search for information in large datasets efficiently and effectively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A filter component is an accessibility tool&lt;/strong&gt;. It increases the accessibility and visibility of our data by providing a set of predefined options to select information. &lt;strong&gt;Designwise it consists of a form with multiple input fields, each corresponding to a filter condition&lt;/strong&gt;. Filters are used in combination with lists and tables. When a filter form is submitted, the associated list or table is updated, displaying results that match the submitted filter form condition values.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fiy32i3qfj6s5oqo4c48e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fiy32i3qfj6s5oqo4c48e.png" alt="Filter Component in Amazon.com to search for TVs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is important to complement our lists and tables with filters to make it easier for users to navigate through them&lt;/strong&gt;. Implementing filters in your application will be a recurring issue. Each filter will vary depending on multiple aspects (the data model you work with, the list or table attached to the filter, the user needs, etc.). Thus, it is important to implement a filter logic that is easy to reuse in multiple contexts and with different sets of conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basics
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How to filter data?
&lt;/h3&gt;

&lt;p&gt;From an implementation perspective, &lt;strong&gt;filtering consists of performing multiple custom queries to fetch data by different filter conditions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rails &lt;code&gt;ApplicationRecord&lt;/code&gt; model classes count with the &lt;code&gt;ActiveRecord&lt;/code&gt; &lt;a href="https://guides.rubyonrails.org/active_record_querying.html" rel="noopener noreferrer"&gt;query interface&lt;/a&gt;, consisting of a set of class methods to fetch records from the database. They work as an abstraction layer to build raw SQL queries easily. We can use these interface methods to implement our custom queries.&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;Document&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="c1"&gt;# (!) Filter methods are class methods too!&lt;/span&gt;

  &lt;span class="c1"&gt;# Filter document by title&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"title ILIKE ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&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="c1"&gt;# Filter document by author&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by_author&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;joins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"author.name ILIKE ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&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="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# SQL query generated by the query interface for one of our filters&lt;/span&gt;
&lt;span class="no"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"My book"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;
&lt;span class="c1"&gt;# returns: "SELECT \"documents\".* FROM \"documents\" WHERE (title ILIKE '%My book%')"&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Query interface methods return a relation&lt;/strong&gt;. Relations also have access to query interface methods. This allows chaining multiple query method invocations to assemble complex SQL queries easily. To create our filter query, which will include all our custom queries, we just need to chain all our custom queries:&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;# Our document filter&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;filter_by_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter_by_author&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;This query will always filter documents by their title and author. However, &lt;strong&gt;filter conditions are usually optional: when a filter value is missing, its respective condition is not considered for filtering results&lt;/strong&gt; (for example, if I do not pass a title to the document filter, it should only filter results by author).&lt;/p&gt;

&lt;p&gt;We could update our custom query methods, wrapping them in an if statement, so only when the filter value is present is the relation returned. However, returning a nil value instead of a relation will break the chain of custom queries.&lt;/p&gt;

&lt;p&gt;The solution to this problem is using the &lt;code&gt;scope&lt;/code&gt; &lt;a href="https://apidock.com/rails/ActiveRecord/NamedScope/ClassMethods/scope" rel="noopener noreferrer"&gt;class method&lt;/a&gt;, also part of the query interface. This method defines new model class methods for retrieving and querying data, such as the custom queries we previously defined. However, &lt;strong&gt;scope queries are lenient to &lt;code&gt;nil&lt;/code&gt; values, always returning a relation&lt;/strong&gt;. We can use scopes to make our custom queries conditional while preserving their chain ability.&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;Document&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:by_title&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="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"title ILIKE ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&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;if&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:by_author&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;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;joins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"author.name ILIKE ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&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;if&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;by_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;by_author&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author&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="c1"&gt;# This will always return a relation&lt;/span&gt;
&lt;span class="no"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"My document"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Leonardo Dantes"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Even though we come up with a nice way to define our custom queries and implement our filter query with them, our current approach presents multiple design flaws that need to be addressed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Models' main responsibilities are exchanging data with the database and implementing parts of the business logic. &lt;strong&gt;Filters, on the other hand, answer an accessibility problem and thus are not related to model responsibilities&lt;/strong&gt;. Models should not be responsible for handling our filter logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filtering is a model-agnostic logic; all models filter results similarly&lt;/strong&gt;. Placing our &lt;code&gt;filter&lt;/code&gt; method in our model implies that each model must define this filter logic, which is redundant.&lt;/li&gt;
&lt;li&gt;The definition of custom query scopes in model classes will lead to &lt;strong&gt;big model classes overloaded with filter queries&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Query Object Pattern
&lt;/h3&gt;

&lt;p&gt;The Query Object Pattern is a Design Pattern. &lt;strong&gt;It consists of defining a specialized object to encapsulate all information required to perform a specific data query logic&lt;/strong&gt; (in our case, filter queries, but it can be for any other queries, such as sort queries, for example).&lt;/p&gt;

&lt;p&gt;We will use this pattern for our solution, refactoring all our &lt;code&gt;scopes&lt;/code&gt; and filter logic to a specialized object responsible for assembling our filter SQL queries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F1kuc5fplaaua8hgeebvx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F1kuc5fplaaua8hgeebvx.png" alt="Our implementation of the Query Object pattern"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Delegating our filter logic to a query object solves all the problems previously mentioned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Our models do not need to know how to filter records&lt;/strong&gt;; the query object will do. This reduces our models' responsibilities and removes all the filter-related code from them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our query objects can share a common filter logic&lt;/strong&gt;, making our system less redundant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next lines, we will explain how to implement the &lt;em&gt;Query Object Pattern&lt;/em&gt; for a simple scenario.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;We have an application to write &lt;code&gt;Documents&lt;/code&gt;. &lt;code&gt;Documents&lt;/code&gt; can be organized in &lt;code&gt;Projects&lt;/code&gt; (a &lt;code&gt;Project&lt;/code&gt; has zero or many &lt;code&gt;Documents&lt;/code&gt;, and a &lt;code&gt;Document&lt;/code&gt; can belong to a &lt;code&gt;Project&lt;/code&gt;). We want to allow users to filter both &lt;code&gt;Documents&lt;/code&gt; and &lt;code&gt;Projects&lt;/code&gt; by different criteria.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Model
&lt;/h3&gt;

&lt;p&gt;We already learned how we can define optional filter queries with &lt;code&gt;scopes&lt;/code&gt;. Since all our filter custom queries should be optional, we must add a conditional clause to all of them. To avoid this redundancy, we will define our own &lt;code&gt;filter_scope&lt;/code&gt; “scope“ method instead, which will only chain a custom query to the filter query when its value is present.&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;# model/concerns/filter_scopeable.rb&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;FilterScopeable&lt;/span&gt;
  &lt;span class="kp"&gt;extend&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;Concern&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&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;define_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&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;filter_value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;filter_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;

      &lt;span class="n"&gt;instance_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter_value&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;span class="k"&gt;end&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;This approach allows us to define a common filter logic for custom queries and share it across all our filtrable models.&lt;/p&gt;

&lt;p&gt;Let’s start implementing our query object by moving our &lt;code&gt;scopes&lt;/code&gt; out of the model. These methods only work in the context of their respective model class. &lt;strong&gt;To extract and use them, we will extend the default model class scope with our filter scopes via the &lt;a href="https://apidock.com/rails/ActiveRecord/QueryMethods/extending" rel="noopener noreferrer"&gt;extending method&lt;/a&gt;&lt;/strong&gt;. This method requires a known scope (our class model) and a module with methods to extend the scope. Let’s define our first filter query object with that information:&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;# /models/concerns/filters/document_filter_proxy.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Filters&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;DocumentFilterScopes&lt;/span&gt;
    &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;FilterScopeable&lt;/span&gt;

    &lt;span class="c1"&gt;# We define scopes with out new method&lt;/span&gt;
    &lt;span class="n"&gt;filter_scope&lt;/span&gt; &lt;span class="ss"&gt;:name&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;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"name ILIKE ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;filter_scope&lt;/span&gt; &lt;span class="ss"&gt;:status&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="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:)&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;class&lt;/span&gt; &lt;span class="nc"&gt;DocumentFilterProxy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;FilterProxy&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Document&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_scopes_module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DocumentFilterScopes&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;Finally, let's create our &lt;code&gt;FilterProxy&lt;/code&gt; class. It will define the logic to assemble the filter query for our &lt;code&gt;DocumentFilterProxy&lt;/code&gt; and all other future filter proxies:&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;# /models/concerns/filters/filter_proxy.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Filters&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FilterProxy&lt;/span&gt;
    &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;FilterScopeable&lt;/span&gt;

    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
      &lt;span class="c1"&gt;# Model Class whose scope will be extended with our filter scopes module&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query_scope&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"Class &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not define query_scope class method."&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_scopes_module&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"Class &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not define filter_scopes_module class method."&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# extend model class scope with filter methods&lt;/span&gt;
        &lt;span class="n"&gt;extended_scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter_scopes_module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# The payload for filters will be a hash. Each key will have the&lt;/span&gt;
        &lt;span class="c1"&gt;# name of a filter scope. We will map each key value pair to its&lt;/span&gt;
        &lt;span class="c1"&gt;# respective filter scope.&lt;/span&gt;
        &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;filter_scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter_value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;filter_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;extended_scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond_to?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter_scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;extended_scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extended_scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter_scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter_value&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="c1"&gt;# Final relation with all filter scopes from +filters+ payload&lt;/span&gt;
        &lt;span class="n"&gt;extended_scope&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Our solution is very simple, easy to maintain, and to scale&lt;/strong&gt;. All our query objects share a common filtering logic, and the set of filter queries is now isolated in a separate module. If in the future, you want to create filters for a new model, you can do it by simply creating a new module for your queries and a &lt;code&gt;FilterProxy&lt;/code&gt; class with a &lt;code&gt;query_scope&lt;/code&gt; and a &lt;code&gt;filter_scopes_module&lt;/code&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;# /models/concerns/filters/project_filter_proxy.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Filters&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ProjectFilterScopes&lt;/span&gt;
    &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;FilterScopeable&lt;/span&gt;

    &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProjectFilterProxy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;FilterProxy&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Project&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_scopes_module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ProjectFilterScopes&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;Finally, let's integrate our proxy filter logic into our models. &lt;strong&gt;We will expand our models' default &lt;code&gt;ActiveRecord&lt;/code&gt; query interface with a new &lt;code&gt;filter_by&lt;/code&gt; class method&lt;/strong&gt; to provide an easy way to filter records (&lt;code&gt;MyModel.filter_by(...)&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Since this interface will be the same for all filterable models, we will define it in a &lt;code&gt;concern&lt;/code&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;# /models/concerns/filterable_model&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;FilterableModel&lt;/span&gt;
  &lt;span class="kp"&gt;extend&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;Concern&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_proxy&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"
      Model &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; including FilterableModel concern requires filter_proxy method to be defined.
      Method should return filter proxy class associated to model.
    "&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="ss"&gt;:filter_by&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: :filter_proxy&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# /models/project&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;FilterableModel&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ProjectFilterProxy&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# To filter projects now we can do&lt;/span&gt;
&lt;span class="no"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"My Personal Project"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Now each model expanding the &lt;code&gt;FilterableModel&lt;/code&gt; concern will be able to use the &lt;code&gt;filter_by&lt;/code&gt; method. This method will also be available in all our filterable model scopes (for example, &lt;code&gt;Document.all.filter_by&lt;/code&gt;), which again will allow us to assemble our filter queries with other queries to create more complex queries easily:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;

&lt;span class="no"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:documents&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"My Blog"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: :asc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Controller
&lt;/h3&gt;

&lt;p&gt;Controllers handle HTTP requests and return the proper output in response. &lt;strong&gt;Our controllers are responsible for defining the interface to filter records, which consists of defining the filter parameters&lt;/strong&gt;. Each filterable controller will have its own set of filter parameters, but the logic to define and use them to fetch the results will be the same. Thus we can encapsulate it in a concern for better reusability:&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/concerns/filterable_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;FilterableController&lt;/span&gt;
  &lt;span class="kp"&gt;extend&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;Concern&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filter_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond_to?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:filter_by&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"
        Controller &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tried to filter a scope of type &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.
        Scope class does not extend FilterProxy interface.
      "&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;filters&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;def&lt;/span&gt; &lt;span class="nf"&gt;filter_params&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"FilterableModel controller &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not define filter_params method."&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;included&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;helper_method&lt;/span&gt; &lt;span class="ss"&gt;:filter_params&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;This concern defines a &lt;code&gt;filter&lt;/code&gt; method that requires a &lt;code&gt;scope&lt;/code&gt; with a &lt;code&gt;filter_by&lt;/code&gt; method and a set of filters formatted as a &lt;code&gt;Hash&lt;/code&gt;. Each key-value pair in the &lt;code&gt;Hash&lt;/code&gt; will represent the name of the filter scope and the value to filter by, respectively.&lt;/p&gt;

&lt;p&gt;Usually, controllers work with a unique set of &lt;code&gt;filter_params&lt;/code&gt;. However, this is not always the case. For more flexibility using multiple filter parameters, the &lt;code&gt;filter&lt;/code&gt; method admits an optional &lt;code&gt;filters&lt;/code&gt; parameter.&lt;/p&gt;

&lt;p&gt;Last, we defined our &lt;code&gt;filter_params&lt;/code&gt; as a helper method. &lt;strong&gt;The idea is to make each request filter parameters accessible from our filterable controller views&lt;/strong&gt;. Thanks to it, our filter components can fetch these parameters and update the form fields according to them. If you define your own filter parameters method, make sure also to declare it as a helper.&lt;/p&gt;

&lt;p&gt;Now we can include this concern in our controllers and update our actions with filters.&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/projects_controller.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProjectsController&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="o"&gt;...&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;FilterableController&lt;/span&gt;

  &lt;span class="c1"&gt;# GET /projects&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="kp"&gt;private&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_params&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&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;h3&gt;
  
  
  View
&lt;/h3&gt;

&lt;p&gt;As mentioned at the beginning of this post, a filter component is just a form in which each input field represents a filter condition. Implementing a simple form is a straightforward task, thanks to Rails &lt;code&gt;form_for&lt;/code&gt; helper method. There are only two implementation issues to be addressed here.&lt;/p&gt;

&lt;p&gt;The first is loading the form with fields filled with the user request filter parameters. &lt;strong&gt;Thanks to the previously defined &lt;code&gt;filter_params&lt;/code&gt; helper, we can fetch user-request filter parameters and inject them into our filter input fields&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The second is to specify the URL our form filter points to (where the form payload will be submitted). This can be done in two ways.&lt;/p&gt;

&lt;p&gt;One is specifying the URL via named URL helpers, so we explicitly set the URL.&lt;/p&gt;

&lt;p&gt;The other consist of using the URL helper method &lt;code&gt;url_for&lt;/code&gt;. Filter requests attach the filter parameters as query strings to the URL, using the same URL with different parameters to fetch different results on the same page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fzgg1kgnipx0c7nalvs0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fzgg1kgnipx0c7nalvs0w.png" alt="Filter URL structure"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;url_for&lt;/code&gt; method returns the URL of the user request. &lt;strong&gt;Using this as our form URL instead of a specific route name helper, we ensure that the form will always work in all routes it is rendered&lt;/strong&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;# Solution implemented with ViewComponents &amp;amp; TailwindCSS&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Projects&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IndexComponent::FilterComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationComponent&lt;/span&gt;
    &lt;span class="c1"&gt;# Tell the component filter_params is a helper and not a component method&lt;/span&gt;
    &lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="ss"&gt;:filter_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: :helpers&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&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;url: &lt;/span&gt;&lt;span class="n"&gt;url_for&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :get&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;"px-8 py-6 inline-flex gap-6"&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;form&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;"inline-flex gap-4"&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;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&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;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&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;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;filter_params&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;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"border-b-2 focus:border-slate-400 focus:outline-none"&lt;/span&gt;
              &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;"inline-flex gap-4"&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;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Description"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="ss"&gt;:description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;filter_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:description&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;"border-b-2 focus:border-slate-400 focus:outline-none"&lt;/span&gt;
              &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&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;span class="k"&gt;end&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fibdnpxp9wnt0qlspkt06.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fibdnpxp9wnt0qlspkt06.png" alt="How the filter component looks like"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;We must ensure our filter scopes return database records that match the filter criteria and that &lt;code&gt;FilterableControllers&lt;/code&gt; process the &lt;code&gt;filter_parameters&lt;/code&gt;&lt;/strong&gt;, when present, and return a response with the corresponding filtered results.&lt;/p&gt;

&lt;p&gt;To test our filter scopes, we can create a &lt;code&gt;TestCase&lt;/code&gt; per &lt;code&gt;FilterProxy&lt;/code&gt; and test per proxy filter scope. Each test will define a filter payload, create a set of records matching the payload and invoke the &lt;code&gt;filter_by&lt;/code&gt; method to assert the returned records match the created ones.&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;# test/models/concerns/project_filter_proxy_test.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"test_helper"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProjectFilterProxyTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;TestCase&lt;/span&gt;
  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;".name filters documents by name"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"Test Project &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="si"&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;projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;FactoryBot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ProjectFilterProxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;".description filters documents by status"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Test project description &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="si"&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;documents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;FactoryBot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ProjectFilterProxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&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;To test our &lt;code&gt;FilterableControllers&lt;/code&gt; we will add a new test to their corresponding &lt;code&gt;IntegrationTests&lt;/code&gt; to test that requests to actions which filter parameters return filtered results. We need to define a filter payload to pass to our controller request and the corresponding filter proxy and compare that the results they return are the same. This time we will use fixtures for efficiency.&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;# test/controllers/projects_controller_test.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"controller_test_helper"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProjectsControllerTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ControllerTestHelper&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"GET index filters projects"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;filter_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:recipes_book&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;filtered_projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;projects_path&lt;/span&gt;

    &lt;span class="c1"&gt;# https://github.com/rails/rails-controller-testing&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:projects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filtered_projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;
  &lt;span class="k"&gt;end&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;&lt;strong&gt;Finally, we must ensure our &lt;code&gt;FilterProxy&lt;/code&gt; assembles filter queries as expected, combining all individual filter scope SQL clauses into a single query, ignoring all scopes with a blank value&lt;/strong&gt;. Instead of testing arbitrarily complex filter request results, we will check the composition of the resultant SQL queries, which is more aligned with the behavior we want to test.&lt;/p&gt;

&lt;p&gt;We already mentioned how the &lt;code&gt;to_sql&lt;/code&gt; method returns the raw SQL associated with a scope. We will use it to check the resultant &lt;code&gt;filter_by&lt;/code&gt; query contains all the filter scope &lt;code&gt;WHERE&lt;/code&gt; clauses from the filter scopes present in the test payload, and that there is no trace of filter scopes with empty values.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;

&lt;p&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"test_helper"&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FilterProxyTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;TestCase&lt;/span&gt;&lt;br&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvalidFilterProxy&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;Filters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FilterProxy&lt;/span&gt;&lt;br&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;br&gt;
    &lt;span class="vi"&gt;@proxy&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DocumentFilterProxy&lt;/span&gt;&lt;br&gt;
    &lt;span class="vi"&gt;@proxy_model_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Document&lt;/span&gt;&lt;br&gt;
    &lt;span class="vi"&gt;@proxy_scopes_module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Filters::DocumentFilterScopes"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constantize&lt;/span&gt;&lt;br&gt;
    &lt;span class="vi"&gt;@proxy_scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@proxy_model_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@proxy_scopes_module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"#filter_by combines all filter scopes into a single query"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"a name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :idea&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_filter_clause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@proxy_scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"a name"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_filter_clause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@proxy_scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:idea&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;br&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"#filter_by ignores scopes with empty/blank values"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :idea&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;assert_not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_filter_clause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@proxy_scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:idea&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;br&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"#filter_by ignores undefined scopes"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;invalid_attribute: &lt;/span&gt;&lt;span class="s2"&gt;"invalid value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :idea&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;assert_not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"invalid_attribute"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_filter_clause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@proxy_scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:idea&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_sql&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;br&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="kp"&gt;private&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="c1"&gt;# Extracts filter match condition from SQL query, discarding the SELECT part&lt;/span&gt;&lt;br&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sql_filter_clause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/WHERE (.*)/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;captures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;br&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;/p&gt;

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

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Conclusion&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;In this post, we have explained how to create a sustainable filter logic via the Query Object pattern.&lt;/p&gt;

&lt;p&gt;This pattern allows us to isolate common patterns to fetch data present in our business logic in sustainable modules that are easy to maintain and scale.&lt;/p&gt;

&lt;p&gt;We illustrated the pattern's versatility with the case scenario of the filters, but you can find many other patterns in your business logic that can be strong candidates to be implemented with this solution (for example, sorting).&lt;/p&gt;

&lt;p&gt;And that's it! I hope you found this article helpful. Please feel free to share any feedback or opinion in the comments, and thanks for reading it.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>filter</category>
      <category>queryobjectpatter</category>
      <category>sustainability</category>
    </item>
  </channel>
</rss>
