StimulusJS, born at Basecamp, was officially released around a year ago - and I kind-of ignored it’s existence as I simply didn’t need it anywhere (I worked on React-based project for a while). But right now I’m working with an app that relies heavily on backend-rendering and Rails itself - in such scenario dropping whole ecosystem of any javascript framework with client-side rendering makes no sense business-wise. But gluing random js snippets can get you far until a some point.

I think the biggest advantage of stimulus is that it gives you some sane structure. It integrates nicely with webpack/er and introduces idea of controllers. So let’s take a practical example what we can do with it.

PhotoSwipe is pretty neat vanilla-js gallery library. But it definitely requires some time to setup, and docs to be honest looks pretty intimidating. So let’s break it down into most simple example, so we can quickly see some results.

Add required HTML markup

Note: I assume you already added photoswipe, its styles and you have stimulusjs ready to go.

We need to explicitly add required markup somewhere in our document, so let’s slap this partial just before closing </body> tag. You will notice we can already incorporate I18n for translating captions/navigation - which is actually pretty great as you can stick with ruby/rails for that.

<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
  <div class="pswp__bg"></div>
  <div class="pswp__scroll-wrap">
    <div class="pswp__container">
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
    </div>
    <div class="pswp__ui pswp__ui--hidden">
      <div class="pswp__top-bar">
        <div class="pswp__counter"></div>
        <!-- You can replace those t() calls with static text for the sake of example -->
        <button class="pswp__button pswp__button--close" title="<%= t('gallery.close') %>"></button>
        <button class="pswp__button pswp__button--share" title="<%= t('gallery.share') %>"></button>
        <button class="pswp__button pswp__button--fs" title="<%= t('gallery.fullscreen') %>"></button>
        <button class="pswp__button pswp__button--zoom" title="<%= t('gallery.zoom') %>"></button>
        <div class="pswp__preloader">
          <div class="pswp__preloader__icn">
            <div class="pswp__preloader__cut">
              <div class="pswp__preloader__donut"></div>
            </div>
          </div>
        </div>
      </div>

      <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
        <div class="pswp__share-tooltip"></div>
      </div>

      <button class="pswp__button pswp__button--arrow--left" title="<%= t('gallery.previous') %>">
      </button>

      <button class="pswp__button pswp__button--arrow--right" title="<%= t('gallery.next') %>">
      </button>

      <div class="pswp__caption">
        <div class="pswp__caption__center"></div>
      </div>
    </div>
  </div>
</div>

I think the most common scenario is that you already render some markup on the backend and you want to initialize gallery based on that, so at this point we will utilize stimulus for that. Our simple markup might look something like that:

<!-- we point this element to gallery controller -->
<div data-controller="gallery">
  <!-- and within the gallery we want to render our <image> collection/record/whatever -->
  <!-- let's connect click action <a> to onImageClick method in our controller -->
  <!-- and specify target that we can nicely access in controller -->
  <%= link_to image.big_file_destination, title: image.title, data: { action: 'gallery#onImageClick', target: 'gallery.picture' } do %>
    <%= image_tag image.small_thumb %>
  <% end %>
</div>
// gallery_controller.js
import {Controller} from 'stimulus'
import * as PhotoSwipe from 'photoswipe'
import * as PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default'

export default class extends Controller {
  static targets = ['picture']

  onImageClick(event) {
    event.preventDefault()

    // as our gallery markup lives outside of our controller
    // unfortunately we need to query for it, for the simplicity of example
    // let's assume we have single gallery controller in the app and we can call
    // query selector directly by it's class and we don't need to extract it into
    // configurable data-attribute
    const galleryWrapper = document.querySelector('.pswp')

    var options = {
      // we don't want browser history for or example for the sake of simplicity
      history: false,
      // and I'm assuming we have unique links in each gallery
      index: this.items.findIndex(item => item.src === event.currentTarget.getAttribute('href'))
    }

    var gallery = new PhotoSwipe(galleryWrapper, PhotoSwipeUI_Default, this.items, options)

    // PhotoSwipe requires width and height do be declared up-front
    // let's work around that limitation, references:
    // https://github.com/dimsemenov/PhotoSwipe/issues/741#issuecomment-430725838
    gallery.listen('beforeChange', function() {
      const src = gallery.currItem.src

      const image = new Image()
      image.src = src

      image.onload = () => {
        gallery.currItem.w = image.width
        gallery.currItem.h = image.height

        gallery.updateSize()
      }
    })

    gallery.init()
  }

  get items() {
    return this.pictureTargets.map(function(item) {
      return {
        src: item.getAttribute('href'),
        title: item.getAttribute('title'),
        w: 0,
        h: 0
      }
    })
  }
}

So why this is better then simply dropping some js code on DOMContentLoaded somewhere within your app? As said before - it gives you nice structure, our gallery logic is nicely encapsulated within javascript controller, we had to add minimal amount of extra html markup to our code. Let’s say we would like to handle async removal of image elements - combining stimulus with rails ujs events (binding action like ajax:success->gallery#imageRemoved) is pretty powerful and effort-less.

I have been using stimulus for over 2 months now and if you’re not planning on doing any intense client-side rendering / very dynamic UI I think it’s pretty solid tiny framework that is worth checking out.