Getting Started with Stimulus in Rails (The Basics)

Create New Rails App

Make sure you have setup Rails. You can read the guide here:

https://guides.rubyonrails.org/getting_started.html

Before we being make sure we are running node version 18 or above:

nvm use 18

Create our new app rails-view-components

rails new rails-stimulus-demo --css=tailwind --javascript=esbuild
cd rails-stimulus-demo

Spin up the server to test everything is running:

./bin/dev

Setup Home Controller

Create a new controller at app/controllers/home_controller.rb

class HomeController < ApplicationController
  def index
  end
end

Create the view app/views/home/index.html.erb

Hello World

Set it as the default route config/routes.rb

root "home#index"

Refresh the page and we should be in business!

Create a stimulus controller

We will create a controller that automatically grabs the YouTube ID from the share url when pasted in. We can use the rails generator to do this:

rails g stimulus external_platform_id

We can now see our newly created controller here app/javascript/controllers/external_platform_id_controller.js:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="external-platform-id"
export default class extends Controller {
  connect() {
  }
}

You can also check the index.js file to see that it has been registered to be loaded.

import YoutubeIdFieldController from "./youtube_id_field_controller"
application.register("youtube-id-field", YoutubeIdFieldController)

Connect Input to Controller

In our index.html.erb file let's set up an input which will be connected to the stimulus controller:

<input
  class="border border-gray-300 rounded-md p-2 m-4"
  data-controller="youtube-id-field"
  type="text"
/>

The key piece here is the data-controller property. This tells stimulus which controller to use. You can add multiple controllers here by separating them with a space but we will focus on one today.

I like to sanity test to make sure we are connected properly to avoid hours of debugging!

In the connect() function add a console.log call to get the connected element:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="youtube-id-field"
export default class extends Controller {
  connect() {
    console.log(this.element)
  }
}

When you open up the console you should now see the element and can hover over it to highlight it in the browser.

Extracting the YouTube ID:

In our controller add a new method/action that will extract the ID for us:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="youtube-id-field"
export default class extends Controller {
  connect() {
    console.log(this.element)
  }

  extractYoutubeId(e) {
    const { value } = e.target
    console.log(value)
  }
}

Then in the index.html.erb file add link the action:

<input
  class="border border-gray-300 rounded-md p-2 m-4"
  data-action="youtube-id-field#extractYoutubeId"
  data-controller="youtube-id-field"
  type="text"
/>

Now when we paste something in to the input we should see the input value being logged to the console.

Let's add the regex required to strip all of the url except the ID:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="youtube-id-field"
export default class extends Controller {
  connect() {
    console.log(this.element)
  }

  extractYoutubeId(e) {
    const { value } = e.target

    // https://youtu.be/aQuu_-R2Pwg
    // https://www.youtube.com/watch?v=aQuu_-R2Pwg
    // YOUTUBE_ID = aQuu_-R2Pwg
    const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([A-Za-z0-9\-\_]+)/

    const match = value.match(youtubeRegex)
    if (match) {
      e.target.value = match[1]
    }
  }
}

Paste in a valid YouTube url and watch it do its magic!

https://youtu.be/K-XQIas9Asw

Refactoring to use Targets

Let's setup a slightly more advanced example using targets.

Update our index.html.erb file to this:

<div
  class="flex gap-2 p-4"
  data-controller="external-platform-id"
>
  <input
    class="border border-gray-300 rounded-md p-2"
    data-action="external-platform-id#extractYoutubeId"
    placeholder="Paste a YouTube URL"
    type="text"
  />

  <input
    class="border border-gray-300 rounded-md p-2"
    data-external-platform-id-target="idField"
    type="text"
  />
</div>

We will allow the user to paste in one field and show the extracted ID in the other.

We defined the targets using the following:

static targets = ["idField"]

This matches the data attribute in the HTML:

data-external-platform-id-target="idField"

We can check that we have the target registered by using the naming convention this.has[Name]Target :

this.hasIdFieldTarget

Our updated controller looks like this:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="external-platform-id"
export default class extends Controller {
  static targets = ["idField"]

  extractYoutubeId(e) {
    const { value } = e.target

    // https://youtu.be/aQuu_-R2Pwg
    // https://www.youtube.com/watch?v=aQuu_-R2Pwg
    // YOUTUBE_ID = aQuu_-R2Pwg
    const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([A-Za-z0-9\-\_]+)/

    const match = value.match(youtubeRegex)
    if (match && this.hasIdFieldTarget) {
      this.idFieldTarget.value = match[1]
    }
  }
}

Now when we paste the url in the field we get the ID in the other.

Using Values

We can add values to our controller like so:

<div
  class="flex gap-2 p-4"
  data-controller="youtube-id-field"
  data-youtube-id-field-platform-value="youtube"
>
  <input
    class="border border-gray-300 rounded-md p-2"
    data-action="youtube-id-field#extractYoutubeId"
    placeholder="Paste a YouTube URL"
    type="text"
  />

  <input
    class="border border-gray-300 rounded-md p-2"
    data-youtube-id-field-target="idField"
    type="text"
  />
</div>

We have added a value for platform and set it to youtube we can use this to add more functionality in the future if required.

Add it to the stimulus controller here:

static values = {
  platform: String
}

We can access the value using this.platformValue

Let's update our controller to support this:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="youtube-id-field"
export default class extends Controller {
  static targets = ["idField"]
  static values = {
    platform: String
  }

  extractPlatformId(e) {
    const { value } = e.target

    if (this.platformValue === "youtube") {
      // https://youtu.be/aQuu_-R2Pwg
      // https://www.youtube.com/watch?v=aQuu_-R2Pwg
      // YOUTUBE_ID = aQuu_-R2Pwg
      const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([A-Za-z0-9\-\_]+)/

      const match = value.match(youtubeRegex)
      if (match && this.hasIdFieldTarget) {
        this.idFieldTarget.value = match[1]
      }
    }
  }
}

And then just update the html:

<div
  class="flex gap-2 p-4"
  data-controller="youtube-id-field"
  data-youtube-id-field-platform-value="youtube"
>
  <input
    class="border border-gray-300 rounded-md p-2"
    data-action="youtube-id-field#extractPlatformId"
    placeholder="Paste a YouTube URL"
    type="text"
  />

  <input
    class="border border-gray-300 rounded-md p-2"
    data-youtube-id-field-target="idField"
    type="text"
  />
</div>

When we change the platform value in the html to something else we will see the code is no longer called. This is useful when you want to set values using data from rails to control how controllers function.

Conclusion

Hopefully that gave you a very basic intro in to Stimulus and the Hotwire suite of tools for Rails. If you want me to dive deeper and show you more complex examples please drop a comment below.

View More Posts

Portfolio built using the open source project by

Initium Studio

Create your own Next.js Portfolio with Contentful CMS by forking this repo onGitHub

Blog

|

Projects

|

Health

|

Recipes

|

About

|

Contact

2024 Greeff Consulting Pty Ltd