Navigation Bar Global Menu Preferences pada Rails
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. Navigation Bar dengan Menu Language dan Currency Preferences
Nah, sudah sedikit terbayang kan.
Jadi, dropdown menu tersebut mempunyai fungsi seperti ini:
- Dapat mengganti language dan currency di setiap halaman.
- Berfungsi pada user maupun guest
- 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
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.
1class Users::LocalesController < ApplicationController2 before_action :authenticate_user!34 def update5 if current_user.update(locale_param)6 redirect_to request.referer, notice: t('navbar.locale_updated')7 else8 redirect_to request.referer, notice: t('navbar.locale_failed_updated')9 end10 end1112 private1314 def locale_param15 params.permit(:locale)16 end17end
1class Users::RatesController < ApplicationController2 before_action :authenticate_user!34 def update5 if current_user.update(rate_param)6 redirect_back request.referer, notice: t('navbar.rate_updated')7 else8 redirect_back request.referer, notice: t('navbar.rate_failed_updated')9 end10 end1112 private1314 def rate_param15 params.permit(:rate)16 end17end
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
.
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
.
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
.
1# Where the I18n library should search for translation files2I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]34# Permitted locales available for the application5I18n.available_locales = ['en', 'ch']67# Set default locale to something other than :en8I18n.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.
1class ApplicationController < ActionController::Base2 before_action :set_rate3 around_action :set_locale45 private67 def set_rate8 @rate = params[:rate]9 end1011 def set_locale(&action)12 locale = current_user.try(:locale) || params[:locale] || I18n.default_locale13 I18n.with_locale(locale, &action)14 end1516 def default_url_options17 current_user ? {locale: I18n.locale} : {locale: I18n.locale, rate: @rate}18 end19end
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.
1Rails.application.routes.draw do2 scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do3 # ...4 # ...56 namespace :users do7 # ...8 # ...9 resources :rates, only: %w[update]10 resources :locales, only: %w[update]11 end12 end1314 namespace :admins do15 # ...16 # ...17 end18end
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
.
1<!DOCTYPE html>2<html>3 <head>4 <title>BANDITHIJO.COM</title>5 <%= csrf_meta_tags %>6 <%= csp_meta_tag %>78 <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>1213 <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.
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>1415 <!-- Dropdown Language Icon -->16 <li class="nav-item d-flex dropdown">1718 <!-- 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 ENG24 <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 CNY30 <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 ENG39 <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 CNY45 <span class="icon-dropdown"></span>46 </a>47 <% end %>48 <% end %>49 <!-- END Dropdown Language Icon -->5051 <!-- Content of Dropdown List Language and Currency -->52 <div class="dropdown-menu border-0 rounded">53 <div class="row no-gutters">5455 <!-- Language Preferences -->56 <%= render 'layouts/users/navbar/locale' %>57 <!-- END Language Preferences -->5859 <!-- Currency Preferences -->60 <%= render 'layouts/users/navbar/rate' %>61 <!-- END Currency Preferences -->6263 </div>64 </div>65 </li>66 <!-- END Dropdown Language Icon -->6768 <!-- Dropdown User Menu -->69 ...70 ...7172 </ul>73 </div>74 <!-- END Right Menu -->75</nav>
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 English14 </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 Chinese25 </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 English32 <% 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 Chinese36 <% end %>37 <% end %>38</div>
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 MYR12 <span class="text-normal">13 Malaysian Ringgit14 </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 USD23 <span class="text-normal">24 US Dollar25 </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 RMB34 <span class="text-normal">35 Chinese Yuan36 </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 MYR42 <span class="text-normal">43 Malaysian Ringgit44 </span>45 <% end %>46 <%= link_to({rate: "USD"}, class: "dropdown-item #{params[:rate] == 'USD' ? 'active' : ''}") do %>47 USD48 <span class="text-normal">49 US Dollar50 </span>51 <% end %>52 <%= link_to({rate: "CNY"}, class: "dropdown-item #{params[:rate] == 'CNY' ? 'active' : ''}") do %>53 RMB54 <span class="text-normal">55 Chinese Yuan56 </span>57 <% end %>58 <% end %>59</div>
Selanjutnya, isi dari file _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
-
guides.rubyonrails.org/routing.html#controller-namespaces-and-routing
Diakses tanggal: 2019/12/21 -
guides.rubyonrails.org/i18n.html
Diakses tanggal: 2019/12/21