Building Boardly: How we decided to try out Hotwire
Login
Technology

Building Boardly: How we decided to try out Hotwire

How we decided to try out Hotwire and why we ended up with our Boardly app.
We're a small startup currently building a blissfully simple planning platform for teams called TeamHQ.  Our product is in beta mode right now, ready for early adopters but not for everyone yet. So while we were gathering feedback, fixing bugs and improving a bit of the app, Hotwire came out. 

It looked like a promising solution to building reactive web applications without the need for such frameworks as React or Angular. We were interested and decided to take a look at it. We wanted to know if it would be a good substitute for our own mini-reactive javascript tool that implemented a similar concept to Hotwire. 

We decided to give it a try. 

Before Hotwire


One of the first decisions that we had to make when we started building TeamHQ was whether to use something like React for the front end.  We decided against investing heavily in the javascript front end for one simple reason. No time to learn React.

Instead, we decided to build a plain Rails/HTML/Javascript app that would introduce reactive behaviour via a simple push/pull model. The server pushes a message that an HTML component with a specific ID needs to be updated, the reactive state manager (150 line javascript file) then requests an HTML update to that component.

Here's a simple diagram of how it works.
Reactive Push/Pull Model


This is a very simple implementation and very robust. It uses Action Cable for communication between Rails processes and the browser.  When  needed to request an item after an update, the controller would send something like this back in a js.erb file

update.js.erb
updateAjaxComponent("news_item_3", true)

The true attributed would tell the javascript to broadcast changes via ActionCable.

Our HTML was simple as well, relying on DOM and ids to manage page section reloads.

show.html.erb
<div id="news_item_3" src="/news/3" autoload="true">
Loading news item ...
</div>

This worked well for our app. 

When Hey email service was released, people started talking about how Hey had a new version of turbolinks. This new version enabled reactive behaviour without relying on React type library.   Someone shared images of the chrome console source, a few others described how it seemed to work. We realized that their model was similar to what we had. 

Fast forward to Dec 2020.  Hotwire was released to much fanfare.  The beta version had some bugs, but it was usable. We decided to take a look at it.  

After reading through the docs and learning about Turbo and TurboStreams, we realized that this implementation was superior to our simplified version and we could benefit from switching our ReactiveStateManager to Hotwire. 

Before we could switch, we wanted to learn more and build a test app.  


Building Boardly


TeamHQ is based on boards and our unique approach to planning and organizing your work.  To truly test if Hotwire would work for TeamHQ, we decided to copy the board/tasks aspect of the app and make it with the new Rails version and Hotwire.

Here's the stack we started with:

  • Rails 6.1
  • Hotwire Rails Gem
  • Bootstrap 5-beta (gem from git)
  • SQLite DB
  • Redis 5

Since this was supposed to be an attempt at using Hotwire, we wanted to keep this process as quick as possible so we dumped WebPacker.

Developing javascript with WebPacker is not fun. It takes forever to compile the javascript after every change. Importing libraries is a pain. You could probably still find at least a dozen articles showing you all the steps necessary to properly import Bootstrap and jQuery.

It is overkill for small apps. And most importantly, there is a better way. Rails Asset pipeline for javascript came back in Rails 6.1. We rejoiced.

Here was our initial application.js. Turbo was set up via a separate <script> tag in our application.html file. 

application.js


We copied out models from the main app, made sure tests were working and got the controllers working without any Hotwire magic, other than the default Turbo setup. 

Problems started to creep up right away.

Bootstrap 5 beta Issue

First was Bootstrap.  Version 5 was in beta release and it turned out that dropdowns weren't working yet.  So instead of trying to fix it or finding a workaround, we switched back to Bootstrap 4.5 and jQuery.

Turbo, Links and Modals

We like using modals to display information, for editing models and other things. Usually, we'll open the modal by doing an Ajax call to a new action in a controller.

new.js.erb
bootstrapModal("#new_task_modal", "open", "<%= j render 'form_modal', task: @task %>")  

_form_modal.html.erb
<div id="new_task_modal" class="modal">
... modal html here ...
</div>

Currently, we have something like this for launching modals.  With Turbo and TurboStreams, this was no longer an option.

Turbo is big on replacing whole pages or parts of pages with TurboFrames and TurboStreams. So implementing modal display and launch via controller wouldn't work too well. 

Chris Oliver of GoRails suggests putting a skeleton of the modal in the body of the page with a turbo_frame_tag. Then you can use TurboStream to replace that frame tag.

While this approach works, it wasn't ideal for our situation.  So instead we did it a little differently.

First, we gave the whole page an id.

application.html.erb
<body id="master_boardly">
...
</body>

Adding an id to the body makes it available for manipulation by TurboStream. (Please note, we are just learning this new framework, so our approach might not be the best one).

To launch the modal we added data-local=true to the link for calling new action. This sent the request as turbo_stream content type, making our controller send back a new.turbo_stream.erb template. 

header.html.erb
<%= link_to "New Board", new_board_path, method: :get, data: { local: true }, class: "btn btn-outline-info btn-sm mr-4" %>

Our controller had the new action that created a blank Board -> @board.  Then we rendered a new template with a turbo_stream variant.

new.turbo_stream.erb
<%= turbo_stream.remove "new_board_form_modal" %>
<%= turbo_stream.append "master_boardly" do %>
  <%= render "boards/new_modal" %>
<% end %>

In this template, the first thing we did was remove an existing modal if there was one, done via TurboStream. Then we append the new modal partial to the body by using #master_boardly id.

_new_modal.html.erb
<div class="modal" id="new_board_form_modal" tabindex="-1" data-controller="new-board modal" data-new-board-target="newBoardModal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-body">
        <h5>Create New Board</h5>
        <%= render "boards/form_modal" %>
      </div>
    </div>
  </div>
</div>

Appending a new modal HTML to the body doesn't actually launch/show the modal.  To solve this, we made a StimulusJS controller to handle modal launches.

modal_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  connect() {
    $(this.element).modal('show')
  }
}

All this controller does is watch for modal div to be attached to the DOM and once it's connected, it just shows the modal with a call to bootstrap's modal function.

With this approach, showing modals was now easy.  However, we did notice a strange effect.  Every time we clicked on the link to launch a new modal, a FORM tag was added to the bottom of the body.
 
Click on the link repeated just kept adding form elements.


We weren't sure if it was because of the data-local="true" attribute on the link.  Chris Oliver mentioned that because Turbo now submits all links as a FORM submission, we'd have to use button_to helper on links. 

It seems like this is a bug in Turbo when it tries to submit a link by creating a FORM tag for it and forgets to clean it up after FORM submission.

We haven't found a solution to it yet.  

We have found a quick fix for this issue. It turns out that the FORM element should be cleaned up, but it isn't. So we just clean it up on turbo:submit-end event.

  $(document).on("turbo:submit-end", function(event){
    event.detail.formSubmission.formElement.remove()
  })

This is a temporary solution and we hope this bug will be fixed soon.

Continuing with modals, we needed to handle form replacement in case there were errors.  Our form partial was simple.

_form.html.erb
<%= simple_form_for @board do |f| %>
  <%= f.input :name, label: false, input_html: { placeholder: "Board Name"} %>
  <div class="mt-3">
    <%= f.input :description, label: false, input_html: { placeholder: "What is this board for?"} %>
  </div>
  <div class="d-flex justify-content-end mt-3">
    <button type="button"  data-dismiss="modal" class="btn btn-outline-secondary" name="button">Cancel</button>
    <%= f.button :submit, class: "ml-3 btn btn-primary" %>
  </div>
<% end %>

If there were form errors, all we did in our controller is render, via a TurboStream, form_modal partial that would replace the whole modal in place. 

  if @board.save

    redirect_to board_path(@board, format: :html), status: "301"
  else
    render turbo_stream: turbo_stream.replace('new_board', partial: 'form_modal')
  end

If the board had no errors, we simply redirected to the board details page.

Redirects Issue

Originally, we had been using turbo_frame_tags to replace and add modals.  But we realized this didn't work as expected.  First, when you submit a form from within a TurboFrame, any response (redirect or otherwise) will be processed within that frame. This is by design.  To break out of the frame, you have to use target="_top". 

This works well. In our case, it didn't. Since we wanted to render a form with errors within the modal, breaking out of the frame wouldn't work. Any error on the page would send the user to a new page. Not what we wanted.

Another issue happened when a user submitted the form successfully. Our controller redirected to a new page, and the redirect returned a full HTML page with board details. Turbo then tried to fit this HTML response into a turbo_frame_tag.  Not a good design.

We discussed this on Hotwire's Discuss board and Github.  The proposed solution was for Turbo to treat redirects as redirects and if the turbo_frame_tag is present replace the matching content., If the frame tag was not present it should do a full page refresh (as a redirect would). 

For now, the solution to a redirect issue is to not use turbo_frame_tags and just use turbo_stream to do replacements.  As we described above. 

Our Conclusion


Hotwire is promising, the concepts make sense and overall it is a good framework to build reactive applications. There are kinks and bugs and they will be fixed.

As for our experiment, as we got deeper into Hotwire and trying things out, our app grew into a full mini version of the planning boards that we have in TeamHQ.

We decided to continue building it and make a super simple version of our Boards for people who work on their own and just want a simple planning board, without bells and whistles.

We launched it on the weekend and the feedback and interest have been surprising. People love the idea. 

Check it out for yourself. Try Boardly App - the simplest planning board. 

We have a few more posts we want to share describing how we built Boardly and how we solved some of the Hotwire issues. Stay tuned. 

The next instalment of Building Boardly series is available here


Get Tech and Tips Thursday articles in your email.