Objective |
---|
We should grasp the concepts related to routing requests, connecting them to controllers, present a view, and gain a naive understanding of the flow of a request and response. |
- Conventions over Configurations
- Models, Views, and Controllers
- HTTP (GET, POST, PUT, Delete) verbs
- The
params
of a request, (Covered in Sinatra) - Forms (action, method, and form fields)
- redirects
Motive: |
---|
Familiarize ourselves with the initial setup of a new application with the intent of making a planes application |
$ rails new route_app
$ cd route_app
$ rails s
Now our app is up and running, localhost:3000. At our root
route we notice a "Welcome aboard message". That's because we have yet to create a controller and views that we can set as our root.
Go to config/routes.rb
and inside the routes block erase all the commented text. Now we can define all our routes.
Let's get ready for our first route.
-
The nature of any route goes as follows:
request_type '/for/some/path/goes', to: "controller#method"
e.g. if we had a
PuppiesController
that had aindex
method we could sayget "/puppies", to: "puppies#index"
-
Using the above routing pattern we'll write our first
/config/routes.rb
RouteApp::Application.routes.draw do get '/', to: "planes#index" end
We'll have to define a
planes
controller and aindex
method soon enough. -
However, we distinguish the
'/'
(root) from other routes, and we just writeroot to: 'planes#show'
to indicate it inroutes.rb
./config/routes.rb
RouteApp::Application.routes.draw do root to: 'planes#index' end
We want to create a planes_controller. NOTE: Controller names are always plural and files should always be snake_case
.
$ subl app/controllers/planes_controller.rb
Let's begin with the following
class PlanesController < ApplicationController
def index
render text: "Hello, pilots."
end
end
We have defined thePlanesController
class, given it the method #index
, and told the #index
to render a text response 'Hello, pilots.'
Note: We've also indicated that
PlanesController
inherits fromApplicationController
, which looks like the following:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
end
Indeed,
ApplicationController
also inherits fromActionController::Base
, which is just the main action handling class. Actions it might handle are requests, responses, rendering views, etc. TheApplicationController
helps define the setup/configuration of all other controllers and has methods defined accross the entire application.
If we go to localhost:3000/ we get the greeting.
Let's seperate our rendered greeting into a view called index.html.erb
, which by default ActionController
will look for in a app/views/planes/
folder. Create the file below
app/views/planes/index.html.erb
Hello, pilots!
and make the following changes to PlanesController
.
app/controllers/planes_controller.rb
PlanesController < ApplicationController
def index
# Note it used to say
# render text: 'Hello, pilots'
render :index
end
end
A model is just a representation of a SQL table in our database, and the communication between the two is handled by rails via ActiveRecord
, which has a list of prestored SQL commands to facilitate communication.
In terminal, we create our plane model using a rails generator as follows,
$ rails g model plane name type description
which just creates the instructions in our app to tell SQL to create our model. To actually create this table data in our SQL database we do a migration. To migrate our database we use rake
as follows:
$ rake db:migrate
Now our application will have access to a model called Plane
that will be persitent.
NOTE: DON'T SKIP THIS STEP
We go straight into terminal to enter rails console.
$ rails console
The command above enters the rails console to play with your application.
To create our first plane model in our database we use our reference the Plane
class and call the Plane#create
method to write our plane to our database.
> Plane.create({name: "x-wing", type: "unknown", description: "top secret"})
=> #<Plane ....>
This will avoid issues later with index
trying to render planes that aren't there.
Motive |
---|
We've seen a little of each part of the MVC framework, and now we cycle back through over and over as we develop an understanding of the work flow. |
We don't have any planes in our database yet. To be able to make planes we must create a route for them. The RESTful convention would be to make a form available at /planes/new
.
Let's add this route.
/config/routes.rb
RouteApp::Application.routes.draw do
root to: 'planes#index'
# just to be RESTful
get '/planes', to: 'planes#index'
# it's a `get` because
# someone is requesting
# a page with a form
get '/planes/new', to: 'planes#new'
end
The request for /planes/new
will search for a planes#new
, so we must create a method to handle this request. This will render the new.html.erb
in the app/views/planes
folder.
app/controllers/planes_controller.rb
PlanesController < ApplicationController
...
def new
render :new
end
end
Let's create the app/views/planes/new.html.erb
with a form that the user can use to sumbit new planes to the application. Note: the action is /planes
because it's the collection we are submiting to, and the method is post
because we want to create.
app/views/planes/new.html.erb
<form action="/planes" method="post">
<input type="text" name="plane[name]">
<input type="text" name="plane[type]">
<textarea name="plane[description]"></textarea>
<button> Save Plane </button>
</form>
Note: how we have now defined our next route
, which is
post "/planes", to: "planes#create"
Our sumbission of the plane
form in new.html.erb
isn't being routed at the moment let's change that
/config/routes.rb
RouteApp::Application.routes.draw do
root to: 'planes#index'
get '/planes', to: 'planes#index'
get '/planes/new', to: 'planes#new'
# handle the submitted form
post '/planes', to: 'planes#create'
end
This leads to the most complicated method yet to be talked about. For now we will just make it redirect to the "/planes"
route.
app/controllers/planes_controller.rb
PlanesController < ApplicationController
...
def create
redirect_to "/planes"
end
end
Someone should now be able to submit a form to our site, right???
It turns out that forms aren't so simple in Rails anymore. There are security concerns that rails is trying to handle from the very beginning.
Recall that we indicated that
PlanesController
inherits fromApplicationController
, which looks like the following:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
end
The line that says
protect_from_forgery with: :exception
means that our forms will have added layer of security.
To even get our rails app to accept our form it needs an authenticity_token
, which we will casually add in and explain later.
app/views/planes/new.html.erb
<form action="/planes" method="post">
<input type="text" name="plane[name]">
<input type="text" name="plane[type]">
<textarea name="plane[description]"></textarea>
<%= token_tag form_authenticity_token %>
<button> Save Plane </button>
</form>
Our form should now submit properly. However, we will see that rails makes handling all the things required in a form easier using something called form helpers later.
We just need to save the data being sent in the request. We might be tempted to do the following.
app/controllers/planes_controller.rb
PlanesController < ApplicationController
...
def create
plane = params[:plane]
Plane.create(plane)
redirect_to "/planes"
end
end
However, while this might be fine in rails 3.2
it won't fly in rails 4.0
, which has something called strong parameters. To follow this strong parameters convention we must change the way we accept params to something like one of the following.
We can grab the :plane
hash out of the params
hash, and the tell it to permit the keys we want: :name
, :type
, and :description
.
app/controllers/planes_controller.rb
PlanesController < ApplicationController
...
def create
plane = params[:plane].permit(:name, :type, :description)
Plane.create(plane)
redirect_to "/planes"
end
end
or, (preferably) just say .require(:plane)
app/controllers/planes_controller.rb
PlanesController < ApplicationController
...
def create
plane = params.require(:plane).permit(:name, :type, :description)
Plane.create(plane)
redirect_to "/planes"
end
end
In reality strong params is just a nice way of making sure someone isn't setting param values that you don't want them to be setting.
We first need to setup our #index
method in planes
app/controllers/planes_controller.rb
PlanesController < ApplicationController
def index
@planes = Plane.all
render :index
end
...
end
Let's finally put some erb
in our index
view.
app/views/index.html.erb
<% @planes.each do |plane| %>
<div>
Name: <%= plane[:name] %> <br>
Type: <%= plane[:type] %> <br>
Description: <%= plane[:description] %>
</div>
<% end %>
We've successfully made an index
, new
, and create
. Let's talk about adding show
, edit
, and update
Right now, our app redirects to #index
after a create, which isn't helpful for quickly verifying what you just created. To do this we create a #show
.
Let's add our show
route.
/config/routes.rb
RouteApp::Application.routes.draw do
root to: 'planes#index'
## My new show method
get '/planes/:id', to: 'planes#show'
get '/planes', to: 'planes#index'
get '/planes/new', to: 'planes#new'
post '/planes', to: 'planes#create'
end
Is this right?? No, our /planes/:id
path is above our /planes/new
path, which means it will never get checked, and all requests for /planes/new
.
It should be as follows
/config/routes.rb
RouteApp::Application.routes.draw do
root to: 'planes#index'
get '/planes', to: 'planes#index'
get '/planes/new', to: 'planes#new'
## My new show method
get '/planes/:id', to: 'planes#show'
post '/planes', to: 'planes#create'
end
A controller method
app/controllers/planes_controller.rb
PlanesController < ApplicationController
...
def show
plane_id = params[:id]
@plane = Plane.find(plane_id)
render :show
end
end
A view for showing a plane
app/views/show.html.erb
<div>
Name: <%= @plane.name %> <br>
Type: <%= @plane.type %> <br>
Description: <%= @plane.description %>
</div>
The #create
method redirects to #index
(the /planes
path), but this isn't very helpful for verrifying that a newly created plane was properly created. The best way to fix this is to have it redirect to #show
.
PlanesController < ApplicationController
...
def create
plane = params.require(:plane).permit(:name, :type, :description)
plane = Plane.create(plane)
redirect_to "/planes/#{plane.id}"
end
end
Recall that the #show
method has a path /planes/:id
, which means we need to specify the id
of the plane we want to show. To do this we added
plane = Plane.create(plane)
and once it's created it has an id
we can use to redirect. We use string interpolation as follows.
redirect_to "/planes/#{plane.id}"
Editing a plane model requires two seperate methods. One to display the model information to be edited by the client, and another to handle updates submitted by the client.
If look back at how we handled the getting of our new
form we see the following pattern.
- Make a route first
- Define a controller method
- render view
The only difference is that now we need to use the id
of the object to be edited. We get the following battle plan.
- Make a route first
- Make sure it specifies the
id
of the thing to be edited
- Make sure it specifies the
- Define a controller method
- Retrieve the
id
of the model to be edited fromparams
- use the
id
to find the model
- Retrieve the
- render view
- use model to display in the form
We begin with handling the request from a client for an edit page.
-
We can easily define a RESTful route to handle getting the edit page as follows
/config/routes.rb
RouteApp::Application.routes.draw do root to: 'planes#index' get '/planes', to: 'planes#index' get '/planes/new', to: 'planes#new' get '/planes/:id', to: 'planes#show' # The Edit path get '/planes/:id/edit, to: 'planes#edit' post '/planes', to: 'planes#create' end
-
Similarly, using our
#show
method as inspiration we write an#edit
methodapp/controllers/planes_controller.rb
PlanesController < ApplicationController ... def edit plane_id = params[:id] @plane = Plane.find(plane_id) render :edit end end
-
Let's quickly begin the setup of an
edit
form using ournew.html.erb
from earlier, by just adding...value="<%= @plane.attr_name %>"...
to each respect field.app/views/planes/edit.html.erb
<form action="/planes" method="post"> <input type="text" name="plane[name]" value="<%= @plane.name %>"> <input type="text" name="plane[type]" value="<%= @plane.type %>"> <textarea name="plane[description]" value="<%= @plane.description %>"></textarea> <%= token_tag form_authenticity_token %> <button> Update Plane </button> </form>
-
Next we have to modify the
action
andmethod
, but we can't explicitly change the method at the top of the form, because the browser still needs to send aPOST
request. Instead we add field that contains the method we actually want to make,PUT
. Using something as follows<input name="_method" type="hidden" value="put" />
Combined with the change we make to the
action
we get the following.app/views/planes/edit.html.erb
<form action="/planes/<%= @plane.id %>" method="post"> <input name="_method" type="hidden" value="put" /> .... </form>
That's pretty much the whole-shebang when comes to getting an edit page. Our previous knowledge has really come to help us understand what we need to do. We'll see this also true for the update that still needs to be handled witht the submission of the form above.
If look back at how we handled the submission of our new
form we see the following pattern.
- Make a route first
- Define a controller method
- redirect to something
The only difference now is that we will need to use the id
of the object being update.
- Make a route first
- Make sure it specifies the
id
of the thing to be updated
- Make sure it specifies the
- Define a controller method
- Retrieve the
id
of the model to be updated fromparams
- use the
id
to find the model - retrieve the updated info sent from the form in
params
- update the model
- Retrieve the
- redirect to show
- use
id
to redirect to#show
- use
-
Make a route that uses the
id
of the object to be updated/config/routes.rb
RouteApp::Application.routes.draw do root to: 'planes#index' get '/planes', to: 'planes#index' get '/planes/new', to: 'planes#new' get '/planes/:id', to: 'planes#show' get '/planes/:id/edit, to: 'planes#edit' post '/planes', to: 'planes#create' # Route the incoming update using the id put '/planes/:id', to 'planes#update' end
Note the method we now need to create is called
#update
-
In the
PlanesController
we will create the#update
method mentioned aboveapp/controllers/planes_controller.rb
PlanesController < ApplicationController ... def update plane_id = params[:id] plane = Plane.find(plane_id) # get updated data updated_attributes = params.require(:plane).permit(:name, :type, :description) # update the plane plane.update_attributes(updated_attributes) #redirect to show redirect_to "/posts/#{plane_id}" end end