It’s been a while since I wrote the last post here, so I decided to write something fun and maybe a good sample for anyone that is learning rails or is a little outdated.
The idea here is to write a simple CRUD from scratch, I’ll use a Grid layout to keep everything in the same page, the app will be a Single Page application, similar to those written with heavy JavaScript frameworks, but we’ll not touch a line of JavaScript, out code will be only Ruby and ERB, using the power of Ruby on Rails, and the new standard Hotwire, to be more specific in this example, we’ll be heavily using TurboFrame that is a part of Hotwire.
The full app creation is in the video bellow, but if you don’t want video you can simply skip it an read the step by step.
We’ll start creating a new rails application:
rails new samplespa
In that application we’ll edit the file config/routes.rb, and make the content match:
Rails.application.routes.draw do resources :people # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check # Defines the root path route ("/") root "people#index" end
The changes are the addition of “resources :people” that will be the only controller in our application and making the root of the app “people#index”.
Now we’ll create our model with the command:
rails g model person name:string email:string description:rich_text
I’m using rich_text as the description type because that will allow us to use ActionText to edit that field, but we’ll need to activate it in the application first, for that we’ll run the command:
rails g action_text:install
That command will create two migrations and configure the application to use ActionText for rich text editing.
We’ll also edit our create_people migration to make the fields not null, by adding “null:false” as options, like bellow:
class CreatePeople < ActiveRecord::Migration[7.1] def change create_table :people do |t| t.string :name, null: false t.string :email, null: false t.timestamps end end end
Now we are ready to migrate our database with the command:
rails db:migrate
Next we’ll edit the person model in the file app/models/person.rb to add some validation like bellow:
class Person < ApplicationRecord has_rich_text :description validates :name, :email, :description, presence: true end
the has_rich_text tells the model to store the description field as ActionText in the proper tables.
Now we create our controller, we can use the rails generator or simply create the file people_controller.rb in the folder app/controllers, the initial content will be this:
class PeopleController < ApplicationController before_action :load_everyone def index if @people.any? @person = @people.first end end private def load_everyone @people = Person.order(id: :desc) end end
The idea is to have the list of people loaded for every method, that isn’t a problem because Rails is smart and will only actually run the query when we fetch data from @people
Now we create our first and only HTML view, it’ll be in the folder app/views/people (create the folder manually if you didn’t use the generator for the controller)
The file will be called index.html.erb and the content is really simple, check bellow:
<div class="people_container"> <div class="people_list"> <%= render 'people_list' %> </div> <div class="person_detail"> <%= render 'person_detail' %> </div> </div>
It has a container div, and two section divs for layout, in those we render the default content for the page.
Now lets take a look at our default CSS to layout this amazing app.
body { height: 100vh; } .people_container { display: grid; grid-template-columns: 200px auto; height: 100%; width: 100%; } .people_list { grid-column: 1; height: 100%;} .person_detail { grid-column: 2; height: 100%; }
It is very simple using grid layout in the container with two columns, one with 200px and the other with the rest, and we set the position of our two inner divs in the grid, also making sure the body has the full height, so height:100% works (I do that every time now, to make sure I don’t forget due to bad experiences in the past trying to fill the vertical portion in the screen)
Now we need to create the partials used in the main view, the first will be the list, create the file app/views/people/_people_list.html.erb with the following content:
<%= turbo_frame_tag 'people_list' do %> <ul> <% @people.each do |person| %> <li> <%= link_to person.name, person, data: {turbo_stream: true, turbo_method: 'get'} %> </li> <% end %> <li> <%= link_to 'Create Person Record', new_person_path, data: {turbo_stream: true, turbo_method: 'get'} %> </li> </ul> <% end %>
Pay attention to the “data: {turbo_stream: true, turbo_method: ‘get’}”, it’ll add to the links generated the properties data-turbo-stream=”true” and data-turbo-method=”get”, the first will activate turbo_stream for the link, it is usually only active for forms, and the later will activate turbo_stream for the method GET, because it is usually only for POST/PATCH, in the video the first try was wrong, I used torbo_frame instead of turbo_stream.
Other important thing is the turbo_frame_tag in the view, it defines areas where turbo frame can easily change content in the page.
Now lets create the file app/view/people/_person_detail.html.erb with the content:
<%= turbo_frame_tag 'person_detail' do %> <div> <div> <b>Name:</b> <%= @person&.name %> </div> <div> <b>Email:</b> <%= @person&.email %> </div> <div> <b>Description:</b> <%= @person&.description %> </div> <div> <% if @person.present? %> <%= link_to 'Edit', edit_person_path(@person), data: {turbo_stream: true, turbo_method: 'get'} %> <% end %> </div> </div> <% end %>
This is a simple and ugly details view that will render the person details, it uses safe navigation for the person because it is possible that with an empty people list, the person will be nil and it’d crash our application.
Now we need to make the links in both the people_list, and person_detail partials work, meaning we need to add to our controller the methods “new”, “show” and edit, just to start.
So open the controller app/controllers/people_controller.rb and add these 3 methods:
def show @person = Person.find params[:id] respond_to do |format| format.turbo_stream end end def new @person = Person.new respond_to do |format| format.turbo_stream end end def edit @person = Person.find params[:id] respond_to do |format| format.turbo_stream do render :new end end end
There are a few important things here, in all methods we have a respond_to and the format is turbo_stream, that will require a turbo_stream view, we’ll need at least two for the application, the show and new views, the edit method uses the new view since it is the same idea of showing a form.
To make it work, lets create a file named app/views/people/show.turbo_stream.erb with the following content:
<%= turbo_stream.replace 'person_detail', partial: 'person_detail' %> <%= turbo_stream.replace 'people_list', partial: 'people_list' %>
The view is very simple and uses a helper from ruby to make our lives easier, we are replacing the contents of the previously defined turbo_frame_tag areas, with the referred partials, and we are redrawing the list so that we can use the same show view for update and create actions too, so if we change the person’s name or add a new person, the list will also be updated.
And the other file app/views/people/new.turbo_stream.erb with the content:
<%= turbo_stream.replace 'person_detail', partial: 'person_form' %>
The idea of this view is to render a form, so we need to create the form partial too, lets create the file app/views/people/_person_form.html.erb with the content:
<%= turbo_frame_tag 'person_detail' do %> <%= form_for @person do |f|%> <div> <div> <%= f.label :name %> <%= f.text_field :name%> </div> <div> <%= f.label :email %> <%= f.text_field :email%> </div> <div> <%= f.label :description %> <%= f.rich_text_area :description %> </div> <div> <%= f.submit %> </div> </div> <% end %> <% end %>
It is a simple form using rails helpers, and the rich_text_area field type, it also goes into the same person_detail turbo frame area, to make our code cleaner.
And the last things needed to make our code work are the create and update methods in our controller, the code is quite simple, lets check bellow:
def create @person = Person.create params.require(:person).permit(:name, :email, :description) respond_to do |format| format.turbo_stream do if @person.valid? render :show else render :new end end end end def update @person = Person.find(params[:id]) @person.update params.require(:person).permit(:name, :email, :description) respond_to do |format| format.turbo_stream do if @person.valid? render :show else render :new end end end end
They are standard methods, without good validation and accepting pretty much anything the client sends, rails will handle escaping the SQL to make our app more secure, and we are using safe parameters, through the methods require and permit, to make sure we only pass the existing parameters to the Person model, and the main difference in the format.turbo_stream block is that if the operation failed, the form is rendered again (and we could add code to show the errors), and show the person details otherwise.
With that we have the full SPA working, without writing a single line of Javascript, if you want to check the full code it is on Github you can just checkout, run the app and change it to study.
To make it les ugly, but still pretty bad, I added Pico CSS to the application.html.erb file, in the header section, if you want to do that add this line:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" />
And if you do that, you’ll also need to update the basic_layout.css file to make the rich text editor buttons visible again with this:
.trix-button { background-color: white !important; }
Please comment bellow if you liked this quick tutorial, and please suggest other subjects, if there aren’t any suggestions I’ll probably write about how to do pop up dialogs with Hotwire/Stimulus in the next post.