Content Loader

Deeper JavaScript integration

Add the following to your public/index.html:

<div data-controller="content-loader"
     data-content-loader-url-value="/messages.txt"
     data-content-loader-refresh-interval-value="5000"></div>

Create a new file named public/messages.txt with the following contents:

<ol>
  <li>New Message: Stimulus Launch Party</li>
  <li>Overdue: Finish Stimulus 1.0</li>
</ol>

Now create a src/controllers/content_loader_controller.js.rb file with the following contents:

class ContentLoaderController < Stimulus::Controller
  self.values = { refreshInterval: Number }

  def connect()
    load
    startRefreshing if hasRefreshIntervalValue
  end

  def disconnect()
    stopRefreshing
  end

  def load()
    fetch(urlValue).then {|response|
      response.text()
    }.then {|html|
      element.innerHTML = html
    }
  end

  def startRefreshing()
    @refreshTimer = setInterval(refreshIntervalValue) {load}
  end

  def stopRefreshing()
    clearInterval @refreshTimer if @refreshTimer
  end
end

Results

If you are running the stimulus-starter on your machine, view the results in your browser. Modify the messages.txt file and see it update in your browser within 5 seconds. View the generated content_loader_controller.js.

If you are viewing this on the Ruby2JS.com site, modifying messages.txt isn’t possible, you can open your browser’s developer tools and see the request and responses going back and forth. One thing that you will see that does change with every response is the date header. You can modify the code above to display this header:

def load()
  fetch(urlValue).then {|response|
    element.innerHTML = response.headers.get('Date')
  }
end

Commentary

First, lets get a few small things out of the way. There are two instances of if as a statement modifier in this example. There also is an instance variable (@refreshTimer). Both of these work just as you would expect.

Next, there is the self.values statement at the top of the class again, but this time only one of the two values is present. As the url is a String there is no need to add it. In general, the Ruby2JS Stimulus filter will only add to – but never replace – the definitions you provide.

More interestingly, there are two methods where Ruby blocks are used. In both cases, the blocks are converted to anonymous JavaScript functions. Within the load action, then functions are passed callbacks in the form of blocks. In the startRefresh action, the setInterval function is passed a callback. Since the setInterval function is a known function, the functions filter knows to insert the block as the first argument rather than the last.

This is just one way these two actions can be defined. Let’s explore two alternatives.

Define an Async action

In JavaScript you can use async and await to code that deals with Promises cleaner. You can do the same with Ruby2JS. Replace the load action above with the following:

  async def load()
    response = await fetch(urlValue)
    html = await response.text()
    element.innerHTML = html
  end

View the generated content_loader_controller.js. It should be exactly what you would expect to see.

Automatic binding

The above code calls setInterval with a Ruby block as this most closely matches the code in the Stimulus Handbook.

But there is a deeper story here. SetInterval accepts as a first argument a function to be executed, but you can’t simply pass this.load as that would be an unbound function meaning that when called the value of this would be wrong. There are multiple ways around this, one is to define an anonymous function using the fat arrow syntax. Another is by calling bind.

These are the gotchas that JavaScript programmers learn the hard way and have learned to deal with every day. But as it turns out, referencing a method as a property in an expression within another method in the same class is a common enough pattern that Ruby2JS handles it automatically.

Try replacing the startRefreshing method with the following:

  def startRefreshing()
    @refreshTimer = setInterval(load, refreshIntervalValue)
  end

Look at the generated content_loader_controller.js.

Note that load is referenced twice, once in the connect method and the other in the startRefreshing method, but the code generated in each case is different. In one case, it is referenced as a statement, in the other case it is referenced as an expression.

More on this on the next page.

Next: Tips & Gotchas