In this post, we’ll build a simple todo app with Ruby on Rails and Turbo. We’ll implement the ability to add, edit, delete and mark as completed tasks. Our app will have one page where we’ll display a list of tasks, and we’ll update them without refreshing the page or writing any JavaScript code. We’ll use Ruby on Rails 7.0.4.2 and Ruby 3.2.1.
SET UP THE APPLICATION
Let’s get started by creating a new application. In the terminal, type the following command:
rails new todo_appOnce the command finished running, we can change directories to this folder:
cd todo_appNext, we will generate a Task model:
bin/rails g model Task name:stringThen we will apply this migration:
bin/rails db:migrate           Let’s add a validation for the name attribute in            the app/models/task.rb file:         
validates_presence_of :name           The next step is to generate a Tasks controller with an           index action:         
bin/rails g controller Tasks indexThis will allow us to display a list of tasks in our application.
           Next, we’ll set the root route to the           index action. Let’s open the           config/routes.rb and replace this line:         
get 'tasks/index'with:
root 'tasks#index'DISPLAY A LIST OF TASKS IN THE INDEX VIEW
           Let’s edit the index method in the controller to add a           @tasks instance variable:         
def index
   @tasks = Task.all
 end           Next, in the app/views/tasks/ folder, we’ll create a           _task.html.erb partial. In the terminal type:         
touch app/views/tasks/_task.html.erbWe wrap each task in a Turbo Frame. Let’s edit this partial as follows:
<%= turbo_frame_tag dom_id task do %>
   <p><%= task.name %></p>
 <% end %>
            Now, we’ll create another partial _tasks.html.erb, where           we’ll display a list of tasks. In the terminal we type:         
touch app/views/tasks/_tasks.html.erbLet’s open this partial and add this to it:
<ul id='tasks-list'>
   <% @tasks.each do |task| %>
     <li><%= render partial: 'task', locals: {task: task} %></li>
   <% end %>
 </ul>Now, we can render it in our index view:
<main>
   <h1>Tasks</h1>
   <%= render 'tasks' %>
 </main>IMPLEMENT THE ABILITY TO ADD A NEW TASK
           Let’s start by making a @task instance variable in the           index view, and assigning a new object of the           Task class to it. Inside the index method           add this:         
@task = Task.new           To be able to add a new task, we’ll need to define a           create action in our controller. But first, let’s write           the private method task_params at the bottom of our           controller:         
private
 def task_params
   params.require(:task).permit(:name)
 endThen, we add the create method:
def create
   @tasks = Task.all
   @task = Task.new(task_params)
    if @task.save
      render turbo_stream: turbo_stream.replace('tasks-list', partial: 'tasks', locals: { tasks: @tasks })
    else
      render turbo_stream: turbo_stream.replace('task-form', partial: 'tasks/form', locals: { task: @task }) 
    end 
 end           We use the turbo_stream method to generate a           Turbo Stream response.  We don’t need to reload           the page or use JavaScript code.         
           Since we retrieve all tasks from the database for every action, we can           write a private method that we’ll call           load_tasks, and then use the           before_action callback to call this method:         
def load_tasks
   @tasks = Task.all
 endWe can delete this line:
@tasks = Task.all           inside the index and create methods, and put           this at the top of our controller:         
before_action :load_tasks           We need a route to the create action. Let’s add it in the           config/routes.rb file:         
resources :tasks, only: :create           Next, we’ll create a _form.html.erb partial. In the           terminal type:         
touch app/views/tasks/_form.html.erbLet’s edit this partial to add a form:
<%= form_with(model: task, id: dom_id(task, 'form')) do |form| %>
   <%= form.text_field :name, id: dom_id(task, 'name') %>
   <%= form.submit data: { 'turbo-permanent': true } %>
 <% end %>           We will display this partial in our index view. Let’s put           it before the tasks partial:         
<%= render 'form', task: @task %>           We can now add tasks to our list. We need to add a little           JavaScript code though because we want to clear the           form input once the task is submitted.  In the           app/javascript/application.js file let’s put this code:         
document.addEventListener('turbo:submit-end', function (event) {
   if (event.target.matches('#form_task')) {
 event.target.reset();
   }
 })           With this, we reset the form. The turbo:submit-end event           fires when a form submission is complete.         
Implement the ability to mark tasks as completed
Next, we’ll implement the ability to mark tasks as completed.
           But first, we’ll define a private method set_task in our           Tasks controller:         
def set_task
   @task = Task.find(params[:id])
 end           And we’ll use the before_action callback to call this           method except for the index and           create actions. Let’s put this line at the top of our           controller:         
before_action :set_task, except: [:index, :create]           We’ll add a status attribute to the           Task model, and will use an enum for this           status. Let’s generate a migration file:         
bin/rails g migration AddStatusToTasks status:integerLet’s edit the migration file, to set a default:
class AddStatusToTasks < ActiveRecord::Migration[7.0]
   def change
     add_column :tasks, :status, :integer, default: 0
   end
 endNow, we apply this migration:
bin/rails db:migrate In the Task model, we define this enum like this:
enum status: { incomplete: 0, completed: 1 }           In the Tasks controller, we’ll define a           toggle_status action to switch between statuses:         
def toggle_status
   if @task.incomplete?
     @task.update(status: :completed)
   else
     @task.update(status: :incomplete)
   end
     render_tasks_partial
 end           Since we are repeating the same Turbo Streams response in           both create and toggle_status methods, and           we’ll use it in other methods too, we can create a private method that           we’ll call render_tasks_partial:         
def render_tasks_partial
   render turbo_stream: turbo_stream.replace('tasks-list', partial: 'tasks', locals: { tasks: @tasks })
 end           And then update our create and           toggle_status methods to replace the           Turbo Streams response code with a call to this           method:         
def create
   @task = Task.new(task_params)
   if @task.save
     render_tasks_partial
   else
     render turbo_stream: turbo_stream.replace('task-form', partial: 'tasks/form', locals: { task: @task }) 
   end 
 end
 def toggle_status
   if @task.incomplete?
     @task.update(status: :completed)
   else
     @task.update(status: :incomplete)
   end
     render_tasks_partial
 end           Let’s add a route to this toggle_status action. Let’s           modify our routes like this:         
resources :tasks, only: :create do
   member do
     put :toggle_status
   end
 endAnd in our partial, we’ll add a button to toggle the task status, after the task name:
<%= turbo_frame_tag dom_id task do %>
   <p><%= task.name %></p>
   <div class='actions'>
     <%= button_to task.incomplete? ? 'Incomplete' : 'Completed', toggle_status_task_path(task), method: :put, class: 'actions-button' %>
   </div>
 <% end %>IMPLEMENT THE ABILITY TO EDIT A TASK
           Now, we’ll implement the ability to edit the tasks. First, let’s add           an edit method to our controller:         
def edit
   render turbo_stream: turbo_stream.update("task_#{params[:id]}", partial: 'tasks/form', locals: { task: @task }) 
 endAlso, we’ll add an update action to the controller:
def update
   if @task.update(task_params)
     render turbo_stream: turbo_stream.replace("task_#{params[:id]}", partial: 'tasks/task', locals: { task: @task }) 
   else
     render turbo_stream: turbo_stream.update("task_#{params[:id]}_form", partial: 'tasks/form', locals: { task: @task }) 
   end
 end           Let’s add routes to these two actions in the           config/routes.rb file:         
resources :tasks, only: [:create, :update] do
   member do
     put :toggle_status
     get :edit
   end
 end           And in the _task.html.erb partial, we add a button to           edit the task:         
<%= turbo_frame_tag dom_id task do %>
   <p><%= task.name %></p>
   <div class="actions">
     <%= button_to task.incomplete? ? 'Incomplete' : 'Completed', toggle_status_task_path(task), method: :put, class: 'actions-button' %>
     <%= link_to 'Edit', edit_task_path(task), class: 'actions-button edit' %>
   </div>
 <% end %>IMPLEMENT THE ABILITY TO DELETE TASKS
           Finally, we will implement the ability to delete the task. First,           let’s define a destroy method in the controller:         
def destroy
   @task.destroy
   render_tasks_partial
 endLet’s add a route to it in config/routes.rb:
resources :tasks, only: [ :create, :destroy, :update] do
   member do
     get :edit
     put :toggle_status
   end
 end           And in the _task.html.erb partial, we’ll add a button to           this action:         
<%= turbo_frame_tag dom_id task do %>
   <p><%= task.name %></p>
     <div class="actions">
       <%= button_to task.incomplete? ? 'Incomplete' : 'Completed', toggle_status_task_path(task), method: :put, class:'actions-button' %>
     <%= link_to "Edit", edit_task_path(task), class: 'actions-button edit' %>
     <%= button_to 'Delete', task, method: :delete, class: 'actions-button' %>
   </div>
 <% end %>           Last, let’s add some basic styles in the           app/assets/stylesheets/application.css file:         
 main {
     margin: 30px;
   }
 
   .actions {
     display: flex;
     flex-direction: row;
   }
 
   .actions-button {
     cursor: pointer;
     margin-right: 15px;
   }
 
   .edit {
     background: #ffeb7a;
     border-radius: 5px;
     padding: 3px;
     text-decoration: none;
   }
 
   #tasks-list {
     margin-top: 60px;
     padding: 0;
   }
 
   li {
     list-style-type: none;
     box-shadow: 0px 1px 5px rgba(29, 29, 29, 0.2);
     padding-bottom: 20px;
   }CONCLUSION
           We implemented our app easily, with the help of the           turbo_stream method, which allows us to create dynamic           web apps without page reloads or complex JavaScript code.