Autocomplete in Ruby on Rails using Stimulus
Tags: rails, stimulus, beginners, ruby
Published: 2020-08-10 15:29:52

---
title: Autocomplete in Ruby on Rails using Stimulus
published: true
date: 2020-08-10 15:00:00 UTC
tags: rubyonrails,stimulus,beginner,ruby
canonical_url: https://www.mskog.com/posts/autocomplete-in-ruby-on-rails-using-stimulus
---

 ![Autocomplete in Ruby on Rails using Stimulus](https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/photo-1534278931827-8a259344abe7.jpeg)

We are going to build a simple application in Ruby on Rails to autocomplete Reddit subreddits.  If you want to view the completed application first then it is available on [here](https://autocomplete-stimulus-example.herokuapp.com/).  The code is available on [Github](https://github.com/mskog/autocomplete_stimulus_example).

The finished application will look like this:

![Autocomplete in Ruby on Rails using Stimulus](https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-27_at_21.24.31-1.png)

We will be using [Stimulus](https://stimulusjs.org/) for this example. Stimulus is a simple Javascript framework that uses the HTML in the application. It is not a virtual DOM framework like React or Vue. I think it works very well when you need something more advanced than say Alpine.js but don't want to move to React or Vue. It works very well with Turbolinks as well so its a great fit for many applications.

Generate a new Rails application using [webpacker](https://github.com/rails/webpacker). Make sure you have Rails 6 installed(`rails -v`). As this application will not use a database then we can skip it as well.

`rails new autocomplete_stimulus --webpack --skip-active-record`

Install Stimulus:

`yarn add stimulus`

We also need autocomplete.js and axios:

`yarn add autocomplete.js axios`

There are many libraries out there for doing simple autocomplete and typeahead stuff. I am a big fan of [autocomplete.js by Algolia](https://github.com/algolia/autocomplete.js). You don't need to use [Algolia](https://www.algolia.com/) to use autocomplete.js however and it comes with all the bells and whistles like debounce support.

Add the following to `app/javascript/packs/application.js` to get Stimulus going. Make sure that you don't delete any of the other content.

```javascript
import { Application } from "stimulus";
import { definitionsFromContext } from "stimulus/webpack-helpers";

const application = Application.start();
const context = require.context("../stimulus/controllers", true, /\\.js$/);
application.load(definitionsFromContext(context));

```

Go into `config/routes.rb` and make sure it looks something like this:

```ruby
Rails.application.routes.draw do
  resource :home, only: [:show]

  root to: 'home#show'
end

```

We will be using `home#show` for our main page so create `app/controllers/home_controller.rb` and add the following code to it:

```ruby
class HomeController < ApplicationController
  def show; end
end

```

Finally we'll need a template for the HTML. Create `app/views/home/show.html.erb` and add the following:

```html
<div style="margin: auto; width: 25%; padding: 10px">
  <h1>Autocomplete</h1>
  <div data-controller="autocomplete">
    <input data-target="autocomplete.field" />
  </div>
</div>

```

Here you can see Stimulus in action. The `data-controller` is where Stimulus will start working. The value of the data attribute is important as that corresponds to a controller named `autocomplete_controller`. Let's create it now! Create the directory tree and file structure like so: `app/javascript/stimulus/controllers/autocomplete_controller.js`

Mind that the naming of the controller is very important in Stimulus so make sure you get it right.

Lets start with some boilerplate code in it:

```javascript
import { Controller } from "stimulus";

export default class extends Controller {
  static targets = ["field"];
}

```

The `targets` array contains all the targets of this Stimulus controller. As you can see it is what we added to the `input` tag in the template. Now we need to setup the Autocomplete part in the controller. The controller should now look something like this:

```javascript
import { Controller } from "stimulus";
import autocomplete from "autocomplete.js";

export default class extends Controller {
  static targets = ["field"];

  connect() {
    this.ac = autocomplete(this.fieldTarget, { hint: false }, [
      {
        source: this.search(),
        debounce: 200,
        templates: {
          suggestion: function (suggestion) {
            return suggestion.name;
          },
        },
      },
    ])
  }
}

```

The `connect` method is run whenever the controller is hooked up to the view. To access the field target we use `this.fieldTarget`. Mind that `this.search()` doesn't exist yet. Lets create it:

```javascript
import { Controller } from "stimulus";
import autocomplete from "autocomplete.js";

export default class extends Controller {
  static targets = ["field"];

  search(query, callback) {
    callback([{ name: "Hello" }])
  }

  connect() {
    this.ac = autocomplete(this.fieldTarget, { hint: false }, [
      {
        source: this.search,
        debounce: 200,
        templates: {
          suggestion: function (suggestion) {
            return suggestion.name;
          },
        },
      },
    ])
  }
}

```

Mind that we also imported Axios to perform HTTP requests. `source` expects a function so the search-function will return a function.

We are now ready to give this a try. Fire up the Rails server with `rails server`

If you go to localhost:3000 you should now see something like this:

![Autocomplete in Ruby on Rails using Stimulus](https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-25_at_12.03.28.png)

If you type something in the input field you should see "Hello" as a suggestion. Stimulus is now working. Excellent!

Now lets move on to actually wiring this up with the backend.

Go to `config/routes.rb` and make it look like this:

```ruby
Rails.application.routes.draw do
  resource :home, only: [:show]
  resource :autocomplete, only: [:show]

  root to: 'home#show'
end

```

We now need to create a controller for the route. Create the file `app/controllers/autocompletes_controller.rb` and add the following to it:

```ruby
class AutocompletesController < ApplicationController
  def show
    data = [
      { name: 'Foo' },
      { name: 'Bar' }
    ]

    render json: data
  end
end

```

We now have a placeholder controller. Now all we have to do is add this to our Stimulus controller to make it request the autocomplete from the server. Change your Stimulus controller to look like this:

```javascript
import { Controller } from "stimulus";
import axios from "axios";
import autocomplete from "autocomplete.js";

export default class extends Controller {
  static targets = ["field"];

  search(query, callback) {
    axios.get("/autocomplete", { params: { query } }).then((response) => {
      callback(response.data);
    });
  }

  connect() {
    this.ac = autocomplete(this.fieldTarget, { hint: false }, [
      {
        source: this.search,
        debounce: 200,
        templates: {
          suggestion: function (suggestion) {
            return suggestion.name;
          },
        },
      },
    ]).on("autocomplete:selected", (event, suggestion, dataset, context) => {
      this.ac.autocomplete.setVal(suggestion.name);
    });
  }
}

```

We have imported `axios` and changed the `search` function to perform requests instead of having placeholder data.

If you try this in the browser it should now always return "Foo" and "Bar". Now lets do it for real and autocomplete subreddit names shall we. I like to use the `HTTP` gem for http requests in my Rails application so install that by adding the following to your Gemfile:

`gem "http"`

Don't forget to restart your rails server after adding the gem.

Change your `AutocompleteController` to perform requests to Reddit:

```ruby
class AutocompletesController < ApplicationController
  def show
    response = JSON.parse(HTTP.get("https://www.reddit.com/subreddits/search.json?q=#{params[:query]}&limit=10"))
    subreddits = response["data"]["children"].map do |subreddit|
      {
        name: subreddit["data"].fetch("display_name").downcase,
        title: subreddit["data"].fetch("title"),
        description: subreddit["data"].fetch("public_description").truncate(100)
      }
    end
    render json: subreddits
  end
end
```

This should be it. If you try this in your browser it should look something like this:

![Autocomplete in Ruby on Rails using Stimulus](https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-25_at_12.24.51.png)

It works! Looks real bad though. Lets add some styling to fix it.

Create `app/assets/stylesheets/style.scss` and add the following to it:

```scss
.algolia-autocomplete {
  width: 100%;
}
.algolia-autocomplete .aa-input,
.algolia-autocomplete .aa-hint {
  width: 100%;
}
.algolia-autocomplete .aa-hint {
  color: #999;
}
.algolia-autocomplete .aa-dropdown-menu {
  width: 100%;
  background-color: #fff;
  border: 1px solid #999;
  border-top: none;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion {
  cursor: pointer;
  padding: 5px 4px;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor {
  background-color: #b2d7ff;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion em {
  font-weight: bold;
  font-style: normal;
}

```

Now try again!

![Autocomplete in Ruby on Rails using Stimulus](https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-27_at_21.02.02.png)

A bit better! Of course, this still looks bad and will need some more love to be used for real.

As an added bonus you can easily change the Stimulus controller so that it gets its API URL from a data attribute instead of having it hard coded in the controller itself. This way you can reuse the Stimulus controller for autocompletes all over the place!

All it takes is to change the HTML to this:

```html
<div style="margin: auto; width: 25%; padding: 10px">
  <h1>Autocomplete</h1>
  <div data-controller="autocomplete" data-autocomplete-url="/autocomplete">
    <input data-target="autocomplete.field" />
  </div>
</div>

```

Note that data attribute that holds the URL. It is also a [Stimulus thing](https://stimulusjs.org/reference/data-maps) so make note of it. Now change the Stimulus controller like so:

```javascript
import { Controller } from "stimulus";
import axios from "axios";
import autocomplete from "autocomplete.js";

export default class extends Controller {
  static targets = ["field"];

  source() {
    const url = this.data.get("url");
    return (query, callback) => {
      axios.get(url, { params: { query } }).then((response) => {
        callback(response.data);
      });
    };
  }

  connect() {
    this.ac = autocomplete(this.fieldTarget, { hint: false }, [
      {
        source: this.source(),
        debounce: 200,
        templates: {
          suggestion: function (suggestion) {
            return suggestion.name;
          },
        },
      },
    ]).on("autocomplete:selected", (event, suggestion, dataset, context) => {
      this.ac.autocomplete.setVal(suggestion.name);
    });
  }
}

```

But what if you want to have some other rendering of the suggestions? Perhaps you would like to add some images or a nice description or something? There are a number of ways to do this. For example, you can use a simple Javascript template string with some HTML in the Stimulus controller and return that in the `suggestion` function. This works but its real dirty to deal with it once the string gets [c](http://complicated.It)omplicated. You could also add some kind of Javascript template system like Mustache in separate files and import those in your controller using a metaprogrammed data attribute. I prefer to keep the suggestion next to the rest of the autocomplete form, so I will show you a way to do it using [Mustache](https://github.com/janl/mustache.js) and script tags. Mind that you don't really need Mustache for this but it makes things a bit cleaner.

Start by installing Mustache:

`yarn add mustache`

Now, change your HTML to this:

```html
<div style="margin: auto; width: 25%; padding: 10px">
  <h1>Autocomplete</h1>
  <div data-controller="autocomplete" data-autocomplete-url="/autocomplete">
    <script data-target="autocomplete.suggestionTemplate" type="text/template">
      <h2>{{name}}</h2>
      <p>{{description}}
    </script>
    <input data-target="autocomplete.field" />
  </div>
</div>

```

Note the new `script` tag. The browser will not render that so it's safe to use. The curly braces are Mustache tags which will correspond to the JSON data from our autocomplete controller on the Ruby side. We also added a new target data attribute to be able to access it from Stimulus. Lets do that now:

```javascript
import { Controller } from "stimulus";
import axios from "axios";
import autocomplete from "autocomplete.js";
import Mustache from "mustache";

export default class extends Controller {
  static targets = ["field", "suggestionTemplate"];

  source() {
    const url = this.data.get("url");
    return (query, callback) => {
      axios.get(url, { params: { query } }).then((response) => {
        callback(response.data);
      });
    };
  }

  connect() {
    const suggestionTemplate = this.suggestionTemplateTarget.innerHTML;

    this.ac = autocomplete(this.fieldTarget, { hint: false }, [
      {
        source: this.source(),
        debounce: 200,
        templates: {
          suggestion: (suggestion) => {
            return Mustache.render(suggestionTemplate, suggestion);
          },
        },
      },
    ]).on("autocomplete:selected", (event, suggestion, dataset, context) => {
      this.ac.autocomplete.setVal(suggestion.name);
    });
  }
}

```

We've added Mustache, the new target and a new way to render suggestions using our new template. It will look something like this:

![Autocomplete in Ruby on Rails using Stimulus](https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-27_at_21.24.31.png)

Ok that looks terrible but you get the idea. Do note that you can still reuse the Stimulus controller for other autocomplete fields. Simply add a new template and URL!

There you have it! A simple way to add a reusable Stimulus component to your Rails applications. The complete application application is available on [Heroku](https://autocomplete-stimulus-example.herokuapp.com/) and the code is available on [Github](https://github.com/mskog/autocomplete_stimulus_example).