Get on board

git clone https://github.com/jcoyne/samvera_connect_north_by.git

bundle install
yarn install

./bin/rails db:seed
./bin/rails server # http://localhost:3000/

The toggle buttons should not work

http://bit.ly/sc2019-stimulus

Frontends with Webpacker and Stimulus

Justin Coyne

Stanford Libraries

Samvera Connect - October 2019

What we're going to do

  • Walk through adding a stimulus feature
  • Talk about webpack and webpacker
  • See where we are on time
  • Take a break 3:15 - 3:30
  • Work together to add more stimulus code

Ground rules:

We're all here to learn -- ask question

Work together

if you are comfortable doing so.

We have a rails app

We want to improve the user experience

But we don't need a single page app

Logistics

How do we convert our code into something that the browser will understand?

Webpack(er)

  • Bundling
  • Transpiling (using Babel)
  • Linking in 3rd party libraries

How do we provide structure to our non-SPA front end?

Stimulus

"A modest Javascript framework for the HTML you already have"

Stimulus has 4 concepts

  • Controllers
  • Actions
  • Targets
  • Data Maps

Controllers

A controller is where stimulus code is executed

A controller is bound to a DOM element

// src/controllers/hello_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
}

Identifiers link the DOM to the Controller

Action

Actions respond to DOM events

// src/controllers/hello_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  greet() {
    console.log("Hello, Stimulus!", this.element)
  }
}

Targets

Targets Map Important Elements To Controller Properties

// src/controllers/hello_controller.js
import { Controller } from "stimulus"

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

  greet() {
    const element = this.nameTarget
    const name = element.value
    console.log(`Hello, ${name}!`)
  }
}
export default class extends Controller {
  static targets = [ "name" ]

  greet() {
    console.log(`Hello, ${this.name}!`)
  }

  get name() {
    return this.nameTarget.value
  }
}

Let's try it together

All the stuff to enter can be found at:

http://bit.ly/sc2019-stimulus
app/views/schedule/show.html.erb:29-30
<section class="day-body"
  id="day-body:<%= schedule_day.day.by_example("2006-01-02") %>"
  data-controller="toggle">

Then open the controller at:

app/javascript/controllers/toggle_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  connect() {
    console.log("The Controller is Connected")
  }
}

Reload the page

You'll see in the log:
[Webpacker] Compiling...

and the message in the Javascript console

Action

app/views/works/index.html.erb:14
Files: 

app/javascript/controllers/toggle_controller.js
toggle() {
  console.log("click")
}

An action associates an arbitrary event with a controller method

event->controller#method

When the event is emitted, Stimulus invokes the controller method

The HTML element that triggers the event must be contained by the element the controller is bound to.

Target

A target is an HTML element of interest to the controller

app/views/schedule/show.html.erb:39
<section data-target="toggle.thingToHide">

...and then register with the controller

static targets = ["thingsToHide"]

Targets make it easy to look up elements of interest

  • this.thingsToHideTarget
  • this.thingsToHideTargets
  • this.hasThingToHideTarget

Let's update our action

toggle() {
  this.thingsToHideTarget.classList.toggle("is-hidden")
}

Data Map

A Data Map allows you to associate data with a controller

app/views/schedule/show.html.erb:29-30
<section class="day-body"
  id="day-body:<%= schedule_day.day.by_example("2006-01-02") %>"
  data-controller="toggle"
  data-toggle-hidden="false">

Data maps belong to the controller

data-controllerName-key
  • get(key)
  • has(key)
  • set(key, stringValue)
  • delete(key)
If we add another target
Hide

We can tie it all together

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = ["text", "thingToHide"]

  isHidden() { return this.data.get("hidden") === "true" }

  flip() { this.data.set("hidden", this.isHidden() ? "false" : "true") }

  toggle() {
    this.flip()
    this.thingToHideTarget.classList.toggle("is-hidden", this.isHidden())
    this.textTarget.innerText = this.isHidden() ? "Show" : "Hide"
  }
}

Why Stimulus

Low overhead

Easy to follow

Remove boilerplate event binding

Predictable file structure

Easy to write small modular effects

Great at handling dynamic changes to the page

Why not Stimulus

No router / SPA

Not super-helpful if there's a lot of state

Webpack

"At its core, webpack is a static module bundler for modern JavaScript applications"

  • Entry
  • Output
  • Loaders / Plugins

Entry

A file that references other files in use

Webpack uses entry points to build a dependency graph

Output

where the bundle ends up

often a single file

Loaders

transform files

For Example

Typescript compiler

CSS loader

File loader

So what is Webpacker?

"Webpacker makes it easy to use webpack to manage JavaScript in Rails"

Useful defaults

Makes some behavior easier to override

Webpacker is the default for new Rails 6 apps

./bin/rails webpacker:install

Providing defaults for

  • React
  • Angular
  • Vue
  • Elm
  • ... and more

and support for

  • Rails helpers
  • Erb
  • asset compression
  • code splitting
  • images and fonts
  • ... and more

Important files:

package.json

/node_modules

config/webpacker.yml

config/webpack/

babel.config.js

postcss.config.js

Conventions:

Any file in application/javascript/packs is considered and entry point for a pack

Adding javascript_pack_tag or stylesheet_pack_tag adds the pack to the page

Pro tip: If you have both JS and CSS in the same pack, import them both in the same .js file, and use both javascript_pack_tag and stylesheet_pack_tag to load them

Rails automatically compiles when you hit a page in development

Or you can use

webpack-dev-server

which compiles when files change

and live reloads

Hands On

Let's build a different date filter

If no dates have been clicked, all dates show
If any dates have been clicked, only clicked dates show

What do we need?

  • Controller
  • Action for each date
  • Action for show all
  • Targets for each date body
  • Data for current state

Put the controller in the HTML

Add the targets and the starting data

Add the action

Add the show all action

Takeaways

Webpack and Webpacker are there to help you write the code the way you want to...

And deliver it to the browser as needed

Stimulus is great for small JS interactions that aren't SPA worthy

Fin.

Special thanks to Noel Rappin who first presented this workshop at RailsConf 2019