2017年1月10日 星期二

Ajax+Devise+Foundation實作Popup註冊頁面

寫在開始之前

這次的主題是想搭配devise gem做出popup式的註冊頁面,效果可以參考Airbnb。而其中的實作又會涉及到ajax的使用,這樣的做法是希望將使用者輸入的資料透過javascript進行非同步傳送至後端,再將結果回傳至前端讓我們判斷下一步要做什麼,例如成功註冊就將會員導至首頁,註冊失敗就顯示出失敗的訊息。
看完這份教學,你會了解:
  1. 用現成的foundation gem快速產生popup頁面(gem裡面稱做Reveal)
  2. Hack原本devise gem的預設controller、方法與頁面(這部分比較麻煩ˊ ˋ)
  3. 使用Ajax傳資料、塞資料
  4. 使用Rails-i18n gem搭配model翻譯訊息
註1:devise gem很強大,他從頭到尾幫你把整套使用者認證包好了,也就是說註冊、登入都是使用他預設的頁面,成功或失敗後會做什麼事情也是他幫你預設好。這部分就是我們要去hack的地方,將頁面、行為客製化,改成符合我們需求的樣子。
註2:ajax最重要的就是資料的拿取跟傳送,當你能拿到對的資料,基本上就已經完成九成了。
註3:Rails-i18n,太重要的一個功能!能幫我們翻譯訊息成中文,對於資料驗證後的錯誤訊息顯示有很大的幫助。



    提醒事項

    1. 請先開好一個Rails專案,本範例中的專案名稱為learn_ajax,並有welcome controller & welcome/index.html.erb
    2. 在本文中寫給讀者看的註解會使用 % 符號 
    3. 如果是終端機的指令前面會加上$,輸入時只需要輸入這個符號後的程式碼

    上工啦

    1. Gemfile中加入以下,然後跑bundle
    gem 'devise'

    2. 終端機中跑devise的generator
    $ rails generate devise:install
    % 這步驟會幫你生成一些基本的設定檔


    3. 接著要產生 User model,以及在資料庫中生成欄位了
    $ rails generate devise User
    $ rake db:migrate 
    % 我們的model名稱為User

    至此,devise的初步設定就差不多了,接下來讓我們加入foundation gem。

    foundation gem

    foundation是一個響應式的前端框架,裡面幫你包好了很多功能,只要添加在程式碼中就可以快速完成某個複雜功能,例如sticky navbar、reveal、carousel、responsive design等。相似的框架如bootstrap、semantic等等。而rails生態圈有人剛好幫我們把foundation的內容包成gem,因此我們只要bundle gem就可以使用了。
    1. 在Gemfile中加入以下,並bundle
    gem 'foundation-rails'
    2. 跑以下指令以自動產生foundation相關檔案
    $ rails g foundation:install
    % 跑完後應該會有foundationandoverrides.scss & _setting.scss檔案

    3. 在 assets/stylesheets/application.css 中加入foundation的css
    *= require foundation_and_overrides
    4. 在 assets/javascripts/application.js 中加入foundation的js,並啟動

    //= require foundation
    
    $(function(){
      $(document).foundation();
    });
    % 完成了3, 4步驟後接下來就有foundation的js & css可以使用了


    turbolink gem 關閉

    turbolink為Rails預設內建的gem,可以加速頁面的render。但這個gem很容易跟其他js有衝突,讓js動不了,因此在這裡我們先停止這個功能,以避免太複雜的情形。
    1. 在 assets/javascripts/application.js 中刪除以下
    //= require turbolinks
    2. 在Gemfile中刪除以下,並bundle
    gem 'turbolinks' 

    simple_form gem


    1. 在Gemfile中加入以下,並bundle
    gem 'simple_form'
    2. 跑generator
    rails generate simple_form:install

    新增註冊popup頁面

    我們要做到以下:
    i. 在welcome/index產生註冊連結
    ii. 該連結會連至我們客製化的註冊頁面

    1. 新增 views/welcome/signup.html.erb 頁面
    <div class="reveal-modal custom-modal" id="signup" data-reveal
                                               data-close-on-click="true"
                                               data-animation-in="fade-in"
                                               data-animation-out="fade-out">
      <h2>來看看如何用ajax進行註冊</h2>
      <div class="alert_info"></div>
      <%= simple_form_for User.new, as: :user, url: user_registration_path, 
                                               html: { id: "registration_form" }, 
                                               remote: true, 
                                               format: :json do |f| %>
          <%= f.input :email %>
          <%= f.input :password %>
          <%= f.input :password_confirmation %>
          <div class="form-actions">
              <%= f.button :submit %>
          </div>
      <% end %>
    
      <button class="close-button" data-close aria-label="Close reveal" type="button">
          <span aria-hidden="true">&times;</span>
      </button>
    </div>
    % 這裡東西比較多,我們一一解釋。當中看到有reveal字樣的,都和foundation reveal有關,照著打就可以產生一個popup的頁面,有興趣dig in的可以去翻foundation的docs。
    % 最外面用 id="signup"包起來,這是等一下我們要用來呼叫這個popup的attribute。
    % alert_info區域是之後要用來顯示錯誤訊息的區塊。
    % simple form裡面是我們註冊所需要輸入的欄位。url指向註冊的路由;id: "registration_form"之後ajax會用到;remote: true是Rails內建的ajax helper,幫助我們送出ajax請求至後端;format: :json指定表單的內容以json的格式傳送。
    % 最後面加上了一個close button。
    2. 配置 welcome/index.html.erb
    <h1>Welcome to this page</h1>
    <h2>Here we'll going to learn ajax from devise.</h2>
    <h3>If you are interested in it, just learn with me :)</h3>
    
    <a href="#" id="signup-link">Ajax註冊</a>
    
    <div id="foundation-popup"></div>
    % 加上了一個開啟註冊的popup連結,連結的地方用#代替,後面會用js驅動連結;連結的id是給js呼叫用的。

    % 最下面的foundation-popup div,是之後要塞popup程式碼的地方。

    3. 在 routes.rb加上路由,完成後應如下面
    Rails.application.routes.draw do
      root 'welcome#index'
    
      devise_for :users
      get 'welcome/signup', to: 'welcome#signup'
    end
    % 這步驟是手動指定路由,指定當網址為 welcome/signup 形式時,交給welcome controller裡面的signup action來動作。

    4. 在 welcome_controller.rb 裡面加上action,完成後如下
    class WelcomeController < ApplicationController
      def index
      end
    
      def signup
        render layout: false
      end
    end
    % 加上signup這個action,運作原理是當我向welcome controller請求signup action時,signup action會執行該段的程式碼,並且render相對應的 welcome/signup.html.erb給前端。而裡面設定 render layout: false,代表我只需要render signup.html.erb裡面程式碼就好,不需要包含application.html.erb裡面的layout。

    5. 使用js驅動連結,在 javascripts/welcome.js 裡面加入
    // open foundation signup modal
    $(function() {
      $("#signup-link").click(function(event) {
        event.preventDefault();
    
        if ($("#signup").length > 0) {
          $("#signup").foundation("open");
        } else {
          $.ajax({
            url: '/welcome/signup',
            type: 'GET',
            success: function(response) {
              $("#foundation-popup").append(response)
              signup = new Foundation.Reveal($("#signup"));
              signup.open();
            },
            error: function(xhr) {
              console.log(xhr)
            }
          });
        }
      });
    });
    % 整段的js意思是監聽 #signup-link 這個連結如果有被按的話,那麼就以popup的方式顯示signup.html.erb。怎麼顯示呢?我們會利用ajax的方式去向後端拿取signup.html.erb的程式碼(else那段),首先指定拿取的網址為/welcome/signup,這會對應到前面設定的路由,將這樣的請求交給welcome controller的signup action去做回應,接著如果回應成功的話我們就用success將回應(response)拿到前端,response的內容就是signup.html.erb裡面的程式碼。再來將程式碼塞入welcome/index.html.erb的 #foundation-popup div裡面,至此,已經讓signup程式碼在首頁出現了,但還沒變成foundation reveal的物件,因此我們抓取 #signup(就是signup.html.erb最外面的div的id),並new成一個reveal的物件,接著再開啟這個物件,就可以看到popup視窗出現了。

    % 上面會看到開啟的方式有兩個,一個是前面提到的ajax拿取資料並開啟,而另一個則是當已經開啟過一次signup之後,signup的程式碼就已經存在首頁了,因此不需要再向後端拿取,只要將他開啟就好。

    當以上都完成後,去點連結應該就會出現註冊的popup。接著要來實作註冊表單輸入後的相對應動作。


    表單輸入後資料的處理

    前面已經將表單設為remote,以ajax的方式將資料送到後端,格式為json。但原本devise已經將registrations_controller整包都寫好了,算是一個黑盒子,而這個黑盒子裡面的程式碼並不完全符合我們的需求,因此我們必須override這個controller,讓他可以依我們的需求回應json格式的資料到前端。

    1. 新增 controllers/registrations_controller.rb,並做修改

    原本devise的 registrations_controller.rb 裡面的create action程式碼如下
    class Devise::RegistrationsController < DeviseController
      def create
        build_resource(sign_up_params)
    
        resource.save
        yield resource if block_given?
        if resource.persisted?
          if resource.active_for_authentication?
            set_flash_message! :notice, :signed_up
            sign_up(resource_name, resource)
            respond_with resource, location: after_sign_up_path_for(resource)
          else
            set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
            expire_data_after_sign_in!
            respond_with resource, location: after_inactive_sign_up_path_for(resource)
          end
        else
          clean_up_passwords resource
          set_minimum_password_length
          respond_with resource
        end
      end
    end
    override的registrations_controller.rb如下

    class RegistrationsController < Devise::RegistrationsController
      # Inherited from Devise::RegistrationsController.
      # For overwriting the respond format from html to json through ajax.
    
      def create
        build_resource(sign_up_params)
    
        if resource.save
          if resource.active_for_authentication?
            set_flash_message :notice, :signed_up if is_navigational_format?
            sign_up(resource_name, resource)
    
            respond_to do |format|
              format.json { render json: {}, status: :ok }
              format.html { respond_with resource, location: after_sign_up_path_for(resource) }
            end
          else
            set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_navigational_format?
            expire_session_data_after_sign_in!
    
            respond_to do |format|
              format.json { render json: {}, status: :ok }
              format.html { respond_with resource, location: after_sign_up_path_for(resource) }
            end
          end
        else
          clean_up_passwords resource
    
          respond_to do |format|
            format.json { render json: resource.errors.full_messages, status: :unprocessable_entity }
            format.html { respond_with resource }
          end
        end
      end
    end
    % 只override create action,因為我們需要客製化的是送出表單、create user完後的動作。這邊的code還沒refactor,大家有閒暇時間可以試試看。
    % 如果user成功儲存的話就回應format.json,裡面回傳一個空的hash&200的status code。如果儲存失敗的話就回應出錯的訊息,errors.full_messages 是Rails ActiveModel預設的方法,會顯示出欄位驗證時的錯誤訊息。

    2. 修改routes.rb,讓程式吃我們客製化的registrations controller,修改完應如以下
    Rails.application.routes.draw do
      root 'welcome#index'
    
      devise_for :users, controllers: { registrations: "registrations" }
      get 'welcome/signup', to: 'welcome#signup'
    end
    3. 接著要接收從後端傳回來的訊息,我們在 welcome.js 裡寫進以下
    // Show signup error messages in popup modal
    $(document).on("submit", "#registration_form", function(e) {
      $("input[type=submit]").prop("disabled", true);
    }).on("ajax:success", "#registration_form", function(e, data, status, xhr) {
      $("input[type=submit]").prop("disabled", false);
      $("#signup").foundation("close");
      location.reload();
    }).on("ajax:error", "#registration_form", function(e, data, status, xhr) {
      $("input[type=submit]").prop("disabled", false);
      $(".alert_info").empty();
    
      for (var key in data.responseJSON) {
        $(".alert_info").append("<p>"+data.responseJSON[key]+"</p>");
        $(".alert_info").css("background-color", "#F88E8B");
      }
    });
    % 這裡用了另外一種方法實作ajax,大家只要先照做即可。我們監聽註冊表單submit的這個動作,當表單送出去後就會由前面override的registrations controller進行處理,接著如果成功儲存user時回傳的json資料就會由"ajax:success"處理,將popup關閉並進行頁面重整。如果user儲存失敗就會由"ajax:error"處理,而這邊會把錯誤的訊息印出來,並塞入popup的 ".alert-info"裡面。

    至此,基本上整套註冊流程就完成了。從按註冊連結會跳出註冊的popup視窗,可以在表單裡輸入資料,按下submit按鍵後資料會以ajax的方式傳送到後端驗證並儲存,如果成功就關掉popup並重整頁面,如果失敗就將失敗的錯誤訊息印出來,讓使用者可以修正。所以大家可以試試看故意不輸入email或是密碼不相同之類,測試能不能印出正確的錯誤的訊息。但你應該會發現印出來的錯誤是「英文版」的,沒錯,因為我們還沒設定i18n這步驟,因此接下來我們要將訊息轉化成中文。


    加入i18n


    1. 在Gemfile中加入 rails_i18n gem並bundle
    gem 'rails-i18n'
    2. 修改 config/application.rb ,設定zh-TW為預設語言,完成後應如以下
    module LearnAjax
      class Application < Rails::Application
        # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
        config.i18n.default_locale = "zh-TW"
    
        # Do not swallow errors in after_commit/after_rollback callbacks.
        config.active_record.raise_in_transactional_callbacks = true
      end
    end
    3. 新增 config/locales/user.zh-TW.yml
    zh-TW:
      activerecord:
        attributes:
          user:
            email: "E-mail"
            password: "密碼"
            password_confirmation: "密碼確認"
    % 這個檔案會讓程式知道user model的幾個欄位相對應的中文是什麼,當在組合錯誤訊息時就會把欄位翻譯成中文。

    4. 新增 config/locales/devise.zh-TW.yml 並到這裡複製中文的devise訊息,並貼上存檔。
    % 這個檔案會讓devise的訊息都轉換為中文。之後註冊過程中如果有錯誤訊息的話就會進行翻譯並組合,可能得到的錯誤會像是:密碼 不能是空白字元


    完結

    至此已經完成了一套完整的popup註冊流程,從一開始讓welcome的註冊連結能夠叫出popup註冊頁面;再來註冊表單能以ajax的方式與後端溝通,並將結果回傳回頁面;最後翻譯錯誤訊息。接著登入的頁面也可以如法炮製,就留給大家當作功課了!如果有任何問題都歡迎留言詢問,或是透過其他方法找到我問哈哈,會盡可能把我會的分享出來。


    參考資料

    1. https://www.airbnb.com.tw/
    2. https://github.com/plataformatec/devise
    3. https://github.com/zurb/foundation-rails/tree/master/vendor/assets
    4. http://api.jquery.com/jquery.ajax/
    5. https://github.com/plataformatec/simple_form







    7 則留言 :