---
description: My simple tutorial to remember how to wire up a basic SPA CRUD.
---

# Rails 7 Turbo/Hotwire Cheatsheet

The following is based on my takeaways from this tutorial:

{% embed url="https://www.hotrails.dev/turbo-rails/turbo-frames-and-turbo-streams" %}

***



Example:  My app is simply a list with `Tasks`.  I want to create a single page app where I can:

* Create new tasks
* see the list of tasks
* delete tasks from the list
* in-line edit tasks on the list
* Click "cancel" to cancel creation or editing of a task
* mark tasks as complete or incomplete



Basic setup:

* Create a scaffold for `Tasks`, each has a title and `completed` boolean
* Change the root route to point to the Tasks index view.  This will be our one page for our SPA.



***



### A note about dom\_id

```
# If the task is persisted and its id is 1:
dom_id(@task) # => "task_1"

# If the task is a new record:
dom_id(Task.new) # => "new_task"

# Note that the dom_id can also take an optional prefix argument
# We will use this later in the tutorial
dom_id(Task.new, "prefix") # "prefix_new_quote"
```

That will be useful when giving turbo\_frame\_tags their IDs, which need to match a specific Task ID or "new\_task".  See below...



### Button to create a 'new' task

Near the top of the index view, we will have a button, "New task", and a place where we want to render the new task form when user clicks "New task".



There are 2 ways we can structure this:

#### Option A:  Replace the container of "New task" button with the form

Tasks controller index action should be structured like this:

* be sure to define @task = Task new (this makes @task = "new\_task" for the new task form).

```
def index
    @tasks = Task.all
    @task = Task.new
  end
```



Structure the index view like this.

* Wrap the new task button in `<%= turbo_frame_tag "task_form" do %>`
* The new task button simply points to `new_task_path`  &#x20;

```
<div class="">
  <%= turbo_frame_tag @task do %>
    <div class="">
      <h1 class="">Tasks</h1>

      <div>
        <%= link_to new_task_path,
                    class: "btn" do %>
          New task
        <% end %>
      </div>
    </div>
  <% end %>

  <div id="tasks" class="min-w-full space-y-4">
    <% @tasks.each do |task| %>
      <%= render task %>
    <% end %>
  </div>
</div>
```

Structure `new.html.erb` like this:

* This view must contain a `<%= turbo_frame_tag id="task_form" do %>`...

```
<div class="">
  <h1 class="">New task</h1>

  <%= turbo_frame_tag @task do %>
    <%= render "form", task: @task %>
  <% end %>
</div>
```

When "New task" is clicked, its container (the turbo\_frame\_tag) will be replaced by the turbo\_frame\_tag found on the new.html.erb view (the form).



#### Option B:  Target a place elsewhere on the page to render the form

Structure index.html.erb like this:

* The "New Task" button has a data attribute `data: { turbo_frame: "task_form" }` which targets that turbo\_frame\_tag elsewhere on this page.
* We place `<%= turbo_frame_tag "task_form" %>` in the spot where we want to render the task form.

```
<div class="">
  <div class="">
  <h1 class="">Tasks</h1>

    <div>
      <%= link_to new_task_path,
                  class: "btn",
                  data: { turbo_frame: "new_task" } do %>
        New task
      <% end %>
    </div>
  </div>
  
  <%= turbo_frame_tag @task %>

  <div id="tasks" class="min-w-full space-y-4">
    <% @tasks.each do |task| %>
      <%= render task %>
    <% end %>
  </div>
</div>
```

The new.html.erb is the same as above.

Now when user clicks "New task", it will render the new task form _below_ where the button is.  The button will remain visible.

***



### Regular links that reside inside turbo\_frames (link to show task)

Let's say we want to link the task title to the task show view.

Since the show view doesn't contain a turbo\_frame\_tag that matches the containing turbo\_frame\_tag of each task, we need to target "\_top" for the link that points to the show view. &#x20;

In `_task.html.erb` I implemented the link that points the task title to the task show view like this:

```
<%= link_to task_path(task),
            data: { turbo_frame: "_top" } do %>
  <%= task.title %>
<% end %>
```



***



### Delete a task

We'll use turbo\_streams for this.

This is the delete button inside each `_task.html.erb`:

```
<%= button_to task,
              method: :delete,
              class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" do %>
    <span>
       Delete
    </span>
  <% end %>
```

The destroy action in the tasks controller should look like this:

```
def destroy
  @task.destroy!

  respond_to do |format|
    format.turbo_stream
  end
end
```

Create the file `destroy.turbo_stream.erb` with this content:

```
<%= turbo_stream.remove @task %>
```

Now when you click the "Delete" button, task is removed from the DOM and deleted.



***



### Creating a task

The create action in tasks controller should be this:

```
def create
  @task = Task.new(task_params)

  respond_to do |format|
    if @task.save
      format.turbo_stream
    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end
```

Create file `create.turbo_stream.erb` with this contents, which does this following:

* Prepends the new task inside the dom element with ID "tasks"
* Replaces the "new\_task" form with the tasks form partial, and instantiates a new task by passing local variable:  Task.new

```
<%= turbo_stream.prepend "tasks", @task %>
<%= turbo_stream.replace "new_task", partial: "tasks/form", locals: { task: Task.new } %>
```

Alternatively, we can use this block syntax to render the new task inside the "tasks" container:

```
<%= turbo_stream.prepend "tasks" do %>
  <%= render "tasks/task", task: @task %>
<% end %>

<%= turbo_stream.replace "new_task" do %>
  <%= render "tasks/form", task: Task.new %>
<% end %>
```



***



### Adding a "Cancel" button

Inside the `_form.html.erb` partial, simply add the cancel link like this:

```
<%= link_to "Cancel", tasks_path, class: "" %>
```

* That points to the tasks index view (`index.html.erb)`
* When user is in the new task form, then it will replace the "new\_task" turbo\_stream\_tag with the contents of the "new\_task" turbo\_stream\_tag found in `index.html.erb`, which has no content.  So it (correctly) replaces it with nothing.
* When user is in the editing task form on any specific task, it will target that task's turbo\_stream\_tag (`task_id`) and replace that with the contents of the the same turbo\_stream\_tag (`task_id`) found on `index.html.erb` which is the content of this task.
