Happy belated Thanksgiving ๐ฆ and Merry almost Christmas ๐, fellow devs!
I still can not believe that I am officially on Module 3! So glad that I passed my Sinatra x Street Fighter project assessment, and enjoyed some downtime over Thanksgiving weekend.
The past few weeks of my first exposure to Rails just blew my mind... David H. Hansson created this magic of Rails, eliminating a bunch of repetitive Sinatra configuration. Ruby on Rails is a web framework with tremendous open source contributions, and provides developers building applications with strong base frame functionality. Rails' architecture still relies upon the MVC (Model-View-Controller) paradigm. The separation of concerns governs the overall Rails file directory structure. Once the command rails new <application_name>
is prompted, new directories emerge including app
, config
, db
, lib
, Gemfile
and so on. You may review each directory and its functionality here. rails s
is a basic command line in order to start the Rails server, while rails c
provides Rails console session (similar to rake console
). One main characteristic from Rails I have learned thus far would be its convention over configuration. This pattern is inherent in the Rails routing system, file naming conventions and/or data flow structure.
My case study focuses mainly on CRUD (Create, Read, Update and Delete) Rails implementation, and it will be in reference to one of my favorite shows, The Mandalorian. "This is The Way." Season 2 is extraordinary! Kudos to Jon Favreau and our beloved Grogu.
Rails Generators
In Sinatra, we manually build many standard features. Rails, on the other hand, provides a more efficient way when building core application functionality. This exercise alone prevents spelling and syntax errors following RESTful (Representational State Transfer) naming patterns. I adore the rails generate
(or rails g
) command line. It automatically populates our basic Rails app framework. There are a few Rails generators including migration, model, controller, resource and scaffold. Each generator is capable of creating a large number of files and codes. This case study will utilize my favorite one, resource generator. It creates some of the basic core functionality without code overflow. Let's create our first resource
, Mandalorian, on the terminal.
> rails g resource Mandalorian name:string spaceship:string companion:string --no-test-framework
The --no-test-framework flag
is being utilized as my case study will not dive in to any testing frameworks. The above command line gives us the following list:
> invoke active_record
> create db/migrate/20201201211632_create_mandalorians.rb
> create app/models/mandalorian.rb
> invoke controller
> create app/controllers/mandalorians_controller.rb
> invoke erb
> create app/views/mandalorians
> invoke helper
> create app/helpers/mandalorians_helper.rb
> invoke assets
> invoke scss
> create app/assets/stylesheets/mandalorians.scss
> invoke resource_route
> route resources :mandalorians
Isn't Rails magic?!
Let's take a peek at some of these files.
# db/migrate/20201201211632_create_mandalorians.rb
class CreateMandalorians < ActiveRecord::Migration[6.0]
def change
create_table :mandalorians do |t|
t.string :name
t.string :spaceship
t.string :companion
t.timestamps
end
end
end
We have a new database table mandalorians
created under folder db/migrate
along with the assigned string attributes, spaceship
and companion
.
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
# app/models/mandalorian.rb
class Mandalorian < ApplicationRecord
end
# app/controllers/mandalorians_controller.rb
class MandaloriansController < ApplicationController
end
Mandalorian
model directly inherits the Active Record values from ApplicationRecord
. The MandaloriansController
currently does not have any single actions. Resource generator allows flexibility when adding necessary features.
# config/routes.rb
Rails.application.routes.draw do
resources :mandalorians
end
A full resources
call currently resides in the config/routes.rb
file. resources :mandalorians
embodies the conventional RESTful routes in order to perform CRUD functions: index
, new
, create
, show
, edit
, update
and destroy
. These routes will be adjusted as we progress on this case study.
I will create our second resource
, Armor in order to establish basic object relationships, has_many
and belongs_to
. Similar Rails file structure will be created upon instantiation.
> rails g resource Armor name:string description:string --no-test-framework
Models
The models are still inheriting from the ActiveRecord::Base
class. Active Record is a strong tool when implementing logic. It provides ORM (Object Relational Mapping) meta-programming methods built into the models entity and associations. Model files may contain object relationships, validations, callbacks, custom scopes and/or others.
Active Record associations come into play especially when creating more complex data relationships. I would have to adjust both models, Mandalorian
and Armor
, along with the database migration. In the universe of my case study, let's assume that a mandalorian has_many
armors, and each armor belongs_to
a mandalorian. We understand the armors
table should include a foreign key
column, a reference to a primary key of the associated mandalorian.
# app/models/mandalorian.rb
class Mandalorian < ApplicationRecord
has_many :armors
validates :name, presence: true, uniqueness: true
end
# app/models/armor.rb
class Armor < ApplicationRecord
belongs_to :mandalorian
validates :description, length: { maximum: 50 }
end
# db/schema.rb
ActiveRecord::Schema.define(version: 2020_12_01_221427) do
create_table "mandalorians", force: :cascade do |t|
t.string "name"
t.string "spaceship"
t.string "companion"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "armors", force: :cascade do |t|
t.string "name"
t.string "description"
t.integer "mandalorian_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
end
Active Record validations are useful in protecting the database from invalid data inputs. validates
method takes two arguments, :name
of the attribute requiring validation, and a hash of options to detail out how the validation works. { presence: true }
prevents the :name
attribute from being empty. { uniqueness: true }
does not allow identical :name
data input.
It is always a good practice to test our models post migration (or, rails db:migrate
). Let's use my second favorite Rails console command line, rails c
.
001 > mando = Mandalorian.create(name: "Din Djarin", spaceship: "Razor Crest", companion: "Grogu")
=> #<Mandalorian id: 1, name: "Din Djarin", spaceship: "Razor Crest", companion: "Grogu", created_at: "2020-12-02 01:11:33.083593", updated_at: "2020-12-02 01:11:33.083593">
002 > flamethrowers = mando.armors.build(name: "Flamethrowers", description: "Fire streams or bursts of fire")
=> #<Armor id: nil, name: "Flamethrowers", description: "Fire streams or bursts of fire", mandalorian_id: 1, created_at: nil, updated_at: nil>
003 > flamethrowers.save
=> [["name", "Flamethrowers"], ["description", "Fire streams or bursts of fire"], ["mandalorian_id", 1], ["created_at", "2020-12-02 01:20:57.923623"], ["updated_at", "2020-12-02 01:20:57.923623"]]
I am able to create
mando from Mandalorian class, with its spaceship and companion attributes. build
method is similar to new
method. mando
obviously has many armors
, and build
method is one of the association macros. Once flamethrowers
saved, Active Record automatically assigns it to mando
, or equivalently, mandalorian_id = 1
. Terrific! Our object relationships work.
There are a lot of more class methods in Rails application aside from my rudimentary case study such as has_many :through
, has_one
and many more.
Controllers
Controllers essentially connect the models, views and routes. They inherit a number of methods built into the Rails controller system through ApplicationController
.
Assuming following conventional mappings of full resources
, the MandaloriansController
has HTTP verbs, methods, paths and descriptions as follows:
HTTP verb | Method | Path | Description |
---|---|---|---|
GET |
index | /mandalorians | show all mandalorians |
POST |
create | /mandalorians | create a new mandalorian |
GET |
new | /mandalorians/new | render the form for new creation |
GET |
edit | /mandalorians/:id/edit | render the form for editing |
GET |
show | /mandalorians/:id | show a single mandalorian |
PATCH |
update | /mandalorians/:id | update a new mandalorian |
DELETE |
destroy | /mandalorians/:id | delete a mandalorian |
The ArmorsController
has similar mappings as above.
These controller actions (or methods) will perform CRUD functions and render views to the end user. The /:id
on the url paths accepts params[:id]
passed to the route. This dynamic routing system
allows our controller methods to receive params input.
# config/routes.rb
Rails.application.routes.draw do
resources :mandalorians, only: [:new, :show, :edit]
# get '/mandalorians', to: 'mandalorians#new'
# post '/mandalorians', to: 'mandalorians#create'
# get '/mandalorians/:id', to: 'mandalorians#show'
# get '/mandalorians/:id/edit', to: 'mandalorians#edit'
# patch '/mandalorians/:id', to: 'mandalorians#update'
resources :armors
end
In Sinatra application, the actions' routes reside in the controller. Rails assigns routes in the config/routes.rb
by default, and elaborate the actions as methods in the controllers' files. resources :mandalorians, only: [:new, :show, :edit]
strictly only allows new, show and edit routing system. Note: I revised previously full path resources
down to three methods
. On the other hand, resources :armors
provides a full set of routing systems; index
, new
, create
, show
, edit
, update
and destroy
.
Since validations were implemented in the models, the controllers require slight modifications to the create
and update
actions. Ideally if validations were to fail, the form needs to be re-rendered. For example, the :new
form params input fails the Active Record validation. :new
template would have to be re-rendered. Similar to the :edit
form.
# app/controllers/mandalorians_controller.rb
class MandaloriansController < ApplicationController
before_action :set_mandalorian, only: [:show, :edit, :update]
def new
@mandalorian = Mandalorian.new
end
def create
@mandalorian = Mandalorian.new(mandalorian_params)
if @mandalorian.save
redirect_to @mandalorian
else
render :new
end
end
def show
end
def edit
end
def update
if @mandalorian.update(mandalorian_params)
redirect_to @mandalorian
else
render :edit
end
end
private
def set_mandalorian
@mandalorian = Mandalorian.find(params[:id])
end
def mandalorian_params
params.require(:mandalorian).permit(:name, :spaceship, :companion)
end
end
before_action :set_mandalorian
helps in keeping our code DRY (Don't Repeat Yourself). It triggers the set_mandalorian
function at the instantiation of show
, edit
and update
actions. Each action requires an object instance of Mandalorian class through .find(params[:id])
.
URL or Route Helpers are another level of abstraction in Rails in order to avoid hard-coded paths. You can implement these route helpers on Controllers and Views files, but not in Models. It also provides more assistance to readability. For example, "mandalorian_path(@mandalorian)"
would be the route helper version of "/mandalorians/#{@mandalorian.id}"
. ActionView
, a sub-gem of Rails, helps us in providing these numerous helper methods. In fact, let's implement another level of abstraction. @mandalorian
is applicable and a more succinct version of "mandalorian_path(@mandalorian)"
.
The goal of strong params is to whitelist the parameters received. In the mandalorian_params
function, the require
method must require a key called :mandalorian
. The permit
method allows more flexibility in key attributes, and is especially useful when assigning mass attributes.
On a side note, I found raise params.inspect
as a great way to print params
when working in between both controllers and views.
Views
View files should have the least amount of logic, and their solid purpose is to render views for the end user. In this exercise, we will only focus on implicit rendering following Rails convention. My application instinctively seeks out view files carrying the same name as the controller actions. Each action from the controller corresponds only to its respective view. For example, new
action only communicates with new.html.erb
. Any instance variables
from new
action only transcribes to new.html.erb
.
Rails forms allow the end user to submit data into form fields. I will directly implement the forms using Rails Route Helpers in lieu of plain HTML format, and form_for
in lieu of form_tag
. form_for yields FormBuilder object, and is full of convenient features. It is best implemented when forms are directly connected with the models, in performing CRUD functions. However, it is also crucial to understand the rudimentary application of form_tag
and its helper methods.
form_authenticity_token
helper is a part of Rails to combat CSRF (Cross-Site Request Forgery). CSRF general flow is one site implementing a request to another site without end user's consent. Fun fact, this hidden_field_tag
is a redundancy as Rails forms by default generates a required authenticity token. It reminds me of Sinatra manual implementation of assigning session[:user_id] = @user.id
.
# app/views/mandalorians/_form.html.erb
<%= form_for @armor do |f| %>
<% if @armor.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(@armor.errors.count, 'error') %>
prohibited this armor from being saved:
</h2>
<ul>
<% @armor.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<%= f.label "Name:"%>
<%= f.text_field :name %><br>
<%= f.label "Description:" %>
<%= f.text_field :description %><br>
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
<%= f.submit %>
<% end %>
# app/views/mandalorians/new.html.erb
<h3>New Form</h3>
<%= render 'form' %>
<%= link_to 'Back', armors_path %>
# app/views/mandalorians/edit.html.erb
<h3>Edit Form</h3>
<%= render 'form' %>
<%= link_to 'Show', @armor %>
<%= link_to 'Back', armors_path %>
Since both :new
and :edit
are almost identical, I created _form.html.erb
where form_for
codes reside. new.html.erb
and edit.html.erb
render files to retrieve codes from _form.html.erb
. form_for
automatically sets up the path where the form will be sent.
errors.full_messages
is optional, but displaying validation errors in views has proved to enhance user experience. Being able to notify users of input errors creates stability in the database. It is most prudent for views to display errors to the user. When the form (either :new
or :edit
) is re-rendered, pre-filling forms with existing user inputs promotes better user experience. None of us wants the hassle of re-typing or re-filling pre-populated forms. form_for
automatically pre-populates form reload with pre-existing values of @mandalorian
data.
In the above example, the view renders an edit form and once reloaded, the model executes an error message that Name has already been taken
from the Active Record validation, validates :name, uniqueness: true
. form_for
realizes that the current @mandalorian
is not a new instance of the Mandalorian class.
I would hate to delete any of our bounty hunters, let's try to implement the delete
request on our Armor model. HTML5 forms officially do not support delete
and patch
methods. We place the delete
request in the armors_controller
, and render the delete
button on show.html.erb
. Once deleted, the end user will be routed to armors index view.
# app/controllers/armors_controller.rb
def destroy
@armor = Armor.find(params[:id])
@armor.destroy
redirect_to armors_path
end
# app/views/armors/show.html.erb
<p>Name: <%= @armor.name %></p>
<p>Description: <%= @armor.description %></p>
<p>Mandalorian: <%= @armor.mandalorian.name %></p>
<%= button_to 'Delete', @armor, method: :delete %>
button_to is a better version of link_to
when sending a delete
request.
In the previous Rails console, we assured our object relationships work. Similar model associations can be implemented in the view files. Since each armor belongs_to
a mandalorian, we should be able to call out @armor.mandalorian.name
method. I repeat. Isn't Rails magic?!
This is The Way.
The Mandalorian
fentybit | GitHub | Twitter | LinkedIn
Top comments (0)