Live query render with Rails 6 and Stimulus JS
Tags: ruby, rails, stimulus, javascript
Published: 2020-08-20 10:47:54

---
title: Live query render with Rails 6 and Stimulus JS
published: true
description: Live query render with Rails 6 and Stimulus JS
tags: ruby, rails, stimulusjs, js
---

I thought I'd give [Stimulus](https://stimulusjs.org/) another try with a side project I'm working on. This time, I only wanted a "splash" of JavaScript magic here and there while I keep our Lord and Saviour in mind, DHH, when designing.

[DHH talks about his love for server-side rendering](https://signalvnoise.com/posts/3697-server-generated-javascript-responses) and [how to break down your controller logic into what I call, "micro-controllers"](https://www.fullstackradio.com/episodes/32). This approach makes much sense, to me.

I'm coming from a React frontend development where I separate the client from the server (api). Everything is done through Restful fetching which returns json. When doing a search/query, you fetch the data then update your state with the returned data and that's how you'd implement a live query. A live query is when you have an input field, the user makes a query and the list updates instantly or, a dropdown is populated with the results. Things work differently with jQuery or Stimulus. In our case, we'll be using Stimulus.

##Perquisites:

* You have Rails 5+ installed
* You have Stimulus installed
* You do not have jQuery installed - 😁 🥳 - Ok, you can but not needed

We won't be using any `js.erb` files here since we're using Stimulus. If [Basecamp doesn't uses it](https://discourse.stimulusjs.org/t/sharing-html-between-stimulus-and-rails-view/463/6), I thought I'd follow suit.

Let's say we have a URL `/customers`, and a controller called `customers_controller.rb`:

```rb
# before_action :authenticate_user! # For Devise
[..]

def index
  @customers = Customer.all.limit(100)
end

[..]
```

And our views `views/customers/index.html.erb`:

```erb
<main>
  <!-- Filter section -->
  <section>
    <input type="text" name="query" value="" placeholder="Search" />
  </section>

  <!-- Results section -->
  <section data-target="customers.display">
   <%= render partial: 'shared/customer_row', locals: {customers: @customers}  %>
  </section>
</main>
```

##Partials
Inside `views/shared/_customer_row.html.erb`:

```erb
<ul>
  <% customers.each do | customer | %>
    <li><%= customer.first_name + ' ' + customer.surname %></li> 
  <% end %>
</ul>
```

With this minimal setup, we should see a text input field and a list of customers.

##JS Magic with Stimulus
As the user types in our text field (input), we need to submit that data to the server (controller). To do that, we need few things:

* A stimulus controller `customers_controller.js`
* a form

```js
// Stimulus controller
import { Controller } from "stimulus"
import Rails from "@rails/ujs"

export default class extends Controller {
  static targets = [ "form", "query", "display"]

  connect() {
    // Depending on your setup
    // you may need to call
    // Rails.start()
    console.log('Hello from customers controller - js')
  }

  search(event) {
    // You could also use
    // const query = this.queryTarget.value
    // Your call.
    const query = event.target.value.toLowerCase()
    console.log(query)
  }

  result(event) {}

  error(event) {
    console.log(event)
  }
}
```
I won't go into how Stimulus works but do have a read on their [reference](https://stimulusjs.org/reference/controllers).

Let's update the `html`:

```erb
<main data-controller="customers">
  <!-- Filter section -->
  <section>
    <form
      data-action="ajax:success->customers#result"
      data-action="ajax:error->customers#error"
      data-target="customer.form"
      data-remote="true"
      method="post"
      action=""
    >
      <input
        data-action="keyup->customers#search"
        data-target="customers.query"
        type="text" 
        name="query" 
        value=""
        placeholder="Search"
      />
    </form>
  </section>

  <!-- Results section -->
  [..]
</main>
```
Refreshing the page then check you browser console, you'd see the message "Hello from customers controller - js". If not, stop and debug you have Stimulus installed correctly and the controller name is present on your html element: `data-controller="customers"`. When entering a value in the input, you should see what you've typed being logged in your browser console.

## Micro Controllers
[This post talks about how DHH organizes his Rails Controllers](http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/). We'll use same principles here.

Inside our rails app `controllers/customers/filter_controller.rb`

```rb
class Customers::FilterController < ApplicationController
  before_action :set_customers
  include ActionView::Helpers::TextHelper
 
  # This controller will never renders any layout.
  layout false

  def filter
    initiate_query
  end

  private
    def set_customers
      # We're duplicating here with customers_controller.rb's index action 😬
      @customers = Customer.all.limit(100)
    end

    def initiate_query
      query = strip_tags(params[:query]).downcase

      if query.present? && query.length > 2
        @customers = Customers::Filter.filter(query)
      end
    end
end
```

## Routing
Inside `routes.rb`

```rb
[..]

post '/customers/filter', to: 'customers/filter#filter', as: 'customers_filter'

[..]
```
We've separated our filter logic from our CRUD customers controller. Now our controller is much simpler to read and manage. We've done the same for our model `Customers::Filter`. Let's create that:

Inside `model/customers/filter.rb`:

```rb
class Customers::Filter < ApplicationRecord
  def self.filter query
    Customer.find_by_sql("
      SELECT * FROM customers cus
      WHERE LOWER(cus.first_name) LIKE '%#{query}%'
      OR LOWER(cus.surname) LIKE '%#{query}%'
      OR CONCAT(LOWER(cus.first_name), ' ', LOWER(cus.surname)) LIKE '%#{query}%'
    ")
  end
end
```
Wow? No. This is just a simple query for a customer by their first name and surname. You may have more logic here, but for brevity, we keep it short and simple.

Though our `Customers::FilterController` will not use a layout, we still need to render the data, right? For that, we need a matching action view name for `filter`. Inside `views/customers/filter/filter.html.erb`:

```erb
<%= render partial: 'shared/customer_row', locals: {customers: @customers}  %>
```
This is what our returned data will looks like - it's server-side rendered HTML.

Now we need to update our form's action `customers_filter` then fetch some data as we type:

```erb
[..]
<!-- Filter section -->
<section>
  <form
    data-action="ajax:success->customers#result"
    data-action="ajax:error->customers#error"
    data-target="customer.form"
    data-remote="true"
    method="post"
    action="<%= customers_filter_path %>"
  >
    <input
      data-action="keyup->customers#search"
      data-target="customers.query"
      type="text" 
      name="query" 
      value=""
      placeholder="Search"
    />
  </form>
</section>
[..]
```

Remember we got `customers_filter` from `routes.rb`. We now need to update our js:

```js
[..]

search(event) {
  Rails.fire(this.formTarget, 'submit')
}

result(event) {
  const data = event.detail[0].body.innerHTML
  if (data.length > 0) {
    return this.displayTarget.innerHTML = data
  }
  
  // You could also show a div with something else?
  this.displayTarget.innerHTML = '<p>No matching results found</p>'
}

[..]
```

In our `search()`, we don't need the query as it's passed to the server via a param. If you have any business logics that need the query text, in JS, then you can do whatever there. Now when you make a query, the HTML results update automatically.

## Update
You should noticed I'm duplicating `@customers = Customer.all.limit(100)`. Let's put this into a concern.

Inside `controllers/concerns/all_customers_concern.rb`

```rb
module AllCustomersConcern
  extend ActiveSupport::Concern

  included do
    helper_method :all_customers
  end
  
  def all_customers
    Customer.all.limit(100)
  end
end
```

Next, update all controllers:

```rb
class CustomersController < ApplicationController
  include AllCustomersConcern

  def index
    @customers = all_customers
  end

  [..]
end

class Customers::FilterController < ApplicationController
  [..]
  include AllCustomersConcern
  [..]
  private
    def set_customers
      @customers = all_customers
    end
end
```

## Conclusion
Rails with Stimulus make it very easy to build any complex filtering system by breaking down logics into micro controllers. Normally I'd put everything in one controller but I guess DHH's approach becomes very useful.

Typos/bugs/improvements? Feel fee to comment and I'll update. I hope this is useful as it does for me. Peace!

## Thanks
A huge shout out to [Jeff Carnes](https://discourse.stimulusjs.org/u/welearnednothing) for [helping me out](https://discourse.stimulusjs.org/t/live-search-without-the-entire-page-reload-just-the-partial/1339). I've never done this before and I'm well pleased.