今回はタグ付け機能を実装してみたので、その部分を振り返っていきます。
目次
どこまで実装したか
自由に複数タグ付けが出来る。
バリデーションで戻って来ても値が保持される。
記号や空白は保存出来ない。
同じタグは追加出来ない。
更新処理も実装出来る、って所です。
大まかな流れ
テーブル、モデル作成、設定
コントローラー、ルート設定
コンポーネント側(新規、更新)
コントローラー側(新規、更新)
って感じで進めていきます。コントローラーは新規と更新はアクションが違いますが、中身のコードは同じです。
前提
投稿機能は実装している前提で話を進めていきます。今回はそこにタグ付けも追加してみようと言った形になります!
基本的に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()が便利。
コンポーネント側はバリデーションで戻って来た時と、元々のデータがある場合のデータの取得部分が少し複雑。
こんな感じで実装出来ました。テストヤラナイト。
今回はこんな感じで!
以上!