Build a Dynamic Search With Stimulus.js and Rails 6
By: Maria Schuessler
Tags: JavaScript, Software Development, Learn To Code, Software Engineering, Programming

Build a Dynamic Search With Stimulus.js and Rails 6

Understand the essentials of Stimulus to jump-start your app

Photo by Andrew Ridley on Unsplash

Before 2018, the Rails framework was stuck in the past. There was a new, dynamic JavaScript way to separate browser-side and server-side development. The web was suddenly built not out of divs, but out of components. It was a faster, more modular approach. Then, the team behind Rails released Stimulus.

Rails 6 integrates beautifully with Turbolinks, which speed up web applications by intercepting HTML requests via HTML. However, the downside of Turbolinks is that they wipe out the entire <body> of the page. This is where StimulusJS comes in.

If you haven’t heard of it before, Stimulus is a lightweight JavaScript framework by the creators of Rails. Think of it like a lightweight React.js— or a less painful Angular.js. Stimulus works by creating a set of controllers to store state and refresh dynamic attributes of the page. I’ve put together a primer to using StimulusJS here, so make sure you are familiar with the framework before starting this search tutorial.

Step 1: Make the base application

The first steps are creating basic MVC for Rails —skip this if you already have an application. I’m setting up an application with PostgreSQL to search through a database of cocktails that I’ve seeded via a JSON API.

#terminal rails new stimulus-search --database=postgresql

cd stimulus-search rails db:create

Next, I am creating my searchable model with three attributes: the cocktail name, the type of glass it is served in, and the mode of preparation. I want to build a search to be able to search by all three of these values.

rails g model cocktail name glass preparation:text

rails db:migrate

Then, add the cocktails from the IBA database:

#db/seeds.rb

url = " require 'open-uri'url = " https://raw.githubusercontent.com/maltyeva/iba-cocktails/master/recipes.json Cocktail.delete_all if Rails.env.development? cocktails = JSON.parse(open(url).read)

cocktails.each do |cocktail|

Cocktail.create!(name: cocktail["name"], glass: cocktail["glass"], preparation: cocktail["preparation"])

end

Finally, create a simple index page to list all of the cocktails. I am setting up a cocktails controller with the index action, which will also serve as the homepage of the application.

#shell

rails g controller cocktails index --skip-routes #config/routes.rb

root "cocktails#index" #app/controllers/cocktails_controller.rb def index

@cocktails = Cocktail.all

end

In the frontend, I am looping through a list of these cocktails with each loop.

#app/views/cocktails/index.html.erb

<h1 class="text-center">Cocktails</h1>

<div class="row justify-content-center">

<div class="col-xs-12 col-sm-6">

<%= render

</div>

</div>

</div> Cocktails @cocktails %> #app/views/cocktails/_cocktail.html.erb

<h4>

<%= cocktail.name %><small><%= cocktail.glass %> glass</small></h4>

<p><%= cocktail.preparation %></p>

The final result, with the search form added in the next.

Step 2. Add the search

One of the easiest ways to build a powerful, dynamic search in a Rails app is through the pg_search gem. PgSearch builds Active Record named scopes that take advantage of postgresql full-text search to create search queries. These searches can be faceted, weighted, and can search across multiple tables. Start by adding the gem into the application.

#Gemfile

gem 'pg_search'

#shell

bundle install

Add the gem to the model which will be queried. The first line needs to be included to connect the model to PgSearch. The pg_search_scope will define how the model will be queried:

pg_search_scope — needs to be declared to create the scope

global_search — the name of the scope, this can be arbitrary

against — the fields against which the search will be queried

tsearch: {prefix: true} — this option permits partial word search. i.e. Mar would return all of the results for Margarita and Martini.

#app/models/cocktail.rb include PgSearch::Model pg_search_scope :global_search,

against: [:name, :glass, :preparation],

using: {

tsearch: { prefix: true }

}

Once the scope is defined, it can be added to the controller

#app/controllers/cocktails_controller.rb

if params[:query].present?



else



end

end def indexif params[:query].present? @cocktails = Cocktail.global_search(params[:query])else @cocktails = Cocktail.allendend

To enable search, add a form to the view to trigger the controller. The traditional way of creating a form in Rails has been to use form_for for forms requiring a model and form_tag for forms without a model object. In Rails 6, these two forms have been combined into one helper called form_with and I’ll use this helper in the tutorial.

Set up the form:

#app/views/cocktails/index.html.erb ....

<%= form_with(url: "/", method: :get) do |f| %>

<%= label_tag(:query, "Search for") %>

<%= text_field_tag(:query) %>

<%= submit_tag("Search", class: "btn btn-primary") %>

<% end %>

....

The form_with helper takes a URL as an argument. Because my search will return the query on the same page (the home page) my URL is just “/” and the form method is get to perform the database query instead of writing it to the database.

Step 3. Add Stimulus.js

Finally, you’re ready to dynamically refresh the page with Stimulus.js.

Install Stimulus if you haven’t already. If you’re not familiar with Stimulus, I put together a basic primer here and the team behind Stimulus has created a handbook for best practices.

#shell

rails webpacker:install:stimulus

Add the Stimulus controller we will use for the search

#shell

touch app/javascript/controllers/search_controller.js #app/javascript/controllers/search_controller.js

import { Controller } from 'stimulus'; export default class extends Controller {

connect() {

console.log("hello from stimulus")

}

}

Stimulus.js works by connecting to the DOM through actions and targets . Think of the action like an eventListener and a target as an element selected by a querySelector .

Connect to the Stimulus controller by adding a data-controller attribute around the search form and the results

#app/views/cocktails/index.html.erb

<div class="container">

<h1 class="text-center">Cocktails</h1>

<div class="row justify-content-center" data-controller="search">

[... search form and content here...]

</div>

</div>

Once the controller attribute is added, refresh to see the output of the connect method, which will run on every page load.

4. Connect stimulus to the search

If the Stimulus controller connected successfully, it’s time to render the search.

The process will look something like this:

User enters the keyword into the form When the form is submitted, it calls the action in the Stimulus controller The action makes a fetch request to the Rails backend The backend provides a JSON response containing the filtered cocktails

Connect the form to the Stimulus controller:

#app/views/cocktails/index.html.erb

...

<%= form_with(url: "/", method: :get, data: {action: "submit->search#submit"}) do |f| %>

<%= label_tag(:query, "Search for") %>

<%= text_field_tag :query, params[:query], data: {target: "search.query"} %>

<%= submit_tag("Search", class: "btn btn-primary") %>

<% end %>

The data-action attribute listens to the form submit to trigger the corresponding action called submit. In addition, the text_field_tag has a data-target attribute to capture the user’s input.

#app/javascript/controllers/search_controller.js

export default class extends Controller {

static targets = ["query", "cocktails"] submit() {

const value = this.queryTarget.value

fetch(`/?query=${value}`, {

headers: { accept: 'application/json'}

}).then((response) => response.text())

.then(data => console.log(data))

}

}

The submit function inside the Stimulus controller calls the JavaScript fetch API to make an HTTP request to the Rails controller and return a promise with the response data.

Finally, the Rails controller must be able to render data both as HTML and JSON to properly send back the data to Stimulus.



class CocktailsController < ApplicationController

def index

if params[:query]



else



end

respond_to do |format|

format.html

format.json { render json: { cocktails:

end

end

end #app/controllers/cocktails_controller.rbclass CocktailsController < ApplicationControllerdef indexif params[:query] @cocktails = Cocktail.global_search(params[:query])else @cocktails = Cocktail.allendrespond_to do |format|format.htmlformat.json { render json: { cocktails: @cocktails } }endendend

The final result will be a JSON object with the new data, that looks something like this.

Step 6. Dynamically update the view

The last step is to replace the cocktails on the frontend with the ones from the JSON list. Before Stimulus, the best way to achieve this would have been by creating a js.erb file and manually writing a function to replace the HTML on the page. With Stimulus, this logic is abstracted into the Stimulus controller.

When the fetch request is returned from the Rails controller back to Stimulus, it comes back as a JSON object, with the key of cocktails and an array containing all of the filtered cocktails. To display the new cocktails, first, iterate through the array and then replace the current cocktails HTML with the new cocktail list. In order to do this, you will need to create a JSX template for the new cocktail that will mimic the cocktail partial in the view.

This step used to be a part of the Rails since it was possible to use erb inside of the js.erb files, however, with Stimulus, this is handled by the JavaScript file and needs to be passed from the JSON object.

#app/javascript/controllers/search_controller.js submit() {

const value = this.queryTarget.value

fetch(`/?query=${value}`, {

headers: { accept: 'application/json'}

}).then((response) => response.json())

.then(data => {

var cocktailHTML = "";

var cocktailArray = Object.values(data)[0]

cocktailArray.forEach(cocktail => {

cocktailHTML += this.cocktailTemplate(cocktail)

});

this.cocktailsTarget.innerHTML = cocktailHTML;

});

} cocktailTemplate(cocktail) {

return `<div>

<h4>${cocktail.name} <small>${cocktail.glass}</small></h4>

<p>${cocktail.preparation}</p>

</div> `

}

The forEach loops over every object inside of the array and append them to an empty div, using the HTML rendered by the cocktailTemplate function. Then, it replaces the innerHTML inside of the cocktails target, as defined in the view file:

#app/views/cocktails/index.html.erb

<div id="cocktails" data-target="search.cocktails">

<%= render

</div>

[...form, etc...] @cocktails %>

Now, the filter updates dynamically and reloads the search results without refreshing the page. Your Stimulus search is complete. Now, kick back and enjoy a cocktail.

GitHub repo here: https://github.com/maltyeva/stimulus-search

Heroku app here: https://stimulus-search.herokuapp.com/

Alternative: Generate JSON response via Rails.ajax

An alternative way to string interpolation on the front-end is to render the JSON response directly from the server using unobtrusive javascript (rails-ujs) instead of the fetch api. This can be a great solution if you are rendering images in the partial, or to ensure csrf protection in the request. Find more about it here.

Replace the submit() method in the search controller with the Rails.ajax() call. Don’t forget to import Rails at the top of the controller files to avoid a ‘Rails is undefined’ error.

#app/javascript/controllers/search_controller.js import Rails from " @rails/ujs "; submit() {

const value = this.queryTarget.value

Rails.ajax({

type: "GET",

url: `/?query=${value}&day=${day}`,

success: (_data, _status, xhr) => {

this.element.outerHTML = xhr.response

}

})

}

Thanks to @adrienpoly for this suggestion!

Find the full code in the GitHub repository here: https://github.com/maltyeva/stimulus-search/tree/rails-ajax