DEV Community

Cover image for Inline Save and Add Another with Rails and Hotwire
Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com

Inline Save and Add Another with Rails and Hotwire

This article was originally published on Rails Designer


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.

Image description

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

Image description

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

I assume you have an up-to-date Rails 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

  • add the task to the bottom of the list;
  • show the form at the bottom of 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 scaffold-generated files. Redirect to the index url 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
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

Clean up the task partial:

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

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

Image description

Rails Designer is the first professionally-designed UI Components Library for Ruby on Rails 🧑‍🎨🎨

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>
Enter fullscreen mode Exit fullscreen mode

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

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

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

Now upon adding a new task, let's create a turbo_stream response for the create-action. Let's 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 %>
Enter fullscreen mode Exit fullscreen mode

Image description

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()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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

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

Top comments (0)