Rails / Ajaxを使って画面遷移しない一時保存機能をつける

Shunsuke Sawada

先日つけてみたソーシャルボタンですが、やっぱ数字が出るってモチベーションになっていいね。
意外とPocketも多いんだなぁ。
  
今回もまたRails。
  
投稿サイトとかでよくある、一時保存機能をやってみた。文章書いていると、何かの拍子で「あ!」てなることが多いから、あるとユーザーに優しい。

フォーム

erb
1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="blog-form">
    <%= form_for(@post) do |f| %>

        <%= f.text_field :tilte %>
        <%= f.text_area :body %>
        <!-- for temporal saving -->
        <div class="create-temp"></div>
        <%= f.submit "送信する" %>

    <% end %>
</div>

フォームは例えばこんな感じ。
一時保存するかどうかのフラグと、行われた後のメッセージ表示用に class="create-temp" がついた div を置いている。
  

コントローラー

ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    def create
        @post = current_user.posts.build(strong_params)
        if @post.save
            redirect_to @post
        else
            render 'new'
        end
    end

    def create_temp
        @post = current_user.posts.build(strong_params)
        @post.published = false
        @post.save
    end

createアクションの他に、一時保存用に、create_tempというのを作った。current_userとかstrong_paramsとかは、人によっていろいろでしょうが、適宜読み替えてください。
一時保存だから、一般には公開したくないわけで、postsテーブルにpublished(boolean)を追加して、create_tempアクションでは @post.published=false としています。
  
config/routesはこんな感じ。

ruby
1
2
3
4
resources :posts
post '/posts/temp',   to: 'posts#create_temp',  as: :temp_post

  

Javascriptでフォーム送信

coffee
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
ready = ->
    # Temporal Saving
    if $('.create-temp')
        window.tempTimer = null
        $('.blog-form form').keydown ->
            # reset
            window.clearTimeout(tempTimer)
            window.tempTimer = window.setTimeout ->
                tempSubmit()
            , 5000

tempSubmit = ->
    # create
    if $('.create-temp span').data('result') != true
        $(".blog-form").ajaxSubmit(
            url: '/posts/temp',
            type: 'post'
        )
    # update
    else
        $(".blog-form form").ajaxSubmit()

# For turbolinks
$(document).ready(ready)
$(document).on('page:load', ready)

  
色んなやり方があると思いますが、keydownされた後、5秒たったら一時保存のフォーム送信をするようにしました。
5秒以内にkeydownされると、キャンセルされます。

ユーザーのアクション関係なくデータを投げるのもありかもしれませんが、ユーザーが画面を開きっぱなしでどこかに行ってしまったら、ずーーっと送信されることになります…ので、なにか手は必要ですね。
ajax送信にはこのプラグインを使いました。
  
Turbolinksなので、page:loadを使わないとダメです。
  

Viewファイル

jsに対応するビューファイルを作ります。

javascript
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
var result = <%= @post.valid? %>;
var msg = "";
var notice = "";

if (result == true) {
    msg = "下書き保存されました";

    // This is for Temporal saving
    // forcing the form to look update method.

    // Add id
    $('.blog-form').prepend('<input name="post[id]" type="hidden" value="<%= @post.id %>">');

    // Chnage REST method
    $('.blog-form input[name=_method]').remove();
    $('.blog-form').prepend('<input name="_method" type="hidden" value="patch">');

    // Change URL
    $('.blog-form').attr('action', '/posts/<%= @post.id %>');

} else {
    msg = "下書き保存のための情報が足りません";
}


// Message
notice = "<span data-result=" + result + ">" + msg + "</span>";
$('.create-temp span').remove();
$('.create-temp').hide().append(notice).fadeIn();

  
一回目の一時保存は普通にcreate_tempアクションに飛んでもらって良いんだけど、次のタイミング(例えば1分に1回保存するとかする場合)では、もうそのレコードはあるんだから、updateアクションに飛んでもらわないと困る。
  
ということで、javascriptでフォームの内容を変更しています。
強引だけど、これ以外に思いつかなかった。

Railsのフォームは input name="_method" で、フォームの送信先を変える情報を持っています。
同じPOST送信でも、value="patch"ならupdateメソッド、value="post"ならcreateメソッドが呼び出されるので、updateしたいタイミングで変更してるのです。
  

updateメソッド

最後にupdateメソッド。
通常のupdateと、一時保存のupdateで、publishedの状態を変える必要があります。

通常update(送信ボタンを押した時)
published = true

一時保存update(下書きとして扱う)
published = false

あと、通常はHTML、一時保存はajaxなので、respond_toで分けています。
こんな感じになりました。

ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    def update
        # 所有権のチェック
        @post.attributes = strong_params
        # publish if update botton was clicked
        @post.published = true if params[:commit]

        respond_to do |format|
            format.js do
                @post.save
            end
            format.html do
                if @post.save
                    flash[:success] = "ブログが更新されました。"
                    redirect_to @post
                else
                    render 'edit'
                end
            end
        end
    end

  
updateの前にこの記事が本当にユーザーの持ち物かどうかチェックする必要がありますが(所有権のチェック)、そのメソッドは省略します。
  
以上です。
初めてやってみたので、つっこみあればお願いします :)

198
Shunsuke Sawada

おすすめの記事

acts-as-taggable-on タグを表示させる順番を決めたい
Railsを4.2にバージョンアップしたら、Vagrantのローカル開発環境にアクセスできなくなった問題
Railsのバリデーションエラー後にレイアウトが崩れるとき