Philippine Address Selector
In this article, we will make a feature that will dynamically populate a <select>
of provinces based on the selected region
using ajax
, or what we we call as two level hierarchy select. We will add an address selection in our post using the data of Philippine Address Service that we previously made by calling the api of provinces and regions with the use of ajax
.
Table Of Contents
What is Ajax?
Ajax is an alias for Asynchronous JavaScript and XML. It can send and receive data in a variety of forms such as JSON, XML, HTML, and text files. The most interesting feature of AJAX is its “asynchronous” nature, which means it may communicate with the server, exchange data, and update the website without requiring a page refresh.
Why use Ajax?
Ajax enhances web application speed and usability. It enables applications to render without data, reducing server traffic inside requests. For that reason, us developers can drastically reduce the time required for both sides responses.
Post Address Association
Let’s start this feature by adding the association of address province and region to post.
Generate migration for references address_region
, address_province
and address
with data type string to posts
table.
root@0122:/usr/src/app# rails g migration AddAddressToPost
invoke active_record
create db/migrate/xxxxxxxxxxxxxx_add_address_to_post.rb
# db/migrate/xxxxxxxxxxxxxx_add_address_to_post.rb
class AddAddressToPost < ActiveRecord::Migration[7.0]
def change
add_column :posts, :address, :string
add_reference :posts, :address_region
add_reference :posts, :address_province
end
end
Then Migrate.
root@0122:/usr/src/app# rails db:migrate
== xxxxxxxxxxxxxx AddAddressToPost: migrating =================================
-- add_column(:posts, :address, :string)
-> 0.0065s
-- add_reference(:posts, :address_region)
-> 0.0157s
-- add_reference(:posts, :address_province)
-> 0.0168s
== xxxxxxxxxxxxxx AddAddressToPost: migrated (0.0393s) ========================
Add respective associations.
# app/models/address/province.rb
class Address::Province < ApplicationRecord
belongs_to :region
has_many :cities
+ has_many :posts, class_name: 'Post', foreign_key: 'address_province_id'
end
# app/models/address/region.rb
class Address::Region < ApplicationRecord
has_many :provinces
+ has_many :posts, class_name: 'Post', foreign_key: 'address_region_id'
end
# app/models/post.rb
class Post < ApplicationRecord
# ...
belongs_to :user
+ belongs_to :region, class_name: 'Address::Region', foreign_key: 'address_region_id'
+ belongs_to :province, class_name: 'Address::Province', foreign_key: 'address_province_id'
mount_uploader :image, ImageUploader
# ...
end
Permit the province and region params to our posts controller.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# ...
def post_params
- params.require(:post).permit(:title, :content, :image, category_ids: [])
+ params.require(:post).permit(:title, :content, :image, :address, :address_region_id, :address_province_id, category_ids: [])
end
# ...
end
Add a display of address to posts index.
<!-- app/views/posts/index.html.erb -->
<!-- ... -->
<table>
<!-- ... -->
+ <td>address</td>
<td>image</td>
<td>action</td>
</thead>
<% @posts.each do |post| %>
<tr>
<!-- ... -->
+ <td><%= "#{post.region&.name} #{post.province&.name} #{post.address}" %></td>
<td><%= image_tag post.image.url if post.image.present? %></td>
<!-- ... -->
Don’t forget to include the region and province in your post query to avoid n+1.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# ...
def index
- @posts = Post.includes(:categories, :user).page(params[:page]).per(5)
+ @posts = Post.includes(:categories, :user, :region, :province).page(params[:page]).per(5)
# ...
end
# ...
end
Post Address Select
Let’s add a <select>
for regions and provinces to posts form.
<!-- app/views/posts/_form.html.erb -->
<%= form_with model: post do |form| %>
<!-- ... -->
<div>
<%= form.file_field :image %>
</div>
+ <div>
+ <%= form.label :address %>
+ <%= form.text_field :address %>
+ </div>
+ <div>
+ <%= form.label :address_region_id %>
+ <%= form.collection_select :address_region_id,
+ Address::Region.all, :id, :name,
+ { prompt: 'Please select region' } %>
+ </div>
+ <div>
+ <%= form.label :address_provinces_id %>
+ <%= form.collection_select :address_provinces_id,
+ Address::Province.all, :id, :name,
+ { prompt: 'Please select province' } %>
+ </div>
<%= form.submit %>
<% end %>
Start your server and let’s take a look at create posts.
This kind of selection for provinces have a problem. It displays all the data of provinces, making it hard for the user to pick and potentially ruin your data because they might pick a province that does not belong to the region they selected. This is where ajax
comes in.
Location Controller
To handle the ajax
request, let’s create a location_controller.js
with method fetchProvince()
and load it right away to our controller index.
// app/javascript/controllers/location_controller.js
import {Controller} from "@hotwired/stimulus"
export default class extends Controller {
fetchProvinces(){
}
}
// app/javascript/controllers/index.js
// ...
+
+ import LocationController from "./location_controller";
+ application.register("location", LocationController)
Then we’ll add the location
controller to our post form and clear out the provinces <select>
options and values.
<!-- app/views/posts/_form.html.erb -->
- <%= form_with model: post do |form| %>
+ <%= form_with model: post, data: { controller: :location } do |form| %>
<!-- ... -->
<div>
<%= form.label :address_provinces_id %>
<%= form.collection_select :address_provinces_id,
- Address::Province.all, :id, :name,
+ [], nil, nil,
{ prompt: 'Please select province' } %>
</div>
<%= form.submit %>
<% end %>
Now let’s start making the populate method. With ajax
, we can make a request calling the api for provinces everytime the region is changed. In order to that, we have to make action event in our region <select>
. While you are on it, also add a target to take the region id that will be passed to our provinces api.
<!-- app/views/posts/_form.html.erb -->
<%= form_with model: post do |form| %>
<%= form_with model: post, data: { controller: :location } do |form| %>
<!-- ... -->
<div>
<%= form.label :address_region_id %>
<%= form.collection_select :address_region_id,
Address::Region.all, :id, :name,
- { prompt: 'Please select region' } %>
+ { prompt: 'Please select region' },
+ data: { location_target: 'selectedRegionId', action: 'change->location#fetchProvinces' } %>
</div>
<!-- ... -->
<%= form.submit %>
<% end %>
Then change the contents of fetchProvinces()
method in our location controller to:
// app/javascript/controllers/location_controller.js
import {Controller} from "@hotwired/stimulus"
export default class extends Controller {
+ static targets = ['selectedRegionId']
fetchProvinces(){
+ $.ajax({
+ type: 'GET',
+ url: '/api/v1/regions/' + this.selectedRegionIdTarget.value + '/provinces',
+ dataType: 'json',
+ success: (response) => {
+ console.log(response)
+ }
+ })
}
}
Now take a look at the dev console.
We now know that the data for provinces is already being taken, all that’s left to do is to fill the contents of provinces <select>
.
Let’s set province <select>
as a target.
<!-- app/views/posts/_form.html.erb -->
<%= form_with model: post, data: { controller: :location } do |form| %>
<!-- ... -->
<div>
<%= form.label :address_province_id %>
<%= form.collection_select :address_province_id,
[], nil, nil,
- { prompt: 'Please select province' } %>
+ { prompt: 'Please select province' },
+ data: { location_target: 'selectProvinceId' } %>
</div>
<%= form.submit %>
<% end %>
Then go back to fetchProvince()
method, go through each provinces api response data, and set it to province targets option and text.
// app/javascript/controllers/location_controller.js
import {Controller} from "@hotwired/stimulus"
export default class extends Controller {
- static targets = ['selectedRegionId']
+ static targets = ['selectedRegionId', 'selectProvinceId']
fetchProvinces(){
+ let target = this.selectProvinceIdTarget
+
$.ajax({
type: 'GET',
url: '/api/v1/regions/' + this.selectedRegionIdTarget.value + '/provinces',
dataType: 'json',
success: (response) => {
console.log(response)
+ $.each(response, function (index, record) {
+ let option = document.createElement('option')
+ option.value = record.id
+ option.text = record.name
+ target.appendChild(option)
+ })
}
})
}
}
Go back to the posts create and check try selecting a region.
It’s almost complete, but this actually have some bug. Notice that it will keep on adding provinces data everytime you change the region. We can solve this by clearing the provinces <select>
before actually setting its contents.
// app/javascript/controllers/location_controller.js
import {Controller} from "@hotwired/stimulus"
export default class extends Controller {
static targets = ['selectedRegionId', 'selectProvinceId']
fetchProvinces(){
let target = this.selectProvinceIdTarget
+ $(target).empty();
$.ajax({
type: 'GET',
url: '/api/regions/' + this.selectedRegionIdTarget.value + '/provinces',
dataType: 'json',
success: (response) => {
console.log(response)
$.each(response, function (index, record) {
let option = document.createElement('option')
option.value = record.id
option.text = record.name
target.appendChild(option)
})
}
})
}
}
Now we successfully made a two level hierarchy select of Philippine Address using ajax
and our Address API.