[Rails]i18n基本使用

首先安裝rails-i18n

Gemfile
gem "rails-i18n"

$ bundle install

我們所有翻譯檔案都會在config/locals目錄下,都是為.yml的檔案,而檔名不重要,純粹是好分辨而已,這邊就創鈔兩個檔案:
en.yml代表英文翻譯
zh-TW.yml代表中文翻譯

自定義的話例如中文翻譯來說:

zh-TW:

  welcome: "嗨世界!#{username}"

  sport:

    ball: 籃球

而英文翻譯:

en:

  welcome: "Hello world! #{username}"

  sport:

    ball: basketball

使用方法如下:

example.html.erb
t("sport.ball")
t(:sport, :scope => :ball )
t(:welcome, :username => "Kurt")

在application_controller.rb設定如下:

application_controller.rb
before_action :set_locale

def set_locale
  # 可以將 ["en", "zh-TW"] 設定為 VALID_LANG 放到 config/environment.rb 
  if params[:locale] && I18n.available_locales.include?( params[:locale].to_sym )
    session[:locale] = params[:locale]
  end

  I18n.locale = session[:locale] || I18n.default_locale
end

在view中切換版本:

example.html.erb
<%= link_to "中文版", :controller => controller_name, :action => action_name, :locale => "zh-TW" %>
<%= link_to "English", :controller => controller_name, :action => action_name, :locale => "en" %>

[Rails]專案引入vue & 利用Attribute給 vue 資料

我的開發環境:
Rails 5.1以上

引入webpacker和vue


先安裝web packer

Gemfile
gem 'webpacker', '~> 3.5'

$ bundle install
$ bundle exec rails webpacker:install

可以查看webpacker帶有什麼指令:
$ rails webpacker

比起npm我比較喜歡用yarn:
$ bundle exec webpacker:yarn_install
$ yarn install

有個指令可以直接安裝vue:
$ bundle exec webpacker:install:vue

利用Attribute給 vue 資料


先在contoller設定變數:

products_controller.rb
def index
  @products = Product.all
end

再到view裡面把資料帶入DOM:

index.html.erb
<% props = {
  products: @products
  }.to_json
%>

<div id="app" data="<%= props %>"></div>

接下來到vue的檔案application:

app/javascript/packs/application.js
import Vue from 'vue'
import App from '../app.vue'

document.addEventListener('DOMContentLoaded', () => {
  const el = "#app"

  const props = JSON.parse($("#app")[0].getAttribute('data'))
  const app = new Vue({
    el: el,
    render: h => h(App, { props })
  })
})

最後到vue 的component:

app/javascript/app.vue
<script>
export default {
  props: ["products"],
  data: function () {
    return {
      message: "Hello Vue!"
    }
  },
  mounted: function () {
    console.log("products", products)
  }
}
</script>

mounted是vue生成完之後會執行的動做,可以參考vue lifecycle
如果有console出東西就ok嘍!!

這個方式的優點十分明顯,可以省去製作API和串接的麻煩,但就是比較不彈性,真正要做到完全的前後端分離建議還是分成兩個專案,Rails寫API而Vue去做前端實現。

參考文章:
Passing Props to Vue in a Rails View

[Rails]rails 搭配 vue-router(目前無法指定vue-routes)

參考文件:
webpacker

Rails版本5.1以上可以直接創造帶有webpack的新專案
$ rails new rails-vue-todolist --skip-turbolinks --skip-spring --webpack=vue
$ cd rails-vue-todolist

先快速建立todolist
$ rails g scaffold Todolist item:string
$ bundle install

設定router

routes.rb
root "todolists#index"

測試有沒有成功

app/views/layout/application.html.erb
<%= javascript_pack_tag 'hello_vue' %>

更新todolist/index

app/views/todolists/index.html.erb
<div id="hello"></div>
...

$ rails s
到首頁刷新頁面應該就會有了,如果有出現:
You may need an appropriate loader to handle this file type.
可以嘗試下下面三個指令
$ bundle update webpacker
$ yarn upgrade @rails/webpacker
$ bundle exec rails webpacker:install:vue

大多會是版本上的問題,如果直接用rails5.1以上應該沒有問題,如果是用其他版本或者是專案升級情況可能要再去查詢下官方log文件

再來如果玩過Vue.js的人有一種開發模式是把一個id="app"當作入口,所以我們會有一個主要的main.js和主要生成vue的app.vue,此時app/javascript/packs/application.js可以把它當作main.js,再來怎麼規劃檔案就都可以了。

我們循著上面的作法先改rails view剛剛的application.html.erb的tag

app/views/layout/application.html.erb
# 把原本的 <%= javascript_pack_tag 'hello_vue' %> 替換如下


<%= javascript_pack_tag 'application' %>

修改index入口點只留下一行當作vue的入口就好

app/views/todolists/index.html.erb
<div id="app"></div>

修改packs/application.js

app/javascript/packs/application.js
import Vue from 'vue'
import App from '../app.vue'

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: "#app",
    render: h => h(App)
  })
})

現在再重新刷新一次頁面如果有render出Hello Vue!恭喜已經設定好所有一切了!

初創index


接下來我們先直接在app.vue裡面創造假資料做出index畫面:

app/javascript/app.vue
<template>
  <div>
    <h1>Todo Lists</h1>
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>Item</th>
        </tr>
      </thead>

      <tbody>
        <tr v-for="todo in list" >
          <td>{{ todo.id }}</td>
          <td>{{ todo.item }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      list: [
        { id: 1, item: "Foo" },
        { id: 2, item: "Bar" }
      ]
    }
  }
}
</script>

應該會list出兩個item出來,但我們要去拿rails裡todolists#index的資料,首先我們在rails console新增兩筆:
$ rails c
$ Todolist.create(item: "Test1")
$ Todolist.create(item: "Test2")
$ exit

然後如何從vue的methods裡面拿取資料,可以用vue-resource這個套件。
$ yarn add vue-resource

然後import在app/javascript/application.js裡:

app/javascript/application.js
import VueResource from 'vue-resource'
Vue.use(VueResource);

現在修改app.vue

app/javascript/app.vue
<template>
  <div>
    <h1>Todo Lists</h1>
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>Item</th>
        </tr>
      </thead>

      <tbody>
        <tr v-for="todo in list" >
          <td>{{ todo.id }}</td>
          <td>{{ todo.item }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      list: []
    }
  },

  created: function() {
   this.fetchTodoLists();
  },

  methods: {
    fetchTodoLists: function() {
       const resource = this.$resource('/todolists.json');
       resource.get()
        .then(response => {
           this.list = response.data
        });
    }
  }
}
</script>

應該會出現剛剛在console創的兩筆資料。

component裡面寫css


webpacker官方文件裡面有規劃資料夾,雖然敘述說會有放js css images的地方但我初創的時候只有放js的資料夾,他的架構如下:
app/javascript:
├── packs:
│ # only webpack entry files here
│ └── application.js
└── src:
│ └── application.css
└── images:
└── logo.svg

照著他的架構創
$ mkdir app/javascript/src
$ touch app/javascript/src/application.css

然後在原本rails的application.html.erb加上連結

app/views/layouts/application.html.erb
<%= stylesheet_pack_tag 'application' %>

就可以在每個.vue檔的component裡面寫css了,例如:

component.vue
<style lang="scss">
  .link {
    color: blue;
    cursor: pointer;
  }
</style>

引入vue-router


再來跟全端開發一樣,rails的gem歸rails的Gemfile,而webpack的歸package.json(Vue也是在這裡面,只不過rails5.1連著專案一起創webpack的話已經內建Vue了!)。

現在安裝vue-router
$ yarn add vue-router

先把router寫在一個js裡面:
$ touch app/javascript/packs/router.js
建立routes

app/javascript/packs/router.js
import Vue from 'vue'

import VueRouter from 'vue-router'
Vue.use(VueRouter)

import todolists_index from './../views/todolist/index.vue'

const router = new VueRouter({
    // 使用 HTML 5 模式

    mode: 'history',
    routes: [
        { path: '/todolists', component: todolists_index },
        { path: '/', redirect: '/todolists' }
    ]
})

export default router

檔名或變數不要取routes避免產生match error

修改packs/application.js把

app/javascript/packs/application.js
import Vue from 'vue'
import App from '../app.vue'

import VueResource from 'vue-resource'
Vue.use(VueResource);

import VueRouter from 'vue-router'
Vue.use(VueRouter)
import router from './router.js'

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: "#app",
    router,
    render: h => h(App)
  })
})

現在創造專給vue的view的資料夾
$ mkdir app/javascript/views
再給todolist一個歸類
$ mkdir app/javascript/views/todolist
最後先做index檔案,根本來的app.vue一模一樣
$ cp app/javascript/app.vue app/javascript/views/todolist/index.vue

最後app.vue簡化如下:

app/javascript/app.vue
<template>
  <div>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

就成功import vue-router了!這樣也算是做出前後端分離的概念了,rails去吐api,而vue就專門做前端的東西吧!

但目前遇到的問題是重整頁面會去讀取rilas routes,要怎麼設定待研究

[Rails]RSpec101

純粹ruby測試


安裝在本機
$ gem install rspec
會安裝5~6個東西

接著在資料夾裡下
$ rspec --init
就可以開始玩了

Rails專案裡實作


Gemfile
group :development, :test do
  gem 'byebug', platform: :mri
  gem 'rspec-rails', '~> 3.5.2'
  gem 'rails-controller-testing'
end

$ bundle install
$ bin/rails generate rspec:install
$ rspec
應該可以運作,terminal出現訊息如下:

No examples found.
Finished in 0.0.00028 seconds (files took 0.08784 seconds to load)
0 examples, 0 failures

就代表成功,可以參考Github上的專案RSpec101的CRUD-test分支做出簡單的CRUD test。

[Rails]will_paginate 做分頁

參考官方文件:
will_paginate

安裝:

Gemfile
gem 'will_paginate', '~> 3.1.0'

$ bundle install
$ rails s

在controller裡面加入:

post_controller.rb
Post.paginate(:page => params[:page], :per_page => 30)

per_page是一頁顯示幾筆

當然也可以搭配任何條件:

post_controller.rb
Post.order("created_at DESC").paginate(:page => params[:page], :per_page => 30)

這樣就可以把最新的Post顯示在最前面

最後在view:

post.html.erb
<%= will_paginate @posts %>

大功告成又很簡單~至於css的話也可以參考官方的:
make those pagination links prettier

[Rails]Ransack in header for global search

基本的使用可以先參考這些資料:
ransack官方文件
[Rails]ransack最基本使用

如果照著[Rails]ransack最基本使用做出來之後把它放在header跳到其他頁面會報錯,因為他會找不到@q這個變數,但要在每個controller加上@q又不切實際,最好的辦法就是把它獨立出來吧!

雖然他是一個form表單,但還是用get比較好,post固然也會運作,但如果原地重整頁面的話就會爆炸嘍!

所以我們先做一個router給他

routes.rb
get "search", :to => "search#index"

做一個controller
$ rails g controller search

設定如下:

search_controller.rb
class SearchController < ApplicationController
  def index
    @q = Model.ransack(params[:q])
    @models = @q.result(distinct: true)
  end
end

再來view就是action到這個router就好:

search.html.erb
<%= search_form_for @q, url: search_path do |f| %>
  <%= f.search_field :title_cont %>
  <%= f.submit "搜尋" %>
<% end %>

這樣其實是創立一個獨立的頁面,也就是說如果本來就有model#index的頁面其實search#index的頁面會是獨立且一模一樣的喲!

但此時我們發現只要她去別的頁面會因為別的controller裡面沒有@q這個變數爆error,我們來設訂一個全域變數吧:

search_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_ransack_argument

  def set_ransack_argument
    @q = Model.ransack(params[:q])
  end
end

這樣的效果就可以讓整個網站都可以使用了,那我們也可以把剛剛search_controller.rb的@q = Model.ransack(params[:q])這行拿掉!

[Rails]操作遠端的 rake 和 console

原本只是想利用capistrano-rails-db 這個套件輕鬆在本地端下指令就好,但rails5有新增一個保護機制,只要drop或者reset會被擋住,只要不要有動到就資料的動作其他其實也蠻正常的,像是cap deploy:db:seed之類的。

是什麼保護機制可以參考這篇:
防止Production数据库被意外清空 | Rails 5

情況:
我是用Capistrano部署在機台上的專案web server是Nginx

如何執行rake


其實直接進到機台操作還是最簡單直接的,首先進到專案裡的current資料夾

如果要reset或drop db的話請先關閉web server:
$ service nginx stop

再來基本上都可以動rake db的指令了,例如reset:
$ RAILS_ENV=production bin/rake db:reset DISABLE_DATABASE_ENVIRONMENT_CHECK=1
DISABLE_DATABASE_ENVIRONMENT_CHECK就是rails5的保護機制,可以參考上面那篇。

都控制玩db之後重新開啟即可:
$ service nginx start

如何進入console


一樣到專案裡的current資料夾
$ bundle exec rails c production
就可以嘍!

[Rails]Capistrano run seed with remote & error File exists

參考文件:
Rails 部署 Capistrano 如何使用seed档
capistrano-rails-db

使用


假設已經使用了Capistrano部署完,如沒有參考:
[Rails] Capistrano 部署rails專案到 linode

再來開始使用套件

Gemfile
gem 'capistrano',  '~> 3.1'
gem 'capistrano-rails', '~> 1.1'
gem 'capistrano-rails-db'
Capfile
require 'capistrano/rails'
require 'capistrano/rails/db'

官方文件上是寫cap deploy:db:seed,我使用如下:
$ cap production deploy:db:seed
就可以了,其他指令應該依樣畫葫蘆就好。

Error


我的seed檔有包含創造圖片,使用paperclip這個套件,因為之前設定deploy.rb檔錯誤發生error,敘述如下:

我把

deploy.rb
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system"

設定成

deploy.rb
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system/000/000"

並且$ cap production deploy

造成run $ cap production deploy:db:seed的時候release出錯:
01 Errno::EEXIST: File exists @ dir_s_mkdir - /home/deploy/movie_review/releases/20180506145631/public/system/movies/images/000/000

所以要手動到機台上面進去這個資料夾把/home/deploy/movie_review/releases/20180506145631/public/system/movies/images/000/
刪除即可

[Rails]form_for preview image with javascript

原理是利用Javascript的FileReader製作一個DOM,再利用CSS把舊的圖片藏起來。

view.html.erb
<%= form_for @profile do |f| %>
  <div class="form-group" style="width: 300px;">
    <%= f.file_field :image, required: true, accept:'image/png,image/jpeg', id: :image %>
  </div>

  <div id="image_preview"></div>

  <% if @profile.image.present? %>
    <%= image_tag @profile.image.url(:medium), :id => "old_image" %>
  <% end %>

  <div class="form-group" style="width: 300px;">
    <%= f.submit "上傳", class: "btn btn-lg btn-primary" %>
  </div>
<% end %>

<script type="text/javascript">
$(function() {
  $('#image').on('change', function(event) {
    let files = event.target.files;
    let image = files[0]
    let reader = new FileReader();
    reader.onload = function(file) {
      let img = new Image(400, 600);
      img.src = file.target.result;
      $('#image_preview').html(img);
      $('#old_image').css("display", "none");
    }
    reader.readAsDataURL(image);
  });
});
</script>

1.上傳圖片後會觸發change這個function
2.event.target可以拿到id="image"這個tag
3.event.target.file[0]則拿到這個檔案的本體File 格式的物件
4.創造一個FileReader
5.註冊一個FileReader的onload事件,只要拿FileReader做事情就會觸發onload事件
6.用readAsDataURL讀取File本體,完成後 result 將以 data: URL 格式(base64 編碼)的字串來表示讀入的資料內容
7.觸發的onload變數file是剛剛readAsDataURL讀取出來的東西
8.創造一個Image DOM 尺寸是400px * 600px
9.file.target.result是剛剛readAsDataURL讀取File本體回傳的data: URL 格式
10.利用html()把它放進 $('#image_preview')裡面
11.css把舊的$('#old_image')藏起來

<%= image_tag @profile.image.url(:medium), :id => "old_image" %>是用paperclip這個套件做的

小補充:
Blob是一個檔案(原始資料)的不可變物件,剛剛的File 格式的物件其實也是一種特殊的Blob,所以也可以用在任何接受 Blob 物件的地方,例如FileReader。

參考文件:
How to preview uploaded image instantly with paperclip in ruby on rails
MDN FileReader
MDN Blob
MDN Image()

[Rails]form_for select簡易使用

先創造一個最基本的select

view.html.erb
<%= form_for @profile do |f| %>
  <%= f.select(:age, options_for_select(["6+", "12+", "15+", "18+"]), {}, { :class => ’style'}) %>
<% end %>

我們建議把options寫在model:

Profile.rb
AGE_CLASS = ["6+", "12+", "15+", "18+"].freeze

常數全部大寫是個好習慣
如此簡單的字串array我們也可以改寫為:

Profile.rb
AGE_CLASS = %w(6+ 12+ 15+ 18+).freeze

把options放到view上:

view.html.erb
<%= form_for @profile do |f| %>
  <%= f.select(:age, options_for_select(Profile::AGE_CLASS), {}, { :class => ’style'}) %>
<% end %>

如果是想用key, value的話options改為:

Profile.rb
AGE_CLASS = [["普遍級","6+"], ["保護級","12+"], ["輔導級","15+"], ["限制級","18+"]].freeze

如果想要類似placeholder的空值的話利用include_blank

view.html.erb
<%= form_for @profile do |f| %>
  <%= f.select(:age, options_for_select(Profile::AGE_CLASS), {:include_blank => "請選擇"}, { :class => ’style'}) %>
<% end %>

但這樣似乎會讓edit帶入不了表單之前的值,所以我們用預設值selected

view.html.erb
<%= form_for @profile do |f| %>
  <%= f.select(:age, options_for_select(Profile::AGE_CLASS, :selected => @profile.age), {:include_blank => "請選擇"}, { :class => ’style'}) %>
<% end %>

這樣如果像是新增或者是先前表單的值並不在options裡面則會跳回include_blank

如果想用html先驗證必填的話加入required

view.html.erb
<%= form_for @profile do |f| %>
  <%= f.select(:age, options_for_select(Profile::AGE_CLASS, :selected => @profile.age), {:include_blank => "請選擇"}, { :class => ’style', :required => true}) %>
<% end %>

如果想讓某個options不能選擇的話加入disabled

view.html.erb
<%= form_for @profile do |f| %>
  <%= f.select(:age, options_for_select(Profile::AGE_CLASS, :selected => @profile.age, :disabled => "18+"), {:include_blank => "請選擇"}, { :class => ’style', :required => true}) %>
<% end %>

甚至可以在裡面寫ruby code,但建議邏輯判斷不要寫在view裡就是了,可以善用helper!