Skip to main content Link Search Menu Expand Document (external link)

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

  1. What is Ajax?
  2. Why use Ajax?
  3. Post Address Association
  4. Post Address Select
  5. Location Controller

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.


Back to top

Copyright © 2020-2022 Secure Smarter Service, Inc. This site is powered by KodaCamp.