LaravelとVue.jsでタグ付け機能実装してみた

2021.01.18

見出し画像

今回はタグ付け機能を実装してみたので、その部分を振り返っていきます。

目次

  1. どこまで実装したか
  2. 大まかな流れ
  3. 前提
  4. テーブル、モデル作成、設定
  5. コントローラー、ルート設定
  6. blade側
  7. コンポーネント側(新規, 更新)
  8. コントローラー側(新規)
  9. 課題
  10. まとめ

どこまで実装したか

自由に複数タグ付けが出来る。

バリデーションで戻って来ても値が保持される。

記号や空白は保存出来ない。

同じタグは追加出来ない。

更新処理も実装出来る、って所です。

大まかな流れ

テーブル、モデル作成、設定

コントローラー、ルート設定

コンポーネント側(新規、更新)

コントローラー側(新規、更新)

って感じで進めていきます。コントローラーは新規と更新はアクションが違いますが、中身のコードは同じです。

前提

投稿機能は実装している前提で話を進めていきます。今回はそこにタグ付けも追加してみようと言った形になります!

基本的にitemに関することは省略します。

テーブル、モデル作成、設定

モデル作成していきますが、-mオプションでマイグレーションファイルも同時生成します。中間テーブルも作成します。

php artisan make:model Tag

php artisan make:model item_tag

マイグレーションファイルを編集

Tagテーブル

  public function up()
   {
       Schema::create('tags', function (Blueprint $table) {
           $table->id();
           $table->string('name');
           $table->timestamps();
       });
   }
public function up()
{
   Schema::create('item_tag', function (Blueprint $table) {
       $table->id();
       $table->foreignId('item_id')->constrained('items')->onDelete('cascade');
       $table->foreignId('tag_id')->constrained('tags')->onDelete('cascade');
       $table->timestamps();
   });
}

ここまで出来たらデータベースに反映させます。

php artisan migrate
もしうまく行かなかったら下記を実行
php artisan migrate:fresh

モデルファイルを編集。itemモデルも一応書いときます。中間テーブルのモデルファイルは使わないので消しておきましょう。

class Tag extends Model
{
   protected $fillable = ['item_id','name'];

   public function items()
   {
       return $this->belongsToMany('App\Item');
   }
}


Itemモデル
public function tags()
{
   return $this->belongsToMany('App\Tag');
}

コントローラー、ルート設定

コントローラーとルートは既に投稿機能が出来ている前提なので、省略します。(store、updateアクション使いましょう)

blade側

コンポーネントを作成ページの適当な場所に追加します。

バリデーションで戻って来た時も値を保持するようにold_tagsを書いて、コンポーネントに渡しています。

<tag-add  :old_tags="{{ json_encode(Session::getOldInput()) }}"></tag-add>

コンポーネント側(新規, 更新)

*注1  新規も更新も同じコンポーネントファイルを使用します。

*注2  新規の際はvalidate__back_tagsは無視して進みます。

<template>
 <div class="category">
   <div class="row col">
 1   <div v-for="(tag, index) in tags" :key="index">
       <span class="mr-1 mt-1 btn btn-primary btn-sm">{{tag}}<span class="ml-2" @click="tag_delete(tag)">x</span></span>
     </div>
   </div>
 2 <textarea class="form-control mt-1" id="tag-list" v-model="text" v-on:keydown.enter.exact.prevent v-on:keyup.enter.exact="createTag"></textarea>
 3 <input type="hidden" name="tags" :value="tags" v-if="tags">
 </div>
</template>

<script>
export default {
 props: ['validate__back_tags', 'old_tags'],
 data() {
   return {
     text: '',
     tags: []
   }
 },
 created() {
4  if(this.validate__back_tags != undefined && this.validate__back_tags.tags != null) {
     this.get_tags(this.validate__back_tags)
   } else {
     if(this.old_tags) {  // newの時は持ってないのでif文が必要
       this.get_tags(this.old_tags)
     }
   }
 },
 methods: {
5  get_tags(list) {
     if(list.tags) {
       let tag_list = list.tags.split(',')
       tag_list.forEach(element => {
         this.tags.push(element)
       });
     } else {
       if(list) {
         list.forEach(element => {
           this.tags.push(element.name)
         });
       }
     }
   },
6  createTag() {
     function check_already(arr, ele) {
       return arr.some(function(val) {
         return val == ele
       })
     }
     let tept = this.text
     // 空白調べ。空白だけだったらなくなる。if(text)がfalseになる
     let text = tept.replace(/\s+/g, "");
     var reg = new RegExp(/^[a-zA-Z0-9]|[ぁ-んァ-ン一-龥]/);
     // 存在チェック
     if(text) {
       // 重複チェックと正規表現での記号チェック
       if(!check_already(this.tags, text) && reg.test(text)) {
         this.tags.push(text)
         this.text = ''
       } else {
         this.text = ''
         return false
       }
     } else {
       this.text = ''
       return false
     }
   },
7  tag_delete(tag) {
     this.tags = this.tags.filter(ele => ele != tag)
   }
 }
}
</script>

1,  タグをeachで回しています。削除メソッドtag_deleteが動くように記載しています。tagを引数で渡しています。

2,  こちらはエンターキーが押された時に6のcreateTagメソッドが動くようにしています。イベントハンドリングを使っています。参考にさせて頂きました。

3, こちらはname属性をつけてformが送信された際にコントローラー側に飛ぶようにしています。飛ばす値は:valueで指定しています。

4,  コンポーネントが読み込まれた際のライフサイクルメソッドです。get_tagsメソッドを呼び出していますが、バリデーションで戻って来ているのかなど、viewが表示された時の状況によって渡す引数を変えています。ここではthis.validate__back_tagsが存在し、かつthis.validate__back_tags.tagsがnullではない場合に実行されます。

5,  バリデーションで戻って来た場合と、元々値がある場合によってlistの中身が変わって来ます。中身によって処理を分けています。

6,  タグを作成し、tagsのなかに格納しています。格納する前に、空白や記号を弾くように条件分岐をしています。同じタグの場合も弾いています。

7,   x印をクリックした時にそのタグが削除される処理をしています。filter()を使い新しい配列をtagsに格納しています。 

コントローラー側(新規)

if($request->tags != null) {          1
   $tags = [];
   $tag_list = explode(',', $request->tags);      2
   foreach($tag_list as $tag) {
       $record = Tag::firstOrCreate(['name' => $tag]);    3
       array_push($tags, $record);     4
   }

   $tag_ids = [];
   foreach($tags as $tag) {
       array_push($tag_ids, $tag->id);    5
   }
   $item->tags()->sync($tag_ids);        6
}

この処理以前に$itemは保存されている前提です。

1,  tagsの情報が含まれる場合にのみ実行されます。

2,  カンマで分けて新しい配列を作成します。

3,  同じタグが生成されているかfirstOrCreateで確認しながら作成します。

4,  出来たレコードをarray_push()を使い$tagsの中に入れていきます。

5,  3で作成したtagのidだけを4で作成した配列から取り出しidだけが入っている配列を作成します。

6,  syncを使い5で生成した配列をitemに紐づいたtagという形で中間テーブルに保存していきます。syncを使う事で紐付け、解除どちらも実行できます。

課題

テストしないとですね。VueもLaravelも。

laravel側はフォームリクエスト作って、テストってイメージ。ただVue.jsは全くイメージ湧かないですw

まとめ

新規も編集も同じコードで実装できる。

コントローラー側はfirstOrCreate()とsync()が便利。

コンポーネント側はバリデーションで戻って来た時と、元々のデータがある場合のデータの取得部分が少し複雑。

こんな感じで実装出来ました。テストヤラナイト。

今回はこんな感じで!

以上!