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
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!
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)
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.
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!
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.
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.
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.