PJCHENder 未整理筆記

[Rails] AJAX 小技巧

2018-01-22

[Rails] AJAX 小技巧

@(Ruby on Rails)[rails, railsTips, ajax, JavaScript]

[TOC]

使用 AJAX 注意事項

  • 如果是透過 props 把資料代到 Vue 裡面使用,那麼最好在送「建立」或「儲存」的時,可以重新轉譯整個 partial view,避免卡一些髒髒的內容。
  • 如果「建立」或「儲存」是透過 AJAX 的方式送出表單,最好鎖上 data-disable="true"data-disable_with = "儲存中" 避免表單重複送出。

方法一:在 Rails 本身中使用 AJAX(回傳的是 js 檔)

**優點:**這麼做的好處在於,不用自己透過 AJAX 的方式把資料透過 JSON 送出去,而是可以直接用 Rails 在 <form> 中慣例的 name。透過 js.erb 來執行送出後的結果。適合用在表單送出後不用使用回傳的資料重新轉譯太多複雜的頁面時。

在 Rails 中使用 AJAX 時,需要在原本的 form 表單中加上 remote: true

:exclamation:用 remote-true 的話,不能使用 querySelector('from').submit(),要使用 Rails.fire(<formElement>, 'submit')

在 form 當中加上 remote: true

1
2
3
<!-- app/views/subdomains/_form.html.erb -->
<%= form_for :subdomain, remote: true, html: { novalidate: true, id: 'needs-validation' } do |f| %>
<% end %>

如果是使用在 link_to 一樣是使用 remote: true

1
<%= link_to 'foobar', url_path, method: :post, remote: true %>

在 Controller 中指定 format.js

1
2
3
4
respond_to do |format|
format.html { render :index }
format.js # ★★★★★
end

在 View 中要加上一支 <action-name>.js.erb 的檔案來透過 JS 處理回傳的結果:

1
// app/views/subdomains/create.js.erb

之所以稱為 Unobtrusive JavaScript 是因為我們不把 JavaScript 混在 HTML 中(inline-javascript)。

如果不是要回傳預設的 action-name.js.erb

在 Controller 中可以使用 render 指定要 render 的 js 是哪一支:

1
2
3
4
5
6
7
# app/controllers/members_controller.rb

# 預設會回傳 create.js.erb,透過 render 可以選擇回傳的 js 檔,這裡是 member/update.js.erb
def create
flash.now[ :notice] = I18n.t('common.remove_successfully')
render 'members/update.js.erb'
end

範例程式碼(回傳 JS)

controller 加上 format.js

1
2
3
4
5
6
7
8
9
10
11
12
# ./app/controllers/products_controller.rb

class ProductsController < ApplicationController
def index
@products = Product.all

respond_to do |format|
format.html {render :index}
format.js # ★★★★★
end
end
end

view 加上 remote: true

1
2
3
4
<!-- ./app/views/products/index.html.erb -->

<!-- ★★★★★ 記得 remote: true -->
<%= link_to 'Refresh with JS', products_path, remote: true %>

JS 中可以使用 Rails view helper

keywords: escape_javascript

這支檔案就和一般的 foo.html.erb 可以使用 Rails View Helper,並且可以取得 controller 中的變數

注意:controller 中的變數如果要直接使用,而不是透過 local_variable 的方式傳到 view 時,要加上 “@”(例如 @foo = '213'

如果是要直接把一整個包含 JS 的 partial view 換掉,可以使用 <%= escape_javascript(inner_html) %> ,如此該 partial 中的 js 才能正確載入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- ./app/views/products/index.js.erb  -->

console.log('All product', '<%= @products.to_json %>')

<%
inner_html =
if notice
render 'shared/flash', type: 'success', message: notice
elsif alert
render 'shared/flash', type: 'alert', message: alert
end
%>

document.querySelector('#flash-message').innerHTML = "<%= escape_javascript(inner_html) %>"

注意:escape_javascript() 裡面放的 html 中若有 JavaScript 是不會被執行到的。

如果要回傳的 js 檔在不同資料夾的話

1
2
3
4
5
# app/controllers/products_controller.rb

def send_js
render 'blogs/index.js' # 會回傳 app/views/blogs/index.js.erb 這隻檔案
end

remote true 的原理

上述 :remote => true 背後的原理,如同以下的 jQuery 程式碼:

1
2
3
4
5
6
7
$("#ajaxscript").click(function(e){
e.preventDefault();
var url = $(this).attr("href");
$.ajax(url, {
dataType: "script" // 希望 server 回傳的資料類型
})
})

ihower AJAX 應用程式

方法二:透過 AJAX 回傳 JSON 檔案

Send Request To Server (FormData)

如果要用 jQuery formData 的方式傳送資料資料,在設定的時候需要註明 contentType,同時 data 需要經過 JSON.stringify()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$.ajax({
url: `${API_URL}/pre_survey`,
type: 'POST',
dataType: 'json', // 希望 server 回傳的資料類型
contentType: 'application/json', // 指定傳送到 server 的資料類型
data: JSON.stringify(preSurvey),
})
.then(function (data) {
console.log('response', data);
})
.fail(function (xhr) {
console.error(xhr);
})
.always(function () {
_vm.frozenIndex = 0;
});

Send Request To Server (JSON)

keywords: data-type

在 View 的部分把需要送出 AJAX Request 的元件使用:

  • remote: true:使用 Rails AJAX 模式
  • data: {type: 'json'}:使用 data-type="json" 可以改變 Request 的 Accept Header,這裏只要回傳 JSON 格式。
1
2
3
<!-- ./app/views/products/index.html.erb -->

<%= link_to '透過 AJAX 回傳', products_path, id: 'send-ajax', remote: true, data: {type: :json} %>

並且綁定 AJAX 事件,Rails 中基於安全性的考量,在傳送請求到伺服器時,必須帶上特定的 token:

  • 確定在 layout 中有放入 <%= csrf_meta_tag %>
  • 在 jQuery AJAX 請求的 beforeSend 中加入 header
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ./app/views/products/index.html.erb
$(function () {
$('#send-ajax').on('click', function (e) {
e.preventDefault();

$.ajax({
beforeSend: function (xhr) {
xhr.setRequestHeader(
'X-CSRF-Token',
$('meta[name="csrf-token"]').attr('content')
);
},
url: $(this).attr('href'), // get the link of <a>
dataType: 'json', // 希望 server 回傳的資料類型
processData: false,
cache: false,
contentType: false, // 指定傳送到 server 的資料類型
data: {
// data send to backend
status: 'send request ',
},
})
.done(function (response) {
console.log('success', response);
})
.fail(function (jqXHR, textStatus, errorThrown) {
console.log('fail', textStatus, errorThrown);
});
});
});

如果希望在所有發出的 request 請求中都代入 token,可以使用:

1
2
3
4
5
$.ajaxSetup({
headers: {
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'),
},
});

WARNING: Can’t verify CSRF token authenticity rails @ StackOverflow

Send JSON Response From Server

如果這個路由只是要回傳 json 可以使用 render :json

1
2
3
4
5
6
7
def update_images
msg = {
status: 'get data',
products: @products.to_json
}
render json: msg
end

How to send simple json response in Rails? @ StackOverflow

如果這個路由同時處理多個請求,可以使用 respond_to ,但在 view 的地方記得加上 data: {type: :json}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ./app/controllers/products_controller.rb
# GET /products
# GET /products.json
def index
@products = Product.all

msg = {
status: 'get data',
products: @products.to_json
}

respond_to do |format|
format.html { render :index }
format.js
format.json { render json: msg }
end
end

Sending Files to Server Through AJAX

透過下面的方式,可以將欲上傳的檔案 append 到 FormData() 上:

1
2
3
4
// form.append("<formFieldName>", <file>)

let form = new FormData();
form.append('product[photos][]', e.target.files[i]);

透過 jQuery 裡的 data 可以把資料掛上:

1
2
3
4
$.ajax({
processData: false,
data: form,
});

要完整的程式範例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let currFiles = e.target.files;

let form = new FormData();
for (let i = 0; i < currFiles.length; i++) {
// check file type
let imageType = /^image\//;
if (!imageType.test(currFiles[i].type)) {
continue;
}

// append photos into form
form.append('product[photos][]', e.target.files[i]);
}

$.ajax({
type: 'PUT',
beforeSend: function (xhr) {
xhr.setRequestHeader(
'X-CSRF-Token',
$('meta[name="csrf-token"]').attr('content')
);
},
url: this.endpoint + '/update_images',
dataType: 'json', // 希望 server 回傳的資料類型
processData: false,
cache: false,
contentType: false, // 指定傳送到 server 的資料類型
data: form,
})
.done(function (data) {
e.target.value = '';
vm.imageGalleryInit(vm, data.photos);
vm.showMask(false);
})
.fail((jqXHR, textStatus, errorThrown) => {
console.log('fail', textStatus, errorThrown);
});

其他

在 Rails 中的表單代入 Token (Optional)

如果碰到表單沒有自己附加上 token 的情況,可以使用下述語法:

1
2
3
4
5
6
7
8
9
var forms = document.querySelectorAll('form');
var token = $('meta[name="csrf-token"]').attr('content');
forms.forEach(function (form) {
var input = document.createElement('input');
input.setAttribute('type', 'hidden');
input.setAttribute('name', 'authenticity_token');
input.setAttribute('value', token);
$(form).prepend(input);
});

將資料以 JSON 從 controller 帶入 view 的方式

方法一:在 HTML tag 中帶入 data attribute

當我們在 HTML 中使用 Rails 來的 JSON 資料時,可以直接使用 .to_json

1
2
3
4
5
6
7
<!-- erb -->
<div id="success-rate-chart" data-success-rate-data="<%= @success_rate_data.to_json %>">
</div>

<script>
var successRateData = $(`#${id}`).data('success-rate-data')
</script>

方法二:使用 raw

另一種方式是,我們直接在 javascript 中引入 Rails 的變數,但這種方法的問題是會出現 & 這類的跳脫字元,在 JS 中無法直接使用,因此若想正常使用,需搭配使用 raw 關鍵字

1
2
3
4
<!-- erb -->
<script>
var array = <%= raw @activity.tickets.map(&:name) %>
</script>

更新或刪除資料

keywords: id, _destroy

在 Rails 中,當一個表單裡面有鑲嵌的資料庫時(例如 Quotation 底下的 Items),這時候若我們想要刪除或修改某一 items 可以在表單中插入具有特定關鍵字的 <input>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<tr v-for="(item, index) in quotation.items">
<td width="10%">
<input type="number"
:name=`quotation[items_attributes][${index}][qty]`
v-model="item.qty"
>
</td>
<!-- ... -->
<td>
<input type="number"
:name=`quotation[items_attributes][${index}][price]`
v-model="item.price"
>
</td>

<!-- 用帶有 [id] 的 name 來讓 Rails 能夠 UPDATE 該 item -->
<input type="hidden" :value="item.id" :name=`quotation[items_attributes][${index}][id]`>

<!-- 用帶有 [_destroy] 的 name 來讓 Rails 能夠 DESTROY 該 item -->
<input type="hidden" v-model="item._isDestroy" :name=`quotation[items_attributes][${index}][_destroy]`>
</tr>

參考

相關閱讀

掃描二維條碼,分享此文章