LiBz Tech Blog

LiBの開発者ブログ

Vue+RailsでGitHubみたいなチェックボックスリスト(マークダウン入力)をサーバよりに作る

f:id:gac777:20190425165208p:plain

はじめに

個人的にVueの勉強のために作ってるタスク管理ツールでGitHubみたいなチェックボックスリスト(マークダウン入力)を作りたくなりました。 f:id:gac777:20190425105338p:plain f:id:gac777:20190418184615p:plain

またこのフィールドにURLが貼り付けられたらリンクにしたい欲求もありました。

f:id:gac777:20190418184737p:plain

まとめると

  • マークダウンの形式でチェックボックスをつけたい
  • URLが書かれたらリンクつけたい(サーバサイドエンジニア思考でRailsで実装)
  • JavaScriptなどは実行されない状態にしたい

の3つの欲求を満足したいと思います。

私はサーバサイドが得意なエンジニアなので、こういう時、サーバサイド側でなんとかしようと頑張る傾向があるため、 今回の記事はサーバサイドで頑張る設計になっています。

設計

サーバサイドエンジニアなので、HTMLエスケープは今回はサーバサイドで行う。
URLなどのリンクもサーバサイドでURLからaタグの生成もサーバサイドで。
チェックボックスのOn/Off検知が必要なのでcheckboxのタグはVue側にします。

使うもの

  • HTMLタグのエスケープはRails側
  • Railsのauto link(https://github.com/tenderlove/rails_autolink)
  • simple_formatは行をタグで囲んだりするので使いにくいので、safe_joinを利用
  • Vueのv-htmlディレクティブで表示します(v-htmlは使い所間違えるとセキュリティリスクがあるので慎重に)
  • checkboxなどのUIについてはElement UI使います

safe_join利用の補足

Rails側のActiveRecordの列に対してbrタグに変更します。 simple_formatでもいいのですが、simple_formatの場合、

文字列を<p>で括る
改行は<br />を付与
連続した改行は、</p><p>を付与

と自動で余分なタグ<p>を入れてVue側から扱いにくいのでsafe_joinで
に変更します。
(Vue側では受けと取ったデータをbrタグでsplitして配列で管理するため。)

auto_link(safe_join(memo.split("\n"),tag(:br)), html: {target: '_blank', rel: "noopener noreferrer"})

Vueでv-if使ってる場合、checkboxなどのインプット属性が更新されないので、更新させるためにkeyを入れる

<el-checkbox
   @change="markCheck(line_no)"
   key="index + 'false'"
>

必要なGEM

RailsでURLにリンクを貼ってくれるGEMを導入します。
Gemfileに以下を追加します。

gem 'rails_autolink'

実装での流れです。

  1. サーバサイドでURLをaタグに変換・改行コードを(¥n)をbrタグに変換・HTMLエスケープ(scriptタグなど)する
  2. Vueで受け取ったデータをsplitでbrタグ区切りで配列に格納する
  3. v-forで上記配列を回して、もし先頭に- [ ] もしくは- [x] があったらcheckboxをつけ、v-htmlで文字列を表示する。(この際にonChangeイベントをつけておく)
  4. onChangeイベントが発行したらサーバ と通信してデータを更新する f:id:gac777:20190424161826p:plain

1.サーバサイドでURLをaタグに変換・改行(¥n)をbrタグに変換・HTMLエスケープ(scriptタグなど)する

モデルに変換したデータを返すメソッドを定義します。

# safe_joinを使うためにインクルード
 include ActionView::Helpers::TextHelper

# 
 def memo_str
    auto_link(safe_join(memo.split("\n"),tag(:br)), html: {target: '_blank', rel: "noopener noreferrer"})
 end

これをそのままjsonに渡せばOKです。

2.Vueで受け取ったデータをsplitでbrタグ区切りで配列に格納する

受け取ったデータを格納する場所の定義

 data: function() {
    return {
        memo,
        memo_lines: []
    }
}

メソッドの定義

    markLines: function(detail) {
      if (detail == undefined) {
        return;
      }
      return lineDetail = detail.split("<br>");
    },

サーバからjsonでモデルの情報を受け取り、配列に格納する

this.memo_lines = this.markLines(response.data.todo.memo_str);

3.v-forで上記配列を回して、もし先頭に- [ ] があったらcheckboxをつけ、v-htmlで文字列を表示する。(この際にonChangeイベントをつけておく)

markCheck/markUnCheckの引数には該当の行番号を引数に渡します。
メソッド内で該当の行数にチェック入れたり、外したりするため

 <ul>
     <li v-for="(line, line_no) in memo_lines" :key="index">
        <span v-if="line.match(/^\-\s\[\s\]\s/)">
             <el-checkbox
                  @change="markCheck(line_no)"
                  :disabled="disable_memo_check"
                  key="index + 'false'"
             >
                 <span v-html="line.slice(5)"></span>
             </el-checkbox>
        </span>
         <span v-else-if="line.match(/^\-\s\[x\]\s/)">
              <el-checkbox
                  @change="markUnCheck(line_no)"
                  :checked="true"
                  :disabled="disable_memo_check"
                  key="index + 'true'"
              >
                  <span v-html="line.slice(5)"></span>
               </el-checkbox>
        </span>
        <span v-else v-html="line"></span>
    </li>
</ul>

4.onChangeイベントが発行したらサーバと通信してデータを更新する

データ送信するメソットであるmarkCheckとmarkUnCheckを定義します。

※1個のメソッドで引数で区別するのでもいいかと思います。
更新時にサーバに送信されるデータのイメージは以下のように全文を送信しています。

- [ ] テスト
- [ ] テスト
- [ ] テスト

markCheckメソッド

このメソッドでやることは、

  • サーバに送信するためのデータの該当箇所の- [ ] を- [x]に変更する (xを入れるとチェックを入れた状態に)
  • サーバに送信する

の2つになります。

    markCheck: function(line_no) {
        // 処理中は他のチェックボックスがいじられないようにする
        this.disable_memo_check = true;
        // DBに格納されてるデータと同じものを加工してサーバに送信する
        // 書き換えのがめ配列に変換
        let lines = this.memo.split(/\r\n|\r|\n/);
        // チェックが入ったので- [x]に該当行を書き換える
        lines[line_no] = lines[line_no].replace(/^\-\s\[\s\]\s/, "- [x] ");
        // 配列をもとに戻す
        this.memo = lines.join("\n");
        // ajaxで通信とデータの更新をupdateCheckで行います。
        this.updateCheck(true);
    },

markUnCheckメソッド

markCheckと変わるのは、-[x]を- [ ]にする部分が違うだけです。

    markUnCheck: function(line_no) {
        this.disable_memo_check = true;
        let lines = this.memo.split(/\r\n|\r|\n/);
        lines[line_no] = lines[line_no].replace(/^\-\s\[x\]\s/, "- [ ] ");
        this.memo = lines.join("\n");
        // ajaxで通信とデータの更新をupdateCheckで行います。
        this.updateCheck(false);
    }

出来ました

マークダウン形式で入力します

f:id:gac777:20190418191018p:plain

保存します

f:id:gac777:20190418191037p:plain

チェックいれます

f:id:gac777:20190418191053p:plain

入力にもどると「x」が入ります。

f:id:gac777:20190418191110p:plain

動画じゃないとわかりにくいので動画でも。 f:id:gac777:20190425110725g:plain

まとめ

Vueの勉強のためにプロダクトを作ってみようと思い作りましたが、その中で、Vueの実装はサーバサイドエンジニア?フロントエンドエンジニア? のどちらのスキルセットになるのかという疑問に駆られました。
LiBではエンジニアがユーザへの価値提供にも責任を持っています。(作るだけではなく作ったものがきちんと価値提供したか?まで責任を持つ)
サーバサイドエンジニアがAPIだけを作ると、実際にユーザに届ける(触れられる)部分を全く見ずに作ることになるので、開発途中で気がつく課題や改善が出づらくなると感じました。
サーバサイドエンジニアもVueを使えるように勉強会など含めて考えようと思うCTOでした。
ということでよきタイミングで弊社のVue使いの阿部さんからVueで何かを作るチュートリアルを書いてもらおうと思います。