For a recent proof-of-concept Rails project, a custom, dynamic, database created URL was required.
The requirements:
- allow a user to see all mountain biking and hiking trails filtered by state
- a bookmark-able (for quick access to the particular state) human-readable URL.
This walkthrough builds off Rubyonrails.org 3.2 Dynamic Segments with more details and support for use.
Assumptions:
- You have a basic/entry-level working knowledge of Rails routes with
resources
helper method (e.g.resources :users, only: [:show, edit]
). - You only need to
get
and notupdate
,patch
,post
. - You have a database column with data suitable for making a url.
- You read 3.2 Dynamic Segments and want a detailed example.
Step 1) Create the Route, aka the Dynamic Segments
Determine what your desired URL structure looks like.
We needed the url to look like /trails/state/wa
, /trails/state/pa
, or /trails/state/az
.
The state abbreviation (e.g. WA, AZ, etc) are the url params[:state ]
(similar to params[id]
); the database contained consistent and searchable two letter state abbreviations. As such, the route in routes.rb
is:
get "trails/state/:state", to: "trails#index", as: "trail_state"
Note: Place this route on the bottom of routes.rb so it does not impact other routes; it is the last route tested.
The route explained:
- get
trails/state/wa
, to thetrails#index
action controller to execute theindex.html.erb
code. -
trail_state
is the helper path method to create the url (and the query!) for the unique state page. *For example, a link to all Washington trails:
<%= link_to "Washington Trails", trail_state_path("WA")%>
Step 2) Query the Database via Your Model
Your exact query will vary but what you query (the query string) is from your url params (in our case params[:state]
).
The ActiveRecord query method is set in the trail.rb model
as such:
def self.bystate(state:, amount: "10")
# default is to pull 10 trails from DB, unless otherwise specified. This is optional.
Trail.joins(:park).where(parks: {state: state}).take(amount)
end
** Note on the model relationships for the curious:
-
trail.rb
belongs_to :park
-
park.rb
has_many :trails
. -
:state
is a column in the trails table. -
:park
is the parks table. -
take(10)
pulls the first only the first 10 records for this proof of concept.
Now we have access to all (errr...10) records based on the query string set by the url.
Step 3) Display Data By Custom Query & URL.
First, create your user interface.
Depending on your needs, you could loop through your database to create the link_to
or you can hardcode.
For the proof of concept, we hardcoded the states and an “All Trails” button on the trails index.html.erb
page (below with CSS formatting removed):
<div >
<h3><%= link_to "California Trails", trail_state_path("CA")%><h3>
<h3><%= link_to "Washington Trails", trail_state_path("WA")%></h3>
<h3><%= link_to "All Trails", trails_path%><h3>
</div>
On-screen a user clicks to see all trails or select state trails. The default index view is all trails via a normal get trails/
route.
Second, determine what to show based on url action.
To determine if we load a custom route and the resulting data query, check if the params[:state]
is blank. If false (ie. no params[wa]
), load all trails.
<%if !params[:state]%>
<%# if there is no request for trails by state, show all the trails%>
<% Trail.all.each do |trail|%>
<%= render partial: "trails/trail", locals: {trail: trail}%>
<%end%>
<%end%>
Otherwise, params[:state]
is true and query the database and reload the index page with only selected state trails
<% Trail.bystate(state:params[:state]).each do |trail| %>
<%= render partial: "trails/trail", locals: {trail: trail}%>
<%end%>
The Full Code Snippets
routes.rb
Rails.application.routes.draw do
resources :parks, only: [:index, :show]
resources :trails, only: [:index, :show]
root to: "homepage#index"
get "trails/state/:state", to: "trails#index", as: "trail_state"
end
trail.rb model
class Trail < ApplicationRecord
belongs_to :park
def self.bystate(state:, amount: "10")
Trail.joins(:park).where(parks: {state: state}).take(amount)
end
end
index.html.rb
<h2 class="font-weight-light text-center mt-5">See Trails by State</h2>
<div class="row mx-md-n5 mt-5">
<h3 class="font-weight-light text-center col px-md-3 d-flex justify-content-center"><%= link_to "California Trails", trail_state_path("CA")%><h3>
<h3 class="font-weight-light text-center col px-md-3"><%= link_to "Washington Trails", trail_state_path("WA")%></h3>
<h3 class="font-weight-light text-center col px-md-3 d-flex justify-content-center"><%= link_to "All Trails", trails_path%><h3>
</div>
<%# if there is a url request to show by state, show only that state%>
<div class="album">
<div class="container-fluid text-center">
<div class="d-flex justify-content-center row">
<% Trail.bystate(state:params[:state]).each do |trail| %>
<%= render partial: "trails/trail", locals: {trail: trail}%>
<%end%>
</div>
</div>
</div>
<%if !params[:state]%>
<%# if there is no request for trails by state, show all the trails%>
<div class="album font-weight-light">
<div class="container-fluid text-center">
<div class="d-flex justify-content-center row">
<% Trail.all.each do |trail|%>
<%= render partial: "trails/trail", locals: {trail: trail}%>
<%end%>
</div>
</div>
</div>
<%end%>
Top comments (3)
Hey Brad, for your curiosity, here's how I would approach this.
First, I would use a scope instead of a class method and remove
take
from it to leave it chainable as is expected from a scope.Take performs the query immediately and you will get an array of values instead of an object that can be chained with another query method (where, etc.).
There's no more pagination, but this should really be in the controller, just like the logic about the trails.
I would introduce a private method to filter trails and fall back on the default
all
scope if no state is provided.I use
limit
instead oftake
to postpone any query and leave the@trails
chainable in case we need to filter further down the road. I introduced an optional:per
param that you can send along with your query to have a more flexible limit (eg.trail_state_path("CA", per: 100)
)I would also save the selected state to use it in the view.
And now you don't need to have two blocks anymore for your list of trails in your index.
Also, you can use collection rendering instead of iterating.
You will probably want to make the currently selected state bolder, but I'll leave it there.
@arnaud Joubay Thank you for the time and suggestions. I'll remember the idea of keeping things as objects so one has the flexibility later. I like the refactor of removing the two blocks to show the states via collection rendering (a new concept for me).
You're welcome, glad you found my comment interesting.