Inline Save and Add Another with Rails and Hotwire

I recently wrote about how to add a “Save and Add Another” feature in your Rails app. This was based on the UX from Linear.

But another way to do this is how Todoist allows you to add new tasks.

Again, just like with the Linear’s style, this too, with Rails and Hotwire, is straightforward to do.

I assume you have an up-to-date Rails app ready and a model and controller set up that makes sense for such a feature. I’ve run rails generate scaffold Task body:text for this example.

The goal of this article is when a new task is created to:

  • append the task to the bottom of the list;
  • show the form below the list.

Set up the basics

I like to start UI’s like these with the bare-minimum: basic HTML responses without any JS.

Let’s make some tweaks to the earlier scaffoldeld files. Redirect to the index view on task creation:

# app/controllers/tasks_controller.rb

class TasksController < ApplicationController
  # …

  def create
    @task = Task.new(task_params)

    respond_to do |format|
      if @task.save
        format.html { redirect_to tasks_url }
        # …
      end
    end
  end

  # …
end

Clean up the form partial to the absolute minimum (and make it look a bit better):

# app/views/tasks/_form.html.erb
<%= form_with(model: task, id: "task_form", class: "flex items-center gap-2 w-full mt-4") do |form| %>
  <⁠%= form.label :body, class: "sr-only" %>
  <%= form.text_area :body, rows: 1, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 w-full" %>

  <%= form.submit "Add", class: "rounded-lg py-2 px-4 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
<% end %>

Clean up the task partial:

# app/views/tasks/_task.html.erb
<p id="<⁠%= dom_id task %>">
  <%= task.body %>
</p>

With all these tweaks made, it’s already looking pretty close! 😅

Adding a sprinkle of Hotwire

Let’s add the button to add the new task form next. Let’s replace the form_part with the following:

# app/views/tasks/index.html.erb

# …
<div class="w-full">
  <!--  -->
  <div id="tasks" class="min-w-full">
    <%= render @tasks %>
  </div>

    <div id="new_form">
      <⁠%= button_to "Add new task", new_task_path, method: :get, data: {turbo_stream: true}, class: "rounded-lg py-2 px-4 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
    </div>
</div>

Using data: { turbo_stream: true } makes sure it expects a turbo_stream response. Let’s create it:

# app/views/tasks/new.turbo_stream.erb
<%= turbo_stream.replace "new_form" do %>
  <⁠%= render partial: "form", locals: { task: Task.new } %>
<% end %>

Now when you click the Add new task, it will replace the #new_form element with the form-partial.

Now upon adding a new task, let’s create a turbo_stream response for the create-action and also replace the form partial, so its content is reset.

# app/views/tasks/create.turbo_stream.erb

<%= turbo_stream.append "tasks" do %>
  <⁠%= render partial: "task", locals: { task: @task } %>
<% end %>

<⁠%= turbo_stream.replace "task_form" do %>
  <%= render partial: "form", locals: { task: Task.new } %>
<% end %>

This is now replicating the Todoist example pretty close. And what’s really cool: no JavaScript written so far! 🤯 There’s one element that’s missing to make the UX better: autofocus the textarea when inserting the element. The idea for this set up is coming from Matt Swanson.

For that, a small Stimulus controller is needed.

// app/javascript/controllers/set_focus_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { selector: String }

  connect() {
    this.#setFocus()

    this.element.remove()
  }

  // private

  #setFocus() {
    if (this.hasSelectorValue) {
      document.querySelector(this.selectorValue)?.focus()
    }
  }
}

Now in the create turbo_stream response, insert this controller like so:

# app/views/tasks/create.turbo_stream.erb

# …

<%= turbo_stream.append "tasks" do %>
  <template data-controller="set-focus" data-set-focus-selector-value="#task_body"></template>
<⁠% end %>

When this turbo_stream response is injected into the dom, this controller queries the DOM for the element with the #task_body id, then sets the focus and finally removes itself from the DOM.

And that’s how you replicate a Todoist-style task creation UX.

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Last updated at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built using ViewComponent

  • Designed with Tailwind CSS

  • Enhanced with Hotwire