- ✔ Introduction to Ruby on the TryRuby app.
- Culttt Ruby series:
- ✔ Getting started with Ruby
- ✔ Working with strings in Ruby
- ✔ Working with Arrays in Ruby
- ✔ Working with Hashes in Ruby
- ✔ Working with Structs in Ruby
- ✔ What are Symbols in Ruby?
- ✔ Understanding and using blocks in Ruby
- ✔ What are Ruby Procs? && Blocks, Procs, and Enumerable && Shorthand syntax for each, map, reduce & more
- ✔ What are Lambdas in Ruby? (to clear it out: diff between proc & lambda)
- ✔ Working with Ruby Methods
- ✔ Writing Ruby Classes
- ✔ The difference between Public, Protected and Private methods in Ruby
- ✔ Understanding Class Methods verses Instance Methods in Ruby
- ✔ What is Monkey Patching in Ruby?
- ✔ Understanding Inheritance in Ruby
- ✔ Creating and using Modules in Ruby
- Working with Mixins in Ruby
- Using Ruby Exceptions
- Ruby Style Guid
- Ruby Cheatsheet
Rails follow the MVC pattern.
- Client sends a request to a web server (one that is running a rails app),
- within the rails app there is a router that checks what the client is requesting, then delegates that request to the appropriate controller,
- the controller interacts with a model, which in return interacts with a database to get some data and sends it back to the controller
- the controller then fills the view with that data (if there is any, and if that's how the app works) and renders the view
- Controller name must be snake_case e.g name_controller.rb (in
controllers/folder) - For the controller, there has to be a folder that has the same name as the controller e.g
example/underviews/folder, that's if views are to be created - For every view there needs to be an action defined (in the controller) and a view with the same name as the action in
views/example/e.g. If the action name ishome(actions are procedures/functions) then the view should be namedhome.html.erb(insideviews/example/)
Note: .erb stands for embeded ruby
Rails Migration allows you to use Ruby to define changes to your database schema, making it possible to use a version control system to keep things synchronized with the actual code.
Read More about migrations:
- Tutorials Point: Ruby on Rails - Migrations
- Ruby on Rails Guides - Migrations
- Ruby on Rails Guides - Active Record Migrations
If we are creating a Todos app and we want to create a Todos table, we need to make:
- Controller name -
todos_controller.rbincontrollers/folder - Table name -
todostable, plural since it contains lots of tables (multiple words table name should be snake_case) - Model name -
todo.rbinmodels/, with a class definitionTodo, singular since the model describes one todo
You can read more about it in Convention over Configuration in Active Record.
To do the above, we first use a generator:
rails generate migration create_todosThis will generate a migration file (inside db/migrate) with a timestamp YYYYMMDDHHMMSS_create_todos.rb (the YYYYMMDDHHMMSS is the timestamp), the file contains a CreateTodos class, exactly like so:
class CreateTodos < ActiveRecord::Migration[5.2]
def change
create_table :todos do |t|
end
end
endwhich we can add columns to. Columns can be added in this (basic) format t.type :column_name, where type is the column type and column_name is the column name.
So if we want to add a title and description columns to the todos table, the class will become like this:
class CreateTodos < ActiveRecord::Migration[5.2]
def change
create_table :todos do |t|
## we specify the type/name of the field
## t.type :name
t.string :title # 'String' type is limited to 256 characters or less
t.text :description
end
end
endDoing this alone will not impact the database. To do that (create the table) we need to run the migration, and we do that with this rails task:
rails db:migrate
This will update your db/schema.rb file to match the structure of your database.
If we try to interact with the created table, in this case the todos table in the rails console, it will throw an error, and that's because we need a model to interact with items in the database.
To create a model, we create a file singular_table_name.rb (snake_case) in models/ folder, in this case todo.rb, within the file there has to be a class named SingularTableName (CamelCase) in this case Todo.
class Todo < ApplicationRecord
endNote: In rails 5 all models are subclasses of the ApplicationRecord provided by rails in models/application_record.rb.
Now we can interact with the table in the rails console, but so far the table is empty.
Note: Validation is also done here. Read more about ActiveRecord Validations
Creating an item in a table is easy and takes three steps (do this in the rails console):
- Initiate a new item object (Todo object in our example), that can be done using
ModelName.new(Todo.new)
> newTodo = Todo.new - Fill the object with values corresponding to the table columns (todo
titleanddescriptionin our case)
> newTodo.title = "Learn Rails"
> newTodo.description = "Learn Ruby on Rails and become a better developer"- Save the object to the database
> newTodo.saveNotes:
- Since we didn't specify a
primary_keyin the todo model, active record will assign anid(starting at1) field as theprimary_keyand will autoincrement it everytime a todo is added. - To both create and save the object to the database you can use
createmethod. e.g.
> Todo.create(title: "Learn Rails", description: "Learn Ruby on Rails and become a better developer")Typing Todo.all will return all todos in the database.
If we want to find a specific todo by id we can use .find:
# looking for todo with id=2
> todoFound = Todo.find 2we can get the first todo
> firstTodo = Todo.first
or the last todo
> lastTodo = Todo.last
We can manipulate the object we got and save it to update the todo:
> todoFound = Todo.find 2
> todoFound.title = "Do something else"
> todoFound.description = "Do something else entirely"
> todoFound.save
To Delete a todo from the database we can use the destroy method:
> todo1 = Todo.find 1
> todo1.destroy
Or to delete all todos we can use .destroy_all
> Todo.destroy_all
Read more about it in Active Record Basics - CRUD: Reading and Writing Data
Adding/Deleting/Updating items in the database through the console is only a developer thing, the user doesn't (and shouldn't) have access to the console. That's is why usually a model has associated routes for CRUD action. This table illustrates these routes and what they are responsible for:
| HTTP Verb | Path | Controller#Action | Used for |
|---|---|---|---|
| GET | /photos | photos#index | display a list of all photos |
| GET | /photos/new | photos#new | return an HTML form for creating a new photo |
| POST | /photos | photos#create | create a new photo |
| GET | /photos/:id | photos#show | display a specific photo |
| GET | /photos/:id/edit | photos#edit | return an HTML form for editing a photo |
| PATCH | /PUT /photos/:id | photos#update | update a specific photo |
| DELETE | /photos/:id | photos#destroy | delete a specific photo |
Specifying every single route can be tiresome, fortunately rails can generate these routes for you. To that open the /config/routes.rb and write this line of code resources :todos, so the file will look this:
Rails.application.routes.draw do
# Other routes here ...
resources :todos
# Other routes here ...
endNavigating to one of the created routes will throw an error which will say that the controller doesn't exist, in this case the TodosController which we will need to create under controller/todos_controller.rb. Writing a TodosController will solve the error:
class TodosController < ApplicationController
endTrying again will throw another error stating that there is no action corresponding to that route, for example if we visit /todos/new the error will be The action 'new' could not be found for TodosController. Writing a new action will solve the error:
class TodosController < ApplicationController
def new
end
endAnd now the error will state that TodosController#new is missing a template for this request format..., which means we need to create a view template file corresponding to this action. To that we first need to create a todos/ folder under views/ and in that folder we create a template for every action (actions that don't need templates are create, update and destroy).
So we create views/todos/new.html.erb. Visiting /todos/new will now show a blank page, all we need to do is fill it accordingly (/todos/new should show a form) and this can be done for other routes too.
Forms in web applications are an essential interface for user input. However, form markup can quickly become tedious to write and maintain because of the need to handle form control naming and its numerous attributes. Rails does away with this complexity by providing view helpers for generating form markup. However, since these helpers have different use cases, developers need to know the differences between the helper methods before putting them to use.
In the past section we created a TodosController with a new action (method) and a template under views/todos/new.html.erb, which enabled us to visit the /todos/new route, but only shows us a blank page and that's because the template is empty.
To create form content we can simple, init a new Todo object and pass it to an instance variable (variables inside classes with @ in the beginning) inside the new action:
def new
@todo = Todo.new
endand in the new.html.erb file we can create form field with th help of form helpers:
<%= form_for @todo do |f| %>
<%= f.label :title %>
<%= f.text_field :title, placeholder: "Give your todo a title" %>
<%= f.label :description %>
<%= f.text_area :description, placeholder: "describe your todo" %>
<%= f.submit "Add" %>
<% end %>
Submitting this form will throw an error as there is no controller to handle the creating of the todo or create route.
For that we create a create action in the TodosController. The action should:
- get the data of the form
- create a new todo and saves it
- if todo is saved we redirect to the created todo
showpage. - else we redirect to the new todo form again and we prompt the errors
- if todo is saved we redirect to the created todo
class TodosController < ApplicationController
# new action ...
def create
end
endNote: As the create action isn't concerned with showing anything, we shouldn't create a template for it. Same goes for the update and delete.
The form data is stored in a hash under the name of params, and since we created the form using rails form helpers and a @todo object, rails sets the new todo form data under params[:todo].
We can now easily create a new todo in the database using the Todo.new(params[:todo]) and then we save the ActiveRecord object we get back, but that would be a bit unsafe since the user can send anything. To avoid that we use Strong Parameters which allows us to "protect attributes from end-user assignment" and that's by creating a private method that checks what we require and permit:
# ...
private
def todo_params
params.require(:todo).permit(:title, :description)
endWhat we are saying here is that we require todo and we only permit title and description on that todo. And we cans simply use it in the create action. We can also use it in the update action later on since it requires the same thing.
class TodosController < ApplicationController
# new action ...
def create
@todo = Todo.new(todo_params)
if @todo.save
redirect_to todo_path(@todo)
else
render 'new'
end
end
private
def todo_params
params.require(:todo).permit(:title, :description)
end
endWe could also show the errors when the todo save fails, and we have access to them in new.html.erb (since we redirected to it on save fail) through @todo.errors, so we could put a conditional statement to check if we have any errors and we show a message
<% if @todo.errors.any? %>
<p>Failed to create todo, here is why:</p>
<ul>
<% @todo.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
<% end %>When the todo is successfully created the server will redirect to the show page of that todo /todos/:id (:id is the created todo id), so we need to create a show action and a template under views/todos/show.html.erb.
In the show action we need to find the created todo by its :id and like we did earlier set it to an instance method so that we can access it in the view and show the todo.
We previously worked with params. params hash also holds the route params. e.g. when visiting a specific todo /todos/:id we have access to the id attribute through params[:id]. So the show action should look something like this:
def show
@todo = Todo.find(params[:id])
endNote: We can also add a condition to check if we actually found the todo, since the show route isn't only accessed when the an item is created.
All we need now is the edit,update and index actions, they are similar to the new and create actions, except in the the edit action we don't show an empty form, we show the form filled with the todo info we want to edit.
Fortunately rails does that for us. So it would look something like this:
# app/controllers/todo_controller.rb
class TodosController < ApplicationController
# other actions
def edit
@todo = Todo.find(params[:id])
end
def update
@todo = Todo.find(params[:id])
# we check if the todo was updated
if @todo.update(todo_params)
# we set a flash message
flash[:notice] = "Todo was updated successfully"
redirect_to todo_path(@todo)
else
flash[:alert] = "Todo was not updated"
render 'edit'
end
end
# other actions
end<%# /app/views/todos/edit.html.erb %>
<%= form_for @todo do |f| %>
<%# form fields go here %>
<% end %>The index action (show all todos route) is easier, we just need to get all todos using Todo.all and loop over them with the .each method in the index.html.erb.
For the destroy action that takes care of deleting a todo, all we need to do is find the todo and delete it, if we successfully delete the todo then we redirect to the todos listing or the index route, else then we show the todo. This can easily be done like so:
def destroy
@todo = Todo.find(params[:id])
if(@todo.destroy)
flash[:notice] = "Todo was deleted successfully"
redirect_to todos_path
else
flash[:alert] = "Todo was not updated"
render todo_path(@todo)
endNote: I also set flash messages, which we will see in the next section
The best way to prompt to the user if the creation of an item has been successful or not is using flash, and that's because they just live to fulfill their purpose which is to show once to the user and get destroyed.
A flash message can be set on the flash hash, and accessed in the view with the same name. Read more about flash in rails.
Having to manually write the url for each route is a cumbersome task, rails comes with helper to make it easy and that's the link_to helper. It takes this form <%= link_to "anchor tag text here" path %>. So if you want to make:
- New todo form link:
<%= link_to "Create new todo", new_todo_path %>
- Edit todo form link (we give it the todo object):
<%= link_to "Edit new todo", edit_todo_path(todo) %>`
- Show todo link:
<%= link_to "Create new todo", todo_path(todo) %>
- Delete todo link (we specify that the method is delete):
<%= link_to "Delete Todo", todo_path(todo), method: :delete, data: {confirm: "Are you sure?"} %>
- Show all todos link:
<%= link_to "Show me all todos", todos_path %>
Partials help us to split the layout into manageable chunks. For example we can take some code that we think can be split into its own file like a header or footer and create separate files for them, for the header chunk create a _header.html.erb and for the footer a _footer.html.erb under views/layouts. If you notice then you'll see that the partial file names start with an _ (underscore).
To reference a partial in a certain file you need to do it like so <%= render 'path/to/partial'%> the path/to/partial must be under the views/ folder. So for example to reference the header partial we talked about above we can just write <%= render 'layouts/header'%>. If you notice again you'll see we don't need to prefix the partial name with an underscore when referencing it.
Note: It's a convention that the partial resides in the same folder (or a child folder) the file referencing it resides in. So for partials that are added to application.html.erb they should reside in views/layouts and for partials that are added to a todos view then they can be added to views/todos.