Concrete5にVueCLIを使ってUIを構築する。2【コンポーネント作成・データ登録編】
技術スタック concrete5JavascriptVue.js

Concrete5にVueCLIを使ってUIを構築する。2【コンポーネント作成・データ登録編】

2021.05.04 2020.08.27

こんにちはjunです。Concrete5にVueCLIを使ってUIを構築する。1【環境構築編】の記事の続きを書いていきます。

今回の記事では

どんなフォームを作成するのか。 テキストインプットを用いたフォームの簡易実装 データの保存のバックエンド実装 を行いたいと思います。ファイルマネージャー、リッチテキストエディタをvueでレンダリングする方法は追加・編集・一覧化が終わった後に書きたいと思います。まずは簡単なフォーム(テキスト)とデータの保存をvueとともに実装していきましょう。

最終的に作成するフォーム

「アルバム管理」たるものを作成しようと思います。インスタグラム的な物で、機能は以下の通りです。

  • 1つのアルバムに複数枚の画像を自由に登録できる。数は基本的に無制限、最低1枚
  • 画像と一緒にその画像に関する情報をリッチテキストエディタで編集できる。
  • 画像は順番の並び替え、追加、削除が可能。
  • ページ内ではブロックを通じてアルバムの写真を出力できる。

ワイヤーフレーム(超簡素)

DB構造

db.xmlを用いて以下のようなテーブルを作成しておきます。

db.xml
<?xml version="1.0" encoding="UTF-8"?>
<schema
  xmlns="http://www.concrete5.org/doctrine-xml/0.5"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.concrete5.org/doctrine-xml/0.5 http://concrete5.github.io/doctrine-xml/doctrine-xml-0.5.xsd">

<table name="album">
    <field name="id" type="integer">
        <autoincrement/>
        <key/>
    </field>
    <field name="title" type="string" size="255"/>
    <field name="created" type="datetime">
      <default value="1000-01-01 00:00:00"/>
      <notnull/>
    </field>
    <field name="modified" type="timestamp">
      <deftimestamp/>
      <notnull/>
    </field>
</table>

<table name="albumPics">
    <field name="id" type="integer">
        <autoincrement/>
        <key/>
    </field>
    <field name="albumID" type="integer">
        <unsigned/>
    </field>
    <field name="fID" type="integer">
        <unsigned/>
    </field>
    <field name="html" type="text" size="65535"/>
    <field name="created" type="datetime">
      <default value="1000-01-01 00:00:00"/>
      <notnull/>
    </field>
    <field name="modified" type="timestamp">
      <deftimestamp/>
      <notnull/>
    </field>
</table>

</schema>

1アルバムに対して不定数の画像が登録されるので上記の様なDBになっています。

パッケージのビューPHPとvueのSrc構成

パッケージ側(PHP)の下準備

それではアルバムののフォームを作っていきましょう。前回インストールした「vuetest」にてフォーム用のシングルページを作成します。

vuetest
├── controller.php //これはパッケージのcontroller
├── controllers
│   └── single_page
│       └── dashboard
│           └── vuetest.php
└── single_pages
    └── dashboard
        └── vuetest
            ├── add.php   //追加画面シングルページ
            ├── edit.php  //編集画面シングルページ
            └── view.php

そしてシングルページのコントローラーであるvuetest.phpで以下の様に入力

vuetest.php
class Vuetest extends DashboardPageController
{
    public $packageHandle = 'vuetest';

   //vuetest のシングルページでvueのjsファイルを読み込む様にする。
  public function on_start() {
        $this->requireAsset('package-vue-production');
    }

    public function view() {
    }

    // /dashboard/vuetest/ に /add というURLを追加できる
    public function add(){
        $this->render('/dashboard/vuetest/add');
    }
}

すると/dashboard/vuetest/add というURLを叩くとadd.phpが表示されます。

add.php
<?php
defined('C5_EXECUTE') or die('Access Denied.');
?>
<p>add用のページだよ</p>

ページの表示が確認できたら、add.phpに以下の様に変更しておきます。

add.php
<?php
defined('C5_EXECUTE') or die('Access Denied.');
?>
<div id="add"></div>

この<div id=”app></div>としたところがvueコンポーネントを流し込むエントリーになります。フォームのビューファイルであるadd.phpはこれだけでおしまいです。

vue側の準備

ではvue側も作っていきましょう。src配下は以下の様に構成します。

├── src
   ├── add.vue
   ├── components
   │   └── form.vue
   ├── edit.vue
   ├── index.vue
   └── main.js

index.vue、add.vue、edit.vueそれぞれが一覧・追加・編集ページのUIを構築します。しかしedit.vueとadd.vueは入力項目が同じで、初期値があるかないかの違いだけです。そのため共通のコンポーネントform.vueを作成します。

form.vueはひとまず以下の様に設定しておきます。

form.vue
<template>
    <div class="ccm-dashboard-content-inner">
        <p>vueレンダリングテスト</p>
        
        <div class="ccm-dashboard-form-actions-wrapper">
            <div class="ccm-dashboard-form-actions">
                <button class="pull-left btn btn-primary">
                    一覧へ戻る
                </button>
                <a href="#" class="pull-right btn btn-primary">
                    登録
                </a>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    name:'albumForm',
    props:['isEdit']
}
</script>

そしてadd.vueでこのform.vueを読み込んで

app.vue
<template>
    <AlbumForm :isEdit="false"/>
</template>

<script>
import AlbumForm from './components/form';
export default {
    name:'add',
    components:{AlbumForm}
}
</script>

main.jsにてid="app"の要素にマウントする様にします。

main.js
import Vue from 'vue'
import Add from './add.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(Add),
}).$mount('#add')

こうしてビルドをしてみましょう。そして/dashboard/vuetest/addへ移動してみると

しっかりとid="app"form.vueがレンダリングされました。ではこのform.vueにガシガシとフォームUIを作っていきましょう!

まずはタイトルを入力するだけのものを作成

最終的なフォームにはリッチテキストエディタとファイルセレクターなど、concrete5で用いられているフォームをvueで実装したいと思います。しかし、その2つはなかなか曲者で今回の記事で説明すると長くなるので次回に話します。

まずはvueとPHP(concrete5)でどう連携させてDBにデータを挿入するかの全体的な流れについて説明するとともに、タイトル部分(プレーンテキスト)を追加できる様にしていきましょう。

POSTでバックに値を送る

vueで構築したUIで入力された値はどうやってバックに渡すか?簡単です。フォームを作る時の様にinput<form method="post"></form>で囲んであげて、送信用のsubmitボタンを作るだけです。今回のパッケージではPOSTの値をAjaxとかで送るのでなく、普通にsubmitでサーバーに送信する様にしました。以下の様にform.vueを変えます。

form.vue
<template>
    <div class="ccm-dashboard-content-inner">
        <h3>アルバム新規追加</h3>
        <hr>
        <form method="post">
            <label for="title" class="control-label">アルバムタイトル</label>
            <input type="text" name="title" class="form-control ccm-input-text" v-model="title">

            <div class="ccm-dashboard-form-actions-wrapper">
                <div class="ccm-dashboard-form-actions">
                    <button class="pull-left btn btn-primary">
                        一覧へ戻る
                    </button>
                    <input value="登録" type="submit" class="pull-right btn btn-primary">
                </div>
            </div>
        </form>
    </div>
</template>

<script>
export default {
    name:'albumForm',
    props:['isEdit'],
    data(){
        return{
            title:''
        }
    }
}
</script>

フロントバリデーションを実装

フォームから入力された値をバリデーションチェックします。しかし今回はせっかくvueを使っているのでフロントでバリデーションをしてあげましょう。例えばtitleが空でないかをチェックする場合以下の様にします。

form.vue
<template>
    <div class="ccm-dashboard-content-inner">
        <h3>アルバム新規追加</h3>
        <hr>
        <form method="post" @submit="checkForm">
            <label for="title" class="control-label">アルバムタイトル</label>
            <input type="text" name="title" class="form-control ccm-input-text" v-model="title">
            <p class="text-danger" v-if="errTitle.length>0">{{errTitle}}</p>

            <div class="ccm-dashboard-form-actions-wrapper">
                <div class="ccm-dashboard-form-actions">
                    <button class="pull-left btn btn-primary">
                        一覧へ戻る
                    </button>
                    <input value="登録" type="submit" class="pull-right btn btn-primary">
                </div>
            </div>
        </form>
    </div>
</template>
<script>
export default {
    name:'albumForm',
    props:['isEdit'],
    data(){
        return{
            title:'',
            errTitle:''
        }
    },
    methods:{
        checkForm($event){
            if(this.title.length >0){
                return true;
            }else{
                $event.preventDefault();
                ConcreteAlert.error({
                    title:'入力項目に誤りがあります。',
                    message:'アルバムタイトルが入力されていません。',
                    delay:5000
                })
                this.errTitle = "タイトルを入力してください。"
            }
        }
    }
}
</script>

formの箇所に@submit="checkForm"というイベントを作成。これはこのformがsubmitされた際にcheckFormというメソッドを発火させるという意味です。そしてcheckFormではtitleの値が空かどうかを判断しています。

もし空でない場合はreturn trueとなってsubmitが通って、サーバーへ値が送信されます。からの場合は$event.preventDefault();が実行されてsubmitされません。そしてConcreteAlert.errorという8.4系から使用できるconcrete5のフラッシュメッセージのjsを出しています。(ConcreteAlert.errorは特に何も読み込まないでも使える)

空で「登録」を押すと以下の様になります。

$event.preventDefault();によってサーバーにデータは送信されず、ユーザーに対してエラーを表示できました。

ESLintがある時のchips

ConcreteAlert はconcrete5が用意してくれた便利なフラッシュメッセージです。しかし、ESLint付きのvueCLI内で使用ようとすると、ビルド時にこの様に怒られます。

ERROR  Failed to compile with 1 errors                                                                                                       23:50:16

 error  in ./src/components/form.vue

Module Error (from ./node_modules/eslint-loader/index.js):

/Applications/MAMP/htdocs/c5test/packages/vuetest/js/packageui/src/components/form.vue
  40:17  error  'ConcreteAlert' is not defined  no-undef

✖ 1 problem (1 error, 0 warnings)

そうです。vueプロジェクト内にはConcreteAlert を定義したjsファイルがない、というかconcreteが用意したjsを読み込めないのでこの様に怒られます。これだとビルドできないのでpackage.jsonのeslintの設定に以下の記述をします。

 "eslintConfig": {
  "globals":{
      "ConcreteAlert": true,
    }
 },

こうするとESLintは「ConcreteAlertってのはグローバルな奴なんだな〜。」と認識してくれて、実際にvueプロジェクト外にあるConcreteAlertに対して怒らなくなります。

バックエンド実装

vueを用いてまずはアルバムのタイトルだけを入力できるフォームを作りました。そしてこのタイトルをDBに挿入するまで行います。と言ってもシングルページコントローラーを以下の様に記述します。

vuetest.php
<?php
namespace Concrete\Package\Vuetest\Controller\SinglePage\Dashboard;
defined('C5_EXECUTE') or die('Access Denied.');
use \Concrete\Core\Page\Controller\DashboardPageController;
use Concrete\Core\Routing\Redirect;
use Concrete\Core\Http\Request;


use Core;
use Database;

class Vuetest extends DashboardPageController
{
    public $packageHandle = 'vuetest';

    public function on_start()
    {
        $this->requireAsset('package-vue-production');
    }

    public function view() {
    }

    public function add(){
        if(Request::isPost() == true){
            $title = $this->post('title');

            if(empty($title)==false){
                $db = Database::connection();
                $db->executeQuery("START TRANSACTION");
                $db->executeQuery(
                    'INSERT album SET `title`=?, `created`=now(), `modified`=now()',
                    array($title)
                );
                $db->executeQuery("COMMIT");
                Redirect::to('/dashboard/vuetest')->send();
            }else{
                Redirect::to('/dashboard/vuetest')->send();
            }

        }else{
            $this->render('/dashboard/vuetest/add');
        }
    }
}

/dashboard/vuetest/add でpostを送るとadd()にて処理が行われます。Request::isPost()というメソッドを用いてリクエストがpostかどうかをチェックします。postであれば値をDBへ挿入するスクリプトを実行し、そうでなければ新規追加の画面を表示します。

DashboardPageController配下では$this->post('name') というメソッドで対応するname属性のinputの値を取得することができます!先ほどのフォームではタイトルの値をname="title"としていたので$title = $this->post('title');で取得します

chips 必ずバックエンドでもバリデーションを実装する

よくみると下記の様にタイトルの値を検査しています。

vuetest.php
if(empty($title)==false){
 ...
}
「フロントエンド でタイトルの値をバリデーションしたから別にやらなくても良くない?」というのは厳禁です。postで来た値は必ずバックエンドで同様にバリデーションをかけます。なぜならブラウザのconsoleでフロントでの値は偽造することもでき、フロントエンドのバリデーションを不正にスルーできるからです。

vueで行っているバリデーションはあくまでユーザー補助、UX的な物でありセキュリティの観点からは言えばガバガバです。エンドユーザーが偽造ができないバックエンドであれば確実にバリデーションをすることができます。

dashbord配下は基本的にサイト管理者が触る物なので、不正な値を入れようとする人はいないと思いますが、フロントからpostされた値は基本的に信用しないスタイルを貫いた方が無難です。

データを入れてみる

では早速使ってみましょう。/dashboard/vuetest/add にアクセスするとタイトル入力フォームが出てきました。仮に「テスト」と入力。そして「登録」を押します。

一覧のページにリダイレクトされました。ちゃんと挿入されたか、phpmyadminでみてみましょう。

いましたね。titleが「テスト」となっているので、正しくデータが入力されました。

次回は…

以上がvueとバックエンド部分の一通りの実装でした。

  • シングルページにvueのエントリーポイントを作る
  • エントリポイントにレンダリングされる様にコンポーネントを設定
  • jsをシングルページに読み込む

¥以上を意識すればconcrete5のシングルページに自由にvueを用いてUIを構築できます。あとはvueの使い方とバックエンドの設計を頑張るだけです。そして次回は編集画面と一覧画面の作成をしていきます。

Copyright © 2021 jun. All rights reserved.