在 Rails 中,常常會使用 remote => true
的方式,使連結變成 Ajax 的方式送出去:
1
| link_to "Delete Comment", comment_path(@comment), :remote => true, :method => :delete
|
這樣的好處是不用另外寫 Ajax ,就可以輕鬆得送出 Ajax。
如果不指定 remote => true
只有指定 method
,在點擊連結以後 jquery-ujs 會幫你產生一個表單,這個表單會放在 <body>
裡面,而且馬上送出去,如此,可以讓連結不只有 GET ,更可以做到 RESTful的效果。
1
| link_to "Delete Comment", comment_path(@comment), :method => :delete
|
但是這樣的方法卻會產生一些 issue。
Issue
以上兩個方法產生出來的 HTML 是一般的 <a>
tag:
1
| <a href="/comments" data-remote="true" data-method="delete">Delete Comment</a>
|
這樣看起來似乎不會有什麼問題,但如果使用者對連結做了開新視窗的動作:
- 按滑鼠中鍵點連結
- 按右鍵 –> 選擇開新視窗
- 按住
CMD
,再點連結
相當於:
1
| <a href="/comments" target="_blank">Delete Comment</a>
|
這樣的行為是發出 GET
到 CommentsController 的 index action,跟原本的發 DELETE
到 destroy action 是不相同的,使用者達不到刪除的效果,而且如果 index 沒東西,那就會造成 500。
button_to solution
在 Rails 有一個 button_to 的 helper:
1
| button_to "Delete Comment", comment_path(@comment), :remote: true, :method => :delete
|
用 button_to 雖然可以解決部份的問題,但仔細看一下他產生的 html:
1
2
3
4
5
6
7
| <form method="post" action="/comments" class="button_to" data-remote="true">
<div>
<input type="hidden" name="_method" value="delete" />
<input value="Delete Comment" type="submit" />
<input name="authenticity_token" type="hidden" value="xxxxxxx"/>
</div>
</form>"
|
這樣的 HTML 會產生幾個問題:
- 因為產生的是
<form>
所以不能在 <form>
裡面使用,HTML 是不允許 nested form 的。
<form>
預設是 display:block
,跟原本的 <a>
,表現方式不一樣,需要 overide style。
<input>
value 裡面不能塞 HTML,所以如果這個按鈕有 icon 之類的 tag 就無法放進去(這個問題在 Rails 4.0有修正)。
Solution
這個問題真的很惱人,原本很好用的小技巧,變成天天被 Airbrake 轟炸的原兇,所以就寫了一個 button_link_to helper:
application_helper.rb1
2
3
4
5
6
7
8
9
10
11
12
| module ApplicationHelper
def button_link_to(name = nil, options = nil, html_options = nil, &block)
html_options, options = options, name if block_given?
options ||= {}
html_options = convert_options_to_data_attributes(options, html_options)
url = url_for(options)
html_options.merge!({"data-url" => url, :type => "button"})
content_tag(:button, name || url, html_options, &block)
end
end
|
這個方法跟 link_to
使用的方式一模一樣,產生出來的是 <button>
tag,而且是單純的 tag,不會有 <form>
包在外面:
1
| button_link_to "Delete Comment", "/comments", :remote => true
|
1
| <button data-url="/comments" data-remote="true" type="button">Delete Comment</button>
|
預設的 jquery-ujs,按到這個 button,就已經會自動發 ajax 出去,但是這邊應該要讓按中鍵也會跟按左鍵有一樣的效果,於是要加上下面的 js:
application.js1
2
3
4
5
6
| $(document).on("mouseup", "button[data-remote]", function(e) {
// middle button click
if(e.which == 2) {
$(e.currentTarget).trigger("click.rails")
}
});
|
但是如果沒有 :remote => true
,只有指定 method
,照原本的規則應該在 <body>
裡面塞 form,並自動送出去,所以要再加入:
application.js1
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
| $(document).on("click.rails", "button[data-method]", function(e) {
button = $(e.currentTarget)
if (button.data('method') && button.data('remote') === undefined) {
if (!$.rails.allowAction(button)) return $.rails.stopEverything(e);
handleMethod(button)
}
});
var handleMethod = function(button) {
var url = button.data("url"),
method = button.data('method'),
target = button.data('target'),
csrf_token = $('meta[name=csrf-token]').attr('content'),
csrf_param = $('meta[name=csrf-param]').attr('content'),
form = $('<form method="post" action="' + url + '"></form>'),
metadata_input = '<input name="_method" value="' + method + '" type="hidden" />';
if (csrf_param !== undefined && csrf_token !== undefined) {
metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />';
}
if (target) { form.attr('target', target); }
form.hide().append(metadata_input).appendTo('body');
form.submit();
}
|
這樣一來 button_link_to 就可以做到 ajax send 和 form submit 的功能了!
Style
原本的 <a>
和 <button>
樣式是不一樣的,一個是文字,一個是按鈕。
這時候只要加上 Twitter Bootstrap 的 btn-link
class 就好了:
1
| button_link_to "Delete Comment", "/comments", :remote => true, :class => "btn-link"
|
HTML button
因為使用的是 button 而不是 input,所以也可以塞 HTML 進去,Icon 就可以正常運作了,使用跟 link_to 一樣的方式丟入 block:
1
2
3
| button_link_to , "/comments", :remote => true, :class => "btn-link" do
<i class="icon-remove"></i> Delete Comment
end
|
實現 Confirm message
1
2
3
4
5
6
7
8
9
10
| $(document).on("click.rails", "button[data-method]", function(e) {
button = $(e.currentTarget)
if (button.data('method') && button.data('remote') === undefined) {
if (!$.rails.allowAction(button)) return $.rails.stopEverything(e);
handleMethod(button)
}
});
|
實現 data-disabled-with
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| $(document).on("click.rails", "button[data-method]", function(e) {
button = $(e.currentTarget)
if (button.data('disable-with')) $.rails.disableElement(button);
if (button.data('method') && button.data('remote') === undefined) {
if (!$.rails.allowAction(button)) return $.rails.stopEverything(e);
handleMethod(button)
}
});
$(document).on("ajax:complete", "button[data-disable-with]", function() {
$.rails.enableElement($(this));
});
|
Code
這邊我把會用到的 code 總結起來,把這些 code 貼到 application_helper.rb
和 application.js
裡面,接著把 link_to 改成 button_link_to,再修一下 style 就可以了!
application_helper.rb1
2
3
4
5
6
7
8
9
10
11
12
| module ApplicationHelper
def button_link_to(name = nil, options = nil, html_options = nil, &block)
html_options, options = options, name if block_given?
options ||= {}
html_options = convert_options_to_data_attributes(options, html_options)
url = url_for(options)
html_options.merge!({"data-url" => url, :type => "button"})
content_tag(:button, name || url, html_options, &block)
end
end
|
application.js1
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| (function() {
$document = $(document)
$(document).on("mouseup", "button[data-remote]", function(e) {
// middle button click
if(e.which == 2) {
$(e.currentTarget).trigger("click.rails")
}
});
$document.on("click.rails", "button[data-method]", function(e) {
button = $(e.currentTarget)
if (button.data('disable-with')) $.rails.disableElement(button);
if (button.data('method') && button.data('remote') === undefined) {
if (!$.rails.allowAction(button)) return $.rails.stopEverything(e);
handleMethod(button)
}
});
$document.on("ajax:complete", "button[data-disable-with]", function() {
$.rails.enableElement($(this));
});
var handleMethod = function(button) {
var url = button.data("url"),
method = button.data('method'),
target = button.data('target'),
csrf_token = $('meta[name=csrf-token]').attr('content'),
csrf_param = $('meta[name=csrf-param]').attr('content'),
form = $('<form method="post" action="' + url + '"></form>'),
metadata_input = '<input name="_method" value="' + method + '" type="hidden" />';
if (csrf_param !== undefined && csrf_token !== undefined) {
metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />';
}
if (target) { form.attr('target', target); }
form.hide().append(metadata_input).appendTo('body');
form.submit();
}
})();
|