Skip to main content

37 Stimulus JS Framework Introduction

Episode 225 · January 23, 2018

A look into Stimulus JS, a new Javascript framework by Basecamp to pair closely with Turbolinks

Javascript


Transcripts

What's up guys? Welcome back, we are going to dive into the Stimulus JavaScript framework this episode, I'm going to give you an introduction to show you how it compares to other JavaScript framewoks and libraries, and then we'll dive into taking a look at some examples, but then, in a future episode, we'll dive into some more complex stuff. This is just your primer, your introduction, we're going to take a look at all of that in this episode. Where does Stimulus fit in? and why do we need another JavaScript framework? and why are you calling it a framework in quotes? Well, Stimulus is very different from what you might expect, so when you think of a JavaScript framework these days, you think of something that helps you rendering HTML, handling events, worrying about your state, doing all kinds of other things like sub components and organizing all your code and all stuff, well it's not really what Stimulus does, it does some of that, but it's designed mostly to just handle events in an organized manner, and so we have to first take a look at where Basecamp has come and their approach on JavaScript on the front end, and so we'll jump back to turbolinks real quick and talk about Turbolinks and why it exists.

There was a time when everybody was talking about how fast it was to build your front end in JavaScript and render out all of the HTML and that was way faster than browsers being sent a GET request, having to reload all of your JavaScript and CSS and making those pages slow just because the browser would clear out all of that on the next request, and so Turbolinks was introduced to give you the speed of a single page app without any of the complexity, so all it does is listen to link clicks and say: Well, instead of doing that with the regular browser process, let's just do that with AJAX, let's replace the DOM with a new DOM, and rerun your JavaScript as necessary, and that is that. That worked out really nicely in a lot of cases, but it didn't address the entire problem that other front end frameworks and single page app frameworks did, so those took care of helping you render HTML, they took care of the state, they did a lot of extra stuff and so that is where Turbolinks fell flat, it only made new page request quick but you were still required to use something like vanilla JavaScript or jQuery to handle events to build out complex forms or wizards or anything like that that might be interactable in a page, and so Stimulus is being introduced as an alternative to doing your front-end framework as a framework again, and you don't have to go through and come up with your own structure for vanilla JavaScript, you don't have to use jQuery either, and so this is designed more as a competitor to jQuery more than is a competitor to Ember, Angular, React or Vue. Those are all still fairly heavy front end things, and Stimulus and Turbolinks are designed to do one thing and do one thing really well, and they kind of fit really well together because they don't step on each other's toes or anything like that, they do very separate things and so Stimulus is mostly designed to help take events that happen on your HTML and then run some JavaScript and basically you just define data attributes and where jQuery might have said: Well, we need to look up this element if it's on the page then we need to connect this event listener and all that. Those things are taken care of for you in Stimulus, and all you do is write some data attributes like data controller and data target and data action and those will be wired up for you and all you have to do is implement the equivalent methods inside of your controller to make those functions work, and so this is kind of like building out your jQuery events in a much more structured way and so you're unlikely to get as much spaguetti JavaScript code as you might where you accidentaly do things in jQuery and don't structure them correctly, so this is very much a competitor to jQuery more than it is with Vue or Angular or React or Ember, and that's something important to keep in mind because I think a lot of people read this and they're like: Does it do AJAX requests for me? Does it handle state? Does it render HTML? NO Pretty much none of that, it just really focuses on events because you also have rails Vuejs to do your AJAX requests, that's built in, you have Turbolinks to make your page render the new content or whatever, and so Stimulus is just designed to say: Well, we have this HTML and we want to make it interactable, how do we do that? And so that is where Stimulus fits in.

Let's dive into and example application, and let's generate a new app. We're going to use webpacker to install Stimulus, so let's just call this

rails new stimulating --webpack

You can always go to the webpacker installation instructions and add webpacker to an old application if that's what you would like to do. This will just go ahead and install it automatically for us so we don't have to worry about that. Once this is set up, we can then go and create our Stimulus set up code inside code inside of our JavaScript pack tag and all of that, so we'll get a Stimulus set up and then we'll be able to use that anywhere in our application. So if we go into this stimulating ap we can

cd stimulating
yarn add stimulus

That's going to install the npm package for us, and then we can open up our application here, we're going to need to go and do one thing first, we're going to go to the application.html.erb, and we'll grab this JavaScript include tag and change it to a pack tag. That will load our app/javascript/packs/application.js, now we don't need any of this in here really, but wo do need to set up our initalizer code for stimulus, so let's take a look at that. We need to import

import { Applicatoin } from 'stimulus'
import { autoload } from 'stumulus/webpack-helpers'

const application = Application.start()
const constrollers = require.context("./controllers", true, /\.js/ )
autoload(controllers, application)

What that's going to do is require us to have a app/javascript/packs/controllers folder, you cal also do ../controllers if you wanted app/javascript/controllers, but we're gonna do that just in the standard folder, so we'll have controllers here, and that folder will show up in a second once we add app/javascript/packs/controllers/hello_controller.js so you need to name it with the same name and underscore like you would with rails, and then underscore controller.js at the end, so this is very similar to how you do a controller file name in rails, and then the difference once you create one of those is that you're going to create a class here and export that. So here first we need to

import { Controller } from 'stimulus'

export default class extends Controller {}

So I have an unnamed class that inherits from controller and then we're going to export this so that when the autoload loads it it will be able to get this class and use access to that somewhere else, and in here we can do all of our actions that we would like, so we don't have any pages in our rails app yet, so let's go create one.

rails g scaffold Event name

let's leave it at that for now and we'll go back in another episode and so some more complex stuff with Stimulus, with this done, let's run

rails db:migrate

Let's go to our rails app and go to the routes file and sert

root to: 'events#index'

Save that, and let's go to the events index.html.erb and in here is where we can begin by adding our Stimulus stuff, so let's just create a new tag here at the bottom

<div data-controller="hello">
  <input data-target="helo.name">

You might think that you could just type name here like you would probably do on your own, but you actually need to namespace this under the controller name for it to work properly, and so keep that in mind, but that's going to help in case you mix these in with other ones, you'll know that this is the name for the hello controller, and you might have html mixed in with another component that has a name as well, and so that way it doesn't get confused.

<div data-controller="hello">
  <input data-target="helo.name" type="text">
  <button data-action="click->hello#log">Log</button>
</div>

This one syntax is very simple, you have the event name similar to all the event names that you would listen to like on submit or on click or on paste or key up, any of those things you can do and then you can tell it with the arrow, call this controller and this action, so we're going to have methods in here that will match those action names, and so then from here, you have access to a special variable called this.targets, that is coming from your controller, which will allow you to grab those targets and you can say

export default class extends Controller {
  log() {
    this.targets.find("name")
  }
}

that will look for anything called "Hello.name", and grab that first one, and then we can ask for the value on that, we can say console.log that out. If we refresh our page, we should be able to see this, and we should be able to say "Test 123", click "Log", and that will print that out.

That worked well, but I want to point out that this works for all events, by default, click is kind of the most common one, a lot of times we need to use that, but if you had a form here, you could actually apply this stuff to rails form, and you could have a data action submit, and you could have a call some JavaScript in your controller when the submit was attempted, and so you could check for validations then and you could cancel the "Submit" if you wanted to, you could do all that kind of stuff, and I'm going to show you how you can interact with the event when you submit something, so for example, instead we don't have a form here, instead let's create a data action on the input element, we'll say: Well, when you paste into this input element, we'll call the hello paste method, and our paste method is just going to be one of those annoying ones that says

paste (event) {
  event.preventDefault()
}

that will happen because we can receive the event as an argument to our action here, so for all of these if you ever want to interact with that, just type event here, or "e", and that will make sure that you get access to that, otherwise it just sends it anyways, but you ignore it and don't save it to a variable, so you can always add that in, and then call preventDefault if you want to intercept and cancel that from happening. From here we could just say console.log("pastes are not allowed"), and so if we grab some text here, copy it to the clipboard, and we refresh our browser, we should be able to paste that in, and it will say: "Pastes are not allowed" and it didn't actually put the text into the box either because the preventDefault stopped it from doing that. That's cool, that's how you could add this to your forms, and then go check all of those data targets, and say: Does this match? Is this filled out or not? Yes or no? Does this match the REGEX that I wanted to match or whatever, all those kind of validations you can do really easily with something like this which is really cool. Last but not least, we want to talk about state in these components, because they are very different than the state you might think of in React or Vue where you have a JSON object doing that, in Stimulus they encourage you to use html attributes to do that. For example, if you wanted to pass in a default value for the name, you would do something like data-hello-name="Chris", and here we would be able to access, we could

get name() {
  if(this.data.has("name")) {
    return this.data.get("name")
  } else {
    return "Default User"
  }
}

We could return this value on there, and so we'll either get the default name or not, and we can define an initialize method here, and this is going to allow us to say:

initialize() {
  this.nameElement.value = this.name
}

That's going to set the default value when we load our page. So loading our page we see that we get "Chris" as the default value in our log box, we can click log, but that's not going to work anymore, because we need to use this.name.element to access the getter, and then if we go and remove the default value there, we can refresh this page, and it's going to say: "Default user" instead, and log in should work now, if we type "Chris" in there, we can type "Log", or hit "Log" and that print out the value that is currently in there.

All of this is kind of designed so that your HTML is what keeps track of the state, and you're going to use that as a place that you can have so that when Turbolinks reloads the cached version of the page, the stuff will continue to work, you're not making extra AJAX requests, just to fill out the form because rails can go ahead and add that data attribute in with JSON or text or whatever values you need, and then your page can immediately be functional as soon as it gets rendered in your browser that way you're not making these extra AJAX requests that make certain widgets of your page load after the page is loaded, which gets kind of frustrating, so this takes an angle at things that's different than your JavaScript frameworks then you're used to, and I really like this for simple stuff, I'm very curious what happens as you go and build more complex things witht this, I think it will work out pretty well, but I haven't built anything super complicated with it yet, and so that is what we're going to be diving into more in the next few weeks as I learn this more and we get to see what other people are building with it. I think it will work out pretty well, and the simplicity of this goes hand in hand with the simplicity of rails, if you know how rails controllers and routes work, this all feels very similar to how those work just in JavaScript land instead, so it's really cool, the one thing I keep forgetting is these namespaces on the targets and the data attributes, I always kind of forget that they need to have the namespace of the controller in there, but so far that's not really that bad. That is it for this episode, I hope that helped you wrap your head around Stimulus and where it fits in and what it's good at and what it's not good at, and hopefully that will make it easier for you to decide if you want to use Stimulus or not in the future. If you have any questions, as always let me know in the comments below and I'll do my best to help answer those and get you a better understanding of Stimulus js. Until next episode, I will talk to you later. Peace v

As a recap, here's the complete app/javascript/packs/controllers/hello_controller.js

import { Controller } from "stimulus"


export default class extends Controller {
  initialize() {
    this.nameElement.value = this.name
  }

  log(event) {
    console.log(this.nameElement.value)
  }

  paste(event) {
    event.preventDefault()
    console.log("pastes are not allowed")
  }

  get name() {
    if (this.data.has("name")) {
      return this.data.get("name")
    } else {
      return "Default User"
    }
  }

  get nameElement() {
    return this.targets.find("name")
  }
}

Transcript written by Miguel

Discussion


Gravatar

I'm literally figuring out StimulusJS right now on an app, and I was hoping you would do a screencast on it soon and save me some time! Awesome timing :)

Gravatar

Hit me up with your questions! I'm still learning it too, but the good news is that it's pretty straightforward so there isn't _too_ much to learn.

Gravatar

I'm compiling a list of problems I'm running into as I stumble my way through it! I use a lot of CoffeeScript classes to interact with my app and avoid spaghetti code, like you show in https://gorails.com/episode....

One issue I ran into already is that it seems the `data-controller="..."` attribute cannot have underscores (_) in the controller name. For example, `app/javascript/packs/transaction_record_controller.js` with `data-controller="transaction_record"` will not work, but once you remove the underscore it works fine. I couldn't find any documentation on this, and I can't think of anything my app is doing that would cause a conflict, so I'm assuming it's the way Stimulus works.

I've been slowly converting several of my CoffeeScript classes into Stimulus controllers, and so far I've found it to be a great way to take care of stuff like adding a datepicker to an AJAX form, etc...

Gravatar

This section in the docs mentions that you can only use hyphens in the controller name in your html. It maps to either a hyphenated or underscore controller.js filename though. https://github.com/stimulus...

And yeah I think this is a nice clean way of refactoring those CS classes. You no longer have to deal with managing event listeners and can purely focus on the code that runs for each event. 👍

Gravatar

Chris, one question that would be awesome for you to cover is handling lists of elements.

For instance, I'm working on a notifications system right now (based off of some of your episodes!), and I want to have a data-controller to maintain list-level actions (like mark all as read) but also individual elements (such as mark an individual notification as read/unread, click the notification to view associated record, etc).

Stimulus has been updating their docs and they now have a section that talks about multiple data-controllers for lists, but I'm not sure what the "standard" is for high-level list actions vs individual item actions (if that makes sense).


Gravatar

Great intro! What impresses me is how basecamp remains an incubator for code concepts thereby battle testing it before releasing to the wild. While programming is a joy for many of us, it is also how we make a living. I appreciated some of DHH's comments about stimulus and I truly appreciate their pragmatic views. I also am impressed how they rethink conventions like using DOM for state, managing a monolith, and supporting the progressive web development. This looks to make a nice addition to the toolset.

Gravatar

Have a link to those comments by DHH about stimulus? Would love to hear his thinking.

Thanks!

Gravatar
Gravatar

Also check out this ruby rogues podcast with DHH: https://devchat.tv/ruby-rog...


Gravatar

Thanks for the interesting video. For a project of mine I used AngularJS (the original 1.x branch) in a similar way basecamp now proposes to use StimulusJS. What I liked about AngularJS in this context was the ability to offer form validation and to go as deep as I want it to go on demand. Right now what I would love to see in further stimulus releases would be something to handle form validation out of the box and some other convenience things every app needs.

Gravatar

I get the feeling that Basecamp won't build out those kinds of features and will keep this generic, but it is the perfect opportunity to build a library on top of stimulus to make validations easy I would imagine.


Gravatar

Gravatar

You had my curiosity, now you have my attention :)


Gravatar

I'm very excited about Stimulus and can't wait to see where it goes!


Gravatar

Interesting Video! Chris - I am curious to know how Stimulus support external resources. For instance, Datatables, google charts ...


Gravatar

How would you structure your JS controllers? would every page have its own JS controllers?

Thanks! Love your videos!

Gravatar

Basically just one controller per feature. You should never do page specific JS or CSS because that means you can't move your features to different pages on your site later on which inevitably always happens.

Gravatar

But couldn't we put our JS code in a utilities folder and import from there? But I guess that means we are separating it by feature anyway. :D

Gravatar

Yes, you can definitely do that. Maybe I'd do that for different things like "admin" area components and so on.


Gravatar

For newbies the application.js has changed so visit the official site: https://stimulusjs.org/hand...


Gravatar

Looks like if you've loaded version >=1 of Stimulus, there are a few changes in the installation. (See handbook at https://stimulusjs.org/hand...

Setup in application.js should look like:

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

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

Gravatar
import { Application } from  'stimulus'
import { definitionsFromContext } from 'stimulus/webpack-helpers'

const application = Application.start()
const controllers = require.context('./controllers', true, /\.js$/)

application.load(definitionsFromContext(controllers))
Gravatar
Any more episodes coming on Stimulus?

Gravatar
Hey Chris, FYI you have a spelling error in your code. 
  • Applicatoin instead of Application
  • stumulus instead of stimulus.
  • constrollers instead of controllers
  • $ needed in .js regex

import { Applicatoin } from 'stimulus'
import { autoload } from 'stumulus/webpack-helpers'

const application = Application.start()
const contsrollers = require.context("./controllers", true, /\.js$/ )

Even after I did this I had an error `TypeError: Object(...) is not a function`. I reviewed the stimulus installation guide and changed to:

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

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

It looks like it should do the same thing. Funnily enough I tried 

import { Application } from "stimulus"
import { autoload } from "stimulus/webpack-helpers"

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

And it had the same error (all I did was rename 'context' to 'controllers' and 'definitionsFromContext' to 'autoload'

Maybe the name 'controllers' has become reserved?

Gravatar
Awesome video Chris! started using Stimulus in a work project and it's a very nice way to keep the JS organized and reusable. Thanks!

Login or create an account to join the conversation.