※ 2018/12/14 ダイアログと一覧の関係について追記(6.ダイアログと一覧の関係)
Vuetify のダイアログを使ってユーザの追加、更新、削除処理を作ってみる
ユーザ一覧のVue親コンポーネントから子コンポーネントのダイアログを呼び出して、
子コンポーネントから処理結果を親コンポーネントに通知したりしてコンポーネント間の親子連携した処理を作ってみる
環境設定他関連記事はこちら
Laravel + Vue + Vuetify で業務サイト作ってみる
こちらの記事がとても参考になりました
Qiita Vue 子から親へ コンポーネント間のデータ受け渡し
https://qiita.com/fukuman/items/b0bc84081ad0d2bc522a
Qiita Vuetify でなるべく疎結合になるように ref を使って Modal(モーダル)を実装する
https://qiita.com/superyusuke/items/9eaad853422937de2391
#1.ユーザ一覧ページにダイアログ呼び出し部分を追加
前回の手順で作ったユーザ一覧ページにダイアログを呼び出す部分を追加して、ダイアログの結果を受け取ったりなんだりの処理も追加してみる
長いので、HTML部分とJavascript部分を分けて記載
<template>
<v-flex>
<v-card xs12 class="m-3 px-3">
<v-card-title class="title">
<v-icon class="pr-2">{{ $route.meta.icon }}</v-icon> {{ $route.meta.name }} {{ /* 社員管理 */ }}
★1 <user-dialog ref="userDialog" @reload="reload" @setsearch="setsearch"></user-dialog>
<v-spacer></v-spacer>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-icon="search"
label="Search"
single-line
hide-details
clearable
></v-text-field>
</v-card-title>
<v-data-table
:headers="headers"
:items="tabledata"
:pagination.sync="pagination"
:rows-per-page-items='[10,25,50,{"text":"All","value":-1}]'
:loading="loading"
:search="search"
class="elevation-0 p-1"
>
<v-progress-linear slot="progress" color="blue" indeterminate></v-progress-linear>
<template slot="items" slot-scope="props">
<tr>
<td class="text-xs-center" xs1>{{ (props.index + 1) + (pagination.page - 1) * pagination.rowsPerPage }}</td>
<template v-for="n in (headers.length - 2)">
<td :class="'text-xs-' + headers[n].align" style="white-space: nowrap;" v-text="props.item[headers[n].value]"></td>
</template>
<td class="text-xs-center" xs1>
★2 <v-btn flat small fab @click="dialogOpen(props.item)"><v-icon color="success">edit</v-icon></v-btn>
★3 <v-btn flat small fab @click="dialogOpen(props.item,true)"><v-icon color="error">delete</v-icon></v-btn>
</td>
</tr>
</template>
</v-data-table>
</v-card>
</v-flex>
</template>
★1 ダイアログコンポーネントを呼び出し
★2 一覧のデータに「更新」ボタンを追加(鉛筆アイコン)
★3 一覧のデータに「削除」ボタンを追加(ゴミ箱アイコン)
続いて Javascript部分
<script>
★1 import user_dialog from './UserDialog.vue'
export default {
name: 'UserComponent',
components: {
★1 'user-dialog': user_dialog,
},
props: {
},
data: () => ({
loading: false,
search: '',
pagination: { sortBy: 'name', descending: false, },
tabledata: [],
headers: [
{ align: 'center', sortable: false, text: 'No', },
{ align: 'left', sortable: true, text: '社員ID', value: 'loginid' },
{ align: 'left', sortable: true, text: '氏名', value: 'name' },
{ align: 'center', sortable: true, text: '権限', value: 'role' },
★2 { align: 'center', sortable: false, text: 'アクション', },
],
}),
created() {
if (process.env.MIX_DEBUG) console.log('User Component created.')
this.initialize()
},
methods: {
initialize() {
this.getUsers()
},
★3 reload() {
if (process.env.MIX_DEBUG) console.log('User Component reload')
this.getUsers()
},
★4 setsearch(id) {
if (process.env.MIX_DEBUG) console.log('User Component set Search')
this.search = id
},
getUsers() {
if (process.env.MIX_DEBUG) console.log('User Component getUsers')
this.loading = true
axios.post('/api/admin/user')
.then( function (response) {
this.loading = false
if (process.env.MIX_DEBUG) console.log(response)
if (response.data.users) {
this.tabledata = response.data.users
this.setRole()
}
}.bind(this))
.catch(function (error) {
this.loading = false
console.log(error)
if (error.response && [401, 419].includes(error.response.status)) {
this.$emit('axios-logout')
}
}.bind(this))
},
setRole() {
for (var i=0; i<this.tabledata.length; i++) {
if (this.tabledata[i].role) {
if (this.tabledata[i].role == 5) { this.tabledata[i].role = '管理者' }
if (this.tabledata[i].role == 10) { this.tabledata[i].role = 'ユーザ' }
}
}
},
★5 dialogOpen(item,flg) {
if (process.env.MIX_DEBUG) console.log('User Component dialog open')
this.$refs.userDialog.open(item, (flg || false))
}
},
}
</script>
★1 ダイアログコンポーネント設定
★2 一覧に「編集」、「削除」ボタン欄を追加
★3 ダイアログの処理が終わったらリロードする用(子コンポーネントから実行)
★4 ログインIDが被った時に一覧から絞り込み表示する用(子コンポーネントから実行)
★5 一覧のデータを設定してダイアログを呼び出す(削除の場合は第二引数に true を設定)
#2.ダイアログコンポーネントを作成
新規追加用のボタンと更新時の「保存」ボタン、削除時の「削除」ボタンを配置
ぱっと見で分かるように、「新規」、「編集」、「削除」時にタイトルの色も変えてみる
新規イメージ
編集イメージ
削除イメージ
こちらも長いので分割してソースを記載
HTML 部分
<template>
<transition name="fade">
<v-dialog v-model="dialog" max-width="650px" persistent>
★1 <v-btn slot="activator" color="primary" dark class="mb-2" flat @click="open(null)"><v-icon class="pr-2">person_add</v-icon></v-btn>
<v-card>
<v-toolbar :color="titlecolor" dark>
★2 <v-toolbar-title><v-icon class="pb-1">{{ $route.meta.icon }}</v-icon> {{ $route.meta.name }} | {{ title }}</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-container>
<v-layout column wrap>
★3 <v-text-field class="pb-3" label="名前" placeholder="氏名を入力してください"
v-model="items.name"
:error-messages="error.name"
:rules="[rules.required, rules.min2]"
maxlength="64"
required
counter
:disabled="type == 'D'"
></v-text-field>
★4 <v-text-field class="pb-3" label="ログインID" placeholder="社員IDを入力してください"
v-model="items.loginid"
:error-messages="error.loginid"
:rules="[rules.required, rules.min6]"
maxlength="128"
required
counter
:disabled="type != 'C'"
></v-text-field>
★5 <v-text-field class="pb-2" label="パスワード" :placeholder="placeholder_password"
v-model="items.pass"
:error-messages="error.pass"
maxlength="128"
counter
:disabled="type == 'D'"
></v-text-field>
★6 <v-checkbox v-model="items.role" label="管理者権限" :disabled="type == 'D'"></v-checkbox>
</v-layout>
</v-container>
</v-card-text>
<v-card-actions>
★7 <v-btn color="gray darken-1" flat block @click.native="close">キャンセル</v-btn>
★8 <v-btn color="primary darken-1" flat block @click.native="save" v-show="type != 'D'">保存</v-btn>
★9 <v-btn color="error darken-1" flat block @click.native="destroy" v-show="type == 'D'">削除</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</transition>
</template>
★1 ユーザ一覧に表示する「新規追加」ボタンを設定
★2 モードによってタイトルを変える(新規、編集、削除)
★3 名前欄:必須項目、最低2文字、最大64文字、「削除」時は無効化
★4 ログインID欄:必須項目、最低6文字、最大128文字、「新規」時以外は無効化(ログインIDは更新できない)
★5 パスワード欄:任意項目、最大128文字、「削除」時は無効化
★6 権限欄:管理者権限をつける場合にチェック、「削除」時は無効化
★7 キャンセルボタン: 押すとダイアログを閉じる
★8 保存ボタン:押すと保存処理(save)を実行、「削除」時は表示しない
★9 削除ボタン:押すと削除処理(destroy)を実行、「削除」時のみ表示
Javascript部分(基本部分)
<script>
export default {
name: 'UserDialog',
props: {
},
data: () => ({
★1 dialog: false,
title: '編集',
titlecolor: 'primary',
placeholder_password: '',
type: '',
★2 items: {
loginid: '',
name: '',
pass: '',
role: false,
},
★3 orig: {},
★4 error: {},
★5 rules: {
required: value => !!value || 'Required.',
min2: value => value.length >= 2 || 'Min 2 characters',
min6: value => value.length >= 6 || 'Min 6 characters',
},
}),
created() {
if (process.env.MIX_DEBUG) console.log('User Dialog created.')
},
methods: {
★6 clearVar() {
this.dialog = true
this.clearError()
this.items = JSON.parse(JSON.stringify(this.error))
this.orig = JSON.parse(JSON.stringify(this.error))
},
★6 clearError() {
this.error = {
loginid: '',
name: '',
pass: '',
role: false,
}
},
★7 close() {
if (process.env.MIX_DEBUG) console.log("User Dialog func close")
this.dialog = false
},
★8 save() {
if (process.env.MIX_DEBUG) console.log("User Dialog func save")
// 変更があった時だけ通信
if (JSON.stringify(this.orig).replace(/[\s| ]+/g,'') !== JSON.stringify(this.items).replace(/[\s| ]+/g,'')){
this.store()
}
// 変更がなければただ閉じる
else {
this.close()
}
},
★1 最初はダイアログは閉じた状態
★2 対象データを保持する領域(loginid、name、pass、role の各項目)
★3 変更があったかを確認するための変更前データを保持する
★4 サーバ通信後にエラーがあった場合のエラーメッセージを保持する
★5 入力ルールを設定(最低文字とか)
★6 変数領域を初期化する
★7 ダイアログを閉じる
★8 保存判定部分、変更前データと比較して変わってる部分があるかをチェックしている
Javascript部分(ダイアログを開いた時の処理部分)
★1 open(item, flg) {
if (process.env.MIX_DEBUG) console.log("User Dialog func open")
// INIT VAR
this.clearVar()
// SET TYPE
if (flg) this.type = 'D' // DELETE
else if (item) this.type = 'U' // UPDATE
else this.type = 'C' // CREATE
// USER CREATE
if (this.type == 'C') {
this.title = "新規追加"
this.titlecolor = 'primary',
this.placeholder_password = "パスワードを指定してください(未指定の場合はログインIDを設定)"
}
// USER UPDATE
if (this.type == 'U') {
this.title = "編集"
this.titlecolor = 'accent',
this.placeholder_password = "変更する場合はパスワードを指定してください(未指定の場合は変更しない)"
}
// USER DELETE
if (this.type == 'D') {
this.title = "削除"
this.titlecolor = 'error',
this.placeholder_password = ""
}
// SET ITEM
if (item) {
if (item.loginid) this.items.loginid = item.loginid
if (item.name) this.items.name = item.name
if (item.role) {
if (item.role == '管理者') {
this.items.role = true
}
}
// COPY ORIG
★2 this.orig = JSON.parse(JSON.stringify(this.items))
}
},
★1 ダイアログを表示する部分
編集や削除の時は対象データ(item)を受け取る
削除の時はフラグ(flg)を受け取る
★2 変更箇所を把握するために開いた直後の状態をコピーして保持しとく
Javascript部分(保存処理部分)
store() {
if (process.env.MIX_DEBUG) console.log("User Dialog func store")
★1 var params = new URLSearchParams()
params.append('loginid', this.items.loginid)
params.append('name', this.items.name)
params.append('pass', this.items.pass)
params.append('role', (this.items.role ? 5 : 10))
params.append('type', this.type)
★2 this.clearError()
★3 axios.post('/api/admin/user/store', params)
★4 .then( function (response) {
this.$emit('reload')
alert(this.items.name + "を保存しました")
this.close() // 保存が正常終了したら閉じる
}.bind(this))
★5 .catch(function (error) {
if (process.env.MIX_DEBUG) console.log("User Dialog store error")
console.log(error)
★6 if (error.response && [401, 419].includes(error.response.status)) {
this.$emit('axios-logout')
}
★7 else if (error.response && [423].includes(error.response.status)) {
this.$emit('setsearch', this.items.loginid)
alert(error.response.data.message)
return
}
★8 else if (error.response && [422].includes(error.response.status)) {
alert(error.response.data.message)
if (error.response.data.errors) {
for (let key in this.error) {
if (error.response.data.errors[key]) {
★9 this.error[key] = error.response.data.errors[key]
}
}
}
★10 return
}
this.close()
}.bind(this))
},
★1 サーバへ送る情報を設定
★2 エラー領域を初期化
★3 サーバの保存処理をajaxで呼び出し
★4 サーバ処理が正常終了したら、メッセージを表示してダイアログを閉じる
★5 サーバ処理でエラーが発生したら。。
★6 401,419 エラーならログアウト(多分タイムアウトとか)
★7 423 エラーならログインIDの重複を検知したのでメッセージ表示
★8 422 エラーならサーバ側のデータチェック処理エラー発生
★9 チェックエラーになった項目のエラーメッセージを設定
★10 エラーが発生した場合はダイアログを閉じない
Javascript部分(削除処理部分)
destroy() {
if (process.env.MIX_DEBUG) console.log("User Dialog func destroy")
★1 var params = new URLSearchParams()
params.append('loginid', this.items.loginid)
★2 axios.post('/api/admin/user/destroy', params)
★3 .then( function (response) {
this.$emit('reload')
alert(this.items.name + "\n" + "を削除しました")
this.close() // 保存が正常終了したら閉じる
}.bind(this))
★4 .catch(function (error) {
if (process.env.MIX_DEBUG) console.log("User Dialog destroy error")
console.log(error)
★5 if (error.response && [401, 419].includes(error.response.status)) {
this.$emit('axios-logout')
}
★6 if (error.response && [422].includes(error.response.status)) {
alert(error.response.data.message)
★7 return
}
this.close()
}.bind(this))
},
},
}
</script>
★1 サーバへ送る情報を設定
★2 サーバの削除処理をajaxで呼び出し
★3 サーバ処理が正常終了したら、メッセージを表示してダイアログを閉じる
★4 サーバ処理でエラーが発生したら。。
★5 401,419 エラーならログアウト(多分タイムアウトとか)
★6 422 エラーならサーバ側のデータチェック処理エラー発生
(削除対象データが存在しない!??)
★7 エラーが発生した場合はダイアログを閉じない
#3.Laravel 側処理作成
新規登録や更新、削除の処理を作成する
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Hash;
use App\User;
use Validator;
class UserController extends Controller
{
public function index()
{
Log::Debug(__CLASS__.':'.__FUNCTION__.' index ');
$users = User::all();
return ['users' => $users];
}
public function store(Request $request)
{
Log::Debug(__CLASS__.':'.__FUNCTION__.' store ');
// 入力項目チェック(必須やら文字数やら)
$data = $this->validator($request->all());
// ユーザ情報DB保存
return $this->storeUser($data);
}
public function destroy(Request $request)
{
Log::Debug(__CLASS__.':'.__FUNCTION__.' destroy :'. print_r($request->all(),true));
// 入力項目チェック(必須やら文字数やら)
$data = $request->all();
// loginID 指定あり?
if (trim($data['loginid'] == '')) {
return response()->json(['message' => 'loginID Not Found' ], 422);
}
// ユーザテーブルから該当者データを取得
$user = User::where('loginid', $data['loginid'])->first();
// 該当者データなし -> エラー
if (! $user) {
return response()->json(['message' => 'User Not Found' ], 422);
}
// 該当者削除
$user->delete();
// RETURN
return ['data' => $user];
}
private function storeUser(array $data)
{
// ユーザテーブルから該当者データを取得
$user = User::where('loginid', $data['loginid'])->first();
// 該当者データあり、更新要求ならデータ更新
if ($user) {
if ($data['type'] == 'U') {
$user->fill($data)->save();
}
// 該当者データあり、更新要求以外ならエラーj
else {
return response()->json(['message' => 'User Exists'], 423);
}
}
// 該当者データなし、新規要求ならデータ新規作成
else {
if ($data['type'] == 'C') {
// パスワードが指定されていなければ、loginid を初期地として設定
if (! array_key_exists('password', $data)) {
$data['password'] = Hash::make($data['loginid']);
}
$user = User::create($data);
}
// 該当者データなし、新規要求以外ならエラー
else {
return response()->json(['message' => 'User Not Found' ], 422);
}
}
// RETURN
return ['data' => $user];
}
private function validator(array $data)
{
Log::Debug(__CLASS__.':'.__FUNCTION__.' validator :'. print_r($data,true));
// ログインIDに許可する 「 記号 」
// ,-.@_ |
$ID_KIGO = ',-.@_\\|';
// パスワードに許可する「 記号 」
// !"#$%& '()*+,-. /:;<=>?@ \[ ]^_`{ |}~
$KIGO = '!"#$%&\'()*+,-.\/:;<=>?@\\\\[\\]^_`{\\|}~';
// 入力項目チェック(必須やら文字数やら)
$validator = Validator::make($data, [
// 氏名: 必須、最小2文字、 最大64文字
'name' => [
'required', // 必須
'min:2', // 最低2文字 (中国人でも2文字はあるよね)
'max:64', // 最大64文字 (アルファベットの名前だと足りないか?)
],
// ログインID:
'loginid' => [
'required', // 必須
'min:6', // 最低6文字(a@a.aa の形でも最低6文字は必要だよね)
'max:128', // 最長128文字(なんとなく)
'unique:users,loginid,'.$data['loginid'].',loginid', // 同じログインIDは登録不可
'regex:/[a-zA-Z\d'.$ID_KIGO.']+\z/',
// 英小文字(a-z) or 英大文字(A-Z)、数字(\d)、記号($ID_KIGO)以外の文字はエラー
],
// パスワード:
'pass' => [
'nullable', // 空でもOK
'min:4', // 最低4文字
'max:128', // 最長128文字(なんとなく)
'regex:/\A(?=.*?[a-zA-Z])(?=.*?\d)(?=.*?['.$KIGO.'])[a-zA-Z\d'.$KIGO.']+\z/',
// 必ず英小文字(a-z)or英大文字(A-Z)、数字(\d)、記号($KIGO)を1文字含む(\A)こと
],
])->validate();
// パスワードが設定されていたらハッシュ化
if ($data['pass'] != ''){
$data['password'] = Hash::make($data['pass']);
}
return $data;
}
}
必須入力や文字種の判定にLaravel のバリデーションを利用してます
判定でエラーになったら勝手に 422 でエラーを返してくれる素敵な仕様 (自動リダイレクトでJSONを返してくれる)
Laravel は便利ですね
#4.ルーティング設定
更新や削除の処理を追加したのでルーティングも設定しときます
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('home');
})->middleware('auth');
// Authentication Routes...
Route::get('/login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('/login', 'Auth\LoginController@login');
Route::post('/logout', 'Auth\LoginController@logout')->name('logout');
// Admin
Route::group( ['middleware' => ['auth', 'can:admin']], function() {
// USER
Route::post('/api/admin/user', 'UserController@index')->name('admin/user');
Route::post('/api/admin/user/store', 'UserController@store')->name('admin/user/store');
Route::post('/api/admin/user/destroy', 'UserController@destroy')->name('admin/user/destroy');
});
// Other
Route::get('/{any}', function () {
return view('home');
})->middleware('auth')->where('any', '.*');
#5.動作確認
いろいろ作ったのでコンパイルして実行
管理者でログインして
npm run dev
php artisan serve --host=172.16.0.100 --port=8000
5-2-1 新規登録ダイアログが開く
5-2-2 情報登録して「保存」
5-2-3 保存確認
5-3 一覧から 編集ボタンを押して
5-3-1 編集ダイアログが開く
5-3-2 情報変更して「保存」(パスワード英文字のみ → エラー)
5-3-3 情報変更して「保存」(パスワード英文字+数字 → エラー)
5-3-4 情報変更して「保存」(パスワード英文字+数字+記号 → OK)
5-4 一覧から 削除ボタンを押して
5-4-1 削除ダイアログが開く
5-4-2 「削除」ボタンを押してユーザを削除
#6.ダイアログと一覧の関係
ちょっとダイアログとの関係について解説をしておく
6-1.親から子へのコンポーネント連携
[親]一覧の「UserComponent.vue」について
・一覧の「編集」ボタンをClickすると dialogOpen[緑色] function を呼び出します
このとき選択した行の情報を引き渡します(props.item[紫色])
・dialogOpen は $refs.userDialog[赤色] のopen[茶色] function を呼び出します
※ <user-dialog > つまり、[子] の UserDialog.vue
このとき、引き渡された情報をそのままさらに引き渡します(props.item -> item)[紫色]
[子] ダイアログの「UserDialog.vue」について
・親から open function [茶色]が呼ばれたら、引数でもらった情報[紫色]を内部変数[水色]にコピーします
内部変数はそのまま Vue.jsの v-model で画面に表示します(v-text-field)
--
こんな感じで親コンポーネントから一覧の「行」情報を子コンポーネントへ引き渡し、子コンポーネントではもらった情報を利用してダイアログ表示してます
6-2.子から親へのコンポーネント連携
子のダイアログで情報を新規追加したり更新・削除したりしたら、親の一覧の情報も更新しないと古い情報のままになってしまいます
そこで、子の処理が終わったところで親へ通知を行っています
[子]ダイアログから親への通知
子の処理が終わったところで親へ通知しているのがここの部分
~~
store() {
~~~
axios.post('/api/admin/user/store', params)
.then( function (response) {
★ this.$emit('reload')
~~
emitで親へ ” reload ” の通知を投げています
別の場所ではサーバ(Laravel)でデータチェックの結果エラーとなった場合の通知も行っています
~~
store() {
~~~
axios.post('/api/admin/user/store', params)
~~~
.catch(function (error) {
~~~
★ this.$emit('setsearch', this.items.loginid)
~~
emitで親へ ” setsearch” の通知と、引数で対象のログインIDを投げています
[親] 一覧での子からの通知を受け取り
子コンポーネントを埋め込むときに受け取り用の設定も行っています
<template>
<v-flex>
<v-card xs12 class="m-3 px-3">
<v-card-title class="title">
<v-icon class="pr-2">{{ $route.meta.icon }}</v-icon> {{ $route.meta.name }} {{ /* 社員管理 */ }}
★ <user-dialog ref="userDialog" @reload="reload" @setsearch="setsearch"></user-dialog>
~~~
子から ” reload " を受け取ったら、reload の function を呼び出す
子から ” setsearch ” を受け取ったら、 setsearch の function を呼び出すように定義してます
子から受け取るメッセージは自由に定義できるし、子から引数でデータを受け取ることも可能なので結構柔軟な仕組みになってますかね
Vue スゴイ!!
今回はデータエラーになった場合にメッセージに出すだけだと対象データを一覧で探すのも大変なので、” setsearch ” で対象者を絞り込んだ状態で表示するようにしてます
---
以上
エラーメッセージとか英語のままなのでそのうち日本語にしてみる
今回もソースはこちら
https://github.com/u9m31/u9m31/tree/step05