DEV Community

Cover image for Rails Intro::Mandalorian
fentybit
fentybit

Posted on • Updated on

Rails Intro::Mandalorian

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.

https://media1.tenor.com/images/30523f960d5d67c0ef26283f441585f5/tenor.gif?itemid=19358531

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

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

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

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

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

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

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.

Alt Text

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

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

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.

https://fastly.syfy.com/sites/syfy/files/styles/1400xauto/public/baby-yoda-carry.gif

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

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

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.

Alt Text

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

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.

Alt Text

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

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?!

https://media1.tenor.com/images/614de537ea516c274f9c95e722fbf9d5/tenor.gif?itemid=15630699

This is The Way.
The Mandalorian



fentybit | GitHub | Twitter | LinkedIn

Oldest comments (0)