BanditHijo.dev

Navigation Bar Global Menu Preferences pada Rails

Created at: 2019-12-21
Author by: BanditHijo

Prerequisite

ruby 2.6.3 rails 5.2.4 postgresql 11.5

Prakata

Sebenarnya saya kurang memahami harus memberikan judul seperti apa untuk catatan kali ini.

Tapi, mudah-mudahan judul yang saya berikan saat ini, dapat mewakili isi dari tulisan yang ingin saya dokumentasikan.

Mungkin saya akan mulai dengan memberikan ilustrasi gambar.

Gambar 1

Gambar 1. Navigation Bar dengan Menu Language dan Currency Preferences

Nah, sudah sedikit terbayang kan.

Jadi, dropdown menu tersebut mempunyai fungsi seperti ini:

  1. Dapat mengganti language dan currency di setiap halaman.
  2. Berfungsi pada user maupun guest
  3. Apabila guest melakukan registrasi, maka language atau currency yang mereka pilih juga akan ikut tersimpan.

Permasalahan

Awalnya saya berfikir, “Bagaimana bisa mengganti language dan currency yang merupakan field atau entitas dari tabel users pada setiap halaman?”

Selama ini, saya hanya melakukan action edit pada controller dan view yang bersangkutan dengan dimana field atau entitas tersebut berada.

Misal, pada halaman edit user. Di dalamnya terdapat input field untuk mengganti language dan currency.

Sedangkan, fungsi navbar ini harus dapat melakukan update data saat language dan currency dipilih pada semua halaman di dalam web.

Bagi Junior Rails Developer seperti saya yang masih anak kemarin sore, ini merupakan hal yang sangat baru.

Saking bersemangatnya, rasanya seperti ada tablet Redoxon yang di larutkan di dalam dada. Wkwkwk

Pemecahan Masalah

Sebagai sekenario, saya sudah memiliki tabel users dengan field locales:string untuk menyimpan data language dan field rate:string untuk menyimpan data currency.

Schema

db/schema.rb
1create_table "users", force: :cascade do |t|
2 # ...
3 # ...
4 # ...
5 t.string "locale", default: "en"
6 t.string "rate", default: "MYR"
7 # ...
8 # ...
9 # ...
10end

Controller

Karena locale dan rate terdapat pada tabel users maka kita akan menambahkan kedua controller tersebut untuk users.

Saya akan menggunakan struktur seperti yang pada catatan sebelum-sebelumnya, yaitu “Controller Namespaes and Routing”.1 dengan tujuan untuk memisahkan Admin dan User.

Oh ya, pada catatan kali ini, application_controller.rb yang akan menghandle users. Tidak seperti catatan sebelumnya yang saya pisahkan. Tujuannya agar proses pencatatan menjadi lebih ringkas. Wkwkwk

Masukkan kedua controller yang akan kita buat ke dalam direktori users/ agar nantinya mudah untuk di-maintain.

Kira-kira seperti ini struktur file dan direktorinya.

├─ 📂 app/
│  ├─ 📁 assets/
│  ├─ 📁 channels/
│  ├─ 📂 controllers/
│  │  ├─ 📁 admins/
│  │  ├─ 📁 concerns/
│  │  ├─ 📂 users/ 👈️
│  │  │  ├─ 📄 locales_controller.rb 👈️
│  │  │  └─ 📄 rates_controller.rb 👈️
│  │  ├─ 📄 admins_controller.rb
│  │  └─ 📄 application_controller.rb
│  ├─ ...
│  ...
├─ ...
...

Berikut ini isi dari file-file tersebut.

app/controllers/users/locales_controller.rb
1class Users::LocalesController < ApplicationController
2 before_action :authenticate_user!
3
4 def update
5 if current_user.update(locale_param)
6 redirect_to request.referer, notice: t('navbar.locale_updated')
7 else
8 redirect_to request.referer, notice: t('navbar.locale_failed_updated')
9 end
10 end
11
12 private
13
14 def locale_param
15 params.permit(:locale)
16 end
17end
app/controllers/users/rates_controller.rb
1class Users::RatesController < ApplicationController
2 before_action :authenticate_user!
3
4 def update
5 if current_user.update(rate_param)
6 redirect_back request.referer, notice: t('navbar.rate_updated')
7 else
8 redirect_back request.referer, notice: t('navbar.rate_failed_updated')
9 end
10 end
11
12 private
13
14 def rate_param
15 params.permit(:rate)
16 end
17end

Oh iya, karena catatan kali ini berhubungan dengan bahasa (locale), maka saya akan menyinggung sedikit penggunaan Rails Internationalization (I18n) API 2 untuk fungsi language preferences.

Pada dua controller di atas. Saya sudah menggunakan helper i18n pada object :notice, t('...'), yang nantinya akan memberikan notifikasi apakah proses perubahan berhasil atau tidak.

Oke langsung saja, saya buat dulu file en.yml.

config/locales/en.yml
1en:
2 navbar:
3 rate_updated: "Your currency rate has been updated"
4 rate_failed_update: "Failed to update your currency rate"
5 locale_updated: "Your default language has been updated"
6 locale_failed_update: "Failed to update your default language"

Karena saya juga menggunakan bahasa Mandarin, maka saya akan buat juga file ch.yml.

config/locales/ch.yml
1en:
2 navbar:
3 rate_updated: "您的货币汇率已更新"
4 rate_failed_update: "无法更新您的货币汇率"
5 locale_updated: "您的默认语言已更新"
6 locale_failed_update: "无法更新您的默认语言"

Selanjutnya, saya akan mendifinisikan dimana translation load path, permit available locale, dan default locale yang digunakan di config/initializers/locale.rb.

config/initializers/locale.rb
1# Where the I18n library should search for translation files
2I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]
3
4# Permitted locales available for the application
5I18n.available_locales = ['en', 'ch']
6
7# Set default locale to something other than :en
8I18n.default_locale = 'en'

Selanjutnya, saya akan mengkonfigurasi locale pada application_controller.rb untuk menangani semua permintaan terhadap locale.

Oh ya, sekalian untuk rate preferences juga.

app/controllers/application_controller.rb
1class ApplicationController < ActionController::Base
2 before_action :set_rate
3 around_action :set_locale
4
5 private
6
7 def set_rate
8 @rate = params[:rate]
9 end
10
11 def set_locale(&action)
12 locale = current_user.try(:locale) || params[:locale] || I18n.default_locale
13 I18n.with_locale(locale, &action)
14 end
15
16 def default_url_options
17 current_user ? {locale: I18n.locale} : {locale: I18n.locale, rate: @rate}
18 end
19end

current_user adalah object yang disediakan oleh Devise gem untuk user yang sudah loginmelakukan login.

Saya menggunakan helper default_url_options untuk membuat url form menjadi lebih mudah dibaca.

http://localhost:3000/en/users

Kalau saya tidak menggunakan helper tersebut, maka url secara default akan seperti ini.

http://localhost:3000/users?locale=en

Nah, pasti akan lebih memilih url form yang atas.

Route

Kemudian pada bagian routing, tinggal mengikuti controller.

config/routes.rb
1Rails.application.routes.draw do
2 scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do
3 # ...
4 # ...
5
6 namespace :users do
7 # ...
8 # ...
9 resources :rates, only: %w[update]
10 resources :locales, only: %w[update]
11 end
12 end
13
14 namespace :admins do
15 # ...
16 # ...
17 end
18end

Blok scope "(:locale)", locale: ... dimaksudkan untuk membuat url form menjadi seperti yang saya sebutkan di atas.

Sehingga blok-blok routing yang lain, harus dimasukkan ke dalam blok ini agar memiliki url form yang sama.

Karena Admin tidak memerlukan url form yang bagus, maka saya keluarkan saja dari blok tersebut.

Selanjutnya, tinggal membuat view template.

View

Seperti biasa, stylesheet pada catatan ini hanya sebagai contoh dan merupakan dummy. Jadi, akan tidak sesuai dengan yang ada pada ilustrasi gambar.

Hal ini dimaksudkan agar kode hanya terkonsentrasi pada blok erb yang berhubungan dengan topik yang sedang saya bahas.

Seperti yang tertulis pada judul, saya akan membuat menu ini pada navigation bar yang biasanya ada pada posisi atas dari halaman.

Struktur file dan direktorinya seperti ini.

├─ 📂 app/
│  ├─ 📁 assets/
│  ├─ 📁 channels/
│  ├─ 📁 controllers/
│  ├─ 📁 helpers/
│  ├─ 📁 jobs/
│  ├─ 📁 mailers/
│  ├─ 📁 models/
│  └─ 📂 views/
│     ├─ 📁 admins/
│     ├─ 📂 layouts/
│     │  ├─ 📁 admins/
│     │  ├─ 📂 users/ 👈️
│     │  │  ├─ 📂 navbar/ 👈️
│     │  │  │  ├─ 📄 _locale.html.erb 👈️
│     │  │  │  └─ 📄 _rate.html.erb 👈️
│     │  │  ├─ 📄 _flash_message.html.erb 👈️
│     │  │  └─ 📄 _navbar.html.erb 👈️
│     │  ├─ 📄 admins.html.erb
│     │  ├─ 📄 admins_devise.html.erb
│     │  ├─ 📄 application.html.erb
│     │  ├─ 📄 mailer.html.erb
│     │  └─ 📄 mailer.text.erb
│     └─ 📁 users/
├─ ...
...

Saya mulai dari membuat render partial dari navigation bar pada application.html.erb.

app/views/layouts/application.html.erb
1<!DOCTYPE html>
2<html>
3 <head>
4 <title>BANDITHIJO.COM</title>
5 <%= csrf_meta_tags %>
6 <%= csp_meta_tag %>
7
8 <meta name="viewport" content="width=device-width, initial-scale=1">
9 <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
10 <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
11 </head>
12
13 <body>
14 <%= render 'layouts/users/navbar' %>
15 <%= render 'layouts/users/flash_message' %>
16 <%= yield %>
17 </body>
18</html>

Berikut ini isi dari file _navbar.html.erb.

Saya membuat render partial untuk blok language dan currency.

app/views/layouts/users/_navbar.html.erb
1<nav class="navbar navbar-expand-xl">
2 <!-- Right Menu -->
3 <div class="collapse navbar-collapse" id="navbar4">
4 <ul class="navbar-nav ml-auto justify-content-end flex-grow-1 font-family-medium">
5 <li class="nav-item d-flex">
6 <!-- Community menu -->
7 </li>
8 <li class="nav-item d-flex">
9 <!-- Promo menu -->
10 </li>
11 <li class="nav-item d-flex">
12 <!-- Help menu -->
13 </li>
14
15 <!-- Dropdown Language Icon -->
16 <li class="nav-item d-flex dropdown">
17
18 <!-- For user -->
19 <% if user_signed_in? %>
20 <% unless current_user.locale == "ch" %>
21 <a class="nav-link align-self-center" href="#" id="dropdown-language" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
22 <%= image_tag("label/flag/us.svg", width: "20", height: "20") %>
23 ENG
24 <span class="icon-dropdown align-middle"></span>
25 </a>
26 <% else %>
27 <a class="nav-link align-self-center" href="#" id="dropdown-language" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
28 <%= image_tag("label/flag/ch.svg", width: "20", height: "20") %>
29 CNY
30 <span class="icon-dropdown"></span>
31 </a>
32 <% end %>
33 <!-- For Guest -->
34 <% else %>
35 <% unless current_page?(locale: "ch") %>
36 <a class="nav-link align-self-center" href="#" id="dropdown-language" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
37 <%= image_tag("label/flag/us.svg", width: "20", height: "20") %>
38 ENG
39 <span class="icon-dropdown"></span>
40 </a>
41 <% else %>
42 <a class="nav-link align-self-center" href="#" id="dropdown-language" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
43 <%= image_tag("label/flag/ch.svg", width: "20", height: "20") %>
44 CNY
45 <span class="icon-dropdown"></span>
46 </a>
47 <% end %>
48 <% end %>
49 <!-- END Dropdown Language Icon -->
50
51 <!-- Content of Dropdown List Language and Currency -->
52 <div class="dropdown-menu border-0 rounded">
53 <div class="row no-gutters">
54
55 <!-- Language Preferences -->
56 <%= render 'layouts/users/navbar/locale' %>
57 <!-- END Language Preferences -->
58
59 <!-- Currency Preferences -->
60 <%= render 'layouts/users/navbar/rate' %>
61 <!-- END Currency Preferences -->
62
63 </div>
64 </div>
65 </li>
66 <!-- END Dropdown Language Icon -->
67
68 <!-- Dropdown User Menu -->
69 ...
70 ...
71
72 </ul>
73 </div>
74 <!-- END Right Menu -->
75</nav>
app/views/layouts/users/navbar/_locale.html.erb
1<div class="col-md-auto border-bottom">
2 <span>Language</span>
3 <!-- Change language for user -->
4 <% if user_signed_in? %>
5 <%= button_to users_locale_path(current_user, locale: "en"), method: :put, class: "dropdown-item", style: "outline:none" do %>
6 <% if current_user.locale == "en" %>
7 <i class="icon-check-selected"></i>
8 <% else %>
9 <i class="icon-check-none"></i>
10 <% end %>
11 <span class="text-normal">
12 <%= image_tag("label/flag/us.svg", class: "mr-1", width: "20", height: "20") %>
13 English
14 </span>
15 <% end %>
16 <%= button_to users_locale_path(current_user, locale: "ch"), method: :put, class: "dropdown-item", style: "outline:none" do %>
17 <% if current_user.locale == "ch" %>
18 <i class="icon-check-selected"></i>
19 <% else %>
20 <i class="icon-check-none"></i>
21 <% end %>
22 <span class="text-normal">
23 <%= image_tag("label/flag/ch.svg", class: "mr-1", width: "20", height: "20") %>
24 Chinese
25 </span>
26 <% end %>
27 <!-- Change language for guest -->
28 <% else %>
29 <%= link_to({locale: "en"}, class: "dropdown-item #{"active" if current_page?(locale: "en") || current_page?(locale: "")}") do %>
30 <%= image_tag("label/flag/us.svg", class: "mr-1", width: "20", height: "20") %>
31 English
32 <% end %>
33 <%= link_to({locale: "ch"}, class: "dropdown-item #{"active" if current_page?(locale: "ch")}") do %>
34 <%= image_tag("label/flag/ch.svg", class: "mr-1", width: "20", height: "20") %>
35 Chinese
36 <% end %>
37 <% end %>
38</div>
app/views/layouts/users/navbar/_rate.html.erb
1<div class="col-md-auto px-2">
2 <span><%= t("navbar_menu.currency_title") %></span>
3 <!-- Change currency for user -->
4 <% if user_signed_in? %>
5 <%= button_to users_rate_path(current_user), params: {:rate => "MYR"}, method: :put, class: "dropdown-item", style: "outline:none" do %>
6 <% if current_user.rate == "MYR" %>
7 <i class="icon-check-selected"></i>
8 <% else %>
9 <i class="icon-check-none"></i>
10 <% end %>
11 MYR
12 <span class="text-normal">
13 Malaysian Ringgit
14 </span>
15 <% end %>
16 <%= button_to users_rate_path(current_user), params: {:rate => "USD"}, method: :put, class: "dropdown-item", style: "outline:none" do %>
17 <% if current_user.rate == "USD" %>
18 <i class="icon-check-selected"></i>
19 <% else %>
20 <i class="icon-check-none"></i>
21 <% end %>
22 USD
23 <span class="text-normal">
24 US Dollar
25 </span>
26 <% end %>
27 <%= button_to users_rate_path(current_user), params: {:rate => "CNY"}, method: :put, class: "dropdown-item", style: "outline:none" do %>
28 <% if current_user.rate == "CNY" %>
29 <i class="icon-check-selected"></i>
30 <% else %>
31 <i class="icon-check-none"></i>
32 <% end %>
33 RMB
34 <span class="text-normal">
35 Chinese Yuan
36 </span>
37 <% end %>
38 <!-- Change currency for guest -->
39 <% else %>
40 <%= link_to({rate: "MYR"}, class: "dropdown-item #{(params[:rate] == 'USD' || params[:rate] == 'CNY') ? '' : 'active'}") do %>
41 MYR
42 <span class="text-normal">
43 Malaysian Ringgit
44 </span>
45 <% end %>
46 <%= link_to({rate: "USD"}, class: "dropdown-item #{params[:rate] == 'USD' ? 'active' : ''}") do %>
47 USD
48 <span class="text-normal">
49 US Dollar
50 </span>
51 <% end %>
52 <%= link_to({rate: "CNY"}, class: "dropdown-item #{params[:rate] == 'CNY' ? 'active' : ''}") do %>
53 RMB
54 <span class="text-normal">
55 Chinese Yuan
56 </span>
57 <% end %>
58 <% end %>
59</div>

Selanjutnya, isi dari file _flash_message.html.erb

app/views/layouts/users/_flash_message.html.erb
1<% flash.each do |name, msg| %>
2 <div class="alert bg-<%= name == 'error' ? 'secondary' : 'primary' %> text-center text-white">
3 <i class="fa fa-exclamation-triangle mr-1"></i><%= msg %>
4 <span class="close-button fa fa-times fa-2x" aria-hidden="true" data-dismiss="alert"></span>
5 </div>
6<% end %>

Selesai!

Mudah kan?

Kelihatannya saja banyak. Mungkin dikesempatan yang lain akan saya sederhanakan lagi.

Untuk kali ini, seperti ini dulu.

Mudah-mudahan dapat bermanfaat buat teman-teman.

Terima kasih.

(^_^)

Referensi

  1. guides.rubyonrails.org/routing.html#controller-namespaces-and-routing
    Diakses tanggal: 2019/12/21

  2. guides.rubyonrails.org/i18n.html
    Diakses tanggal: 2019/12/21