Laravel mix vue No.10 - Vue Font Awesome, Vue Formulate, etc - UIの作り込み

こんにちは、あすかのkoheiです。

今回はUIを「Vue Font Awesome」、「Vue Formulate」を使ってもっと使いやすくします。

連載記事

Vue Font Awesome, Vue Formulate, etc - UIの作り込み

サンプル


フォルダ構成


└─ server
   ├─ resources
   |  ├─ js
   |  |  ├─ components
   |  |  |  └─ Header.vue
   |  |  ├─ store
+  |  |  |  ├─ loading.js
   |  |  |  ├─ auth.js
   |  |  |  └─ index.js
   |  |  ├─ pages
   |  |  |  ├─ Home.vue
   |  |  |  ├─ Login.vue
   |  |  |  └─ Reset.vue
+  |  |  ├─ css
+  |  |  |  ├─ reset.css
+  |  |  |  └─ style.css
+  |  |  ├─ sass
+  |  |  |  ├─ snow
+  |  |  |  |   ├─ _inputs.scss
+  |  |  |  |   └─ _variables.scss
+  |  |  |  └─ main.scss
   |  |  ├─ bootstrap.js
   |  |  └─ app.js
   |  └─ views
   |      └─ index.blade.php
   └─ webpack.mix.js

dockerスタート

gitからクローンした場合は.envの作成と設定を忘れないように!

# コンテナスタート
docker-compose start

# コンテナに入る
docker-compose exec php bash

# composerをインストール(前回からの続きで行う場合はいらない)
composer install

# npmをインストール(前回からの続きで行う場合はいらない)
npm i

# encryption keyを作成(前回からの続きで行う場合はいらない)
php artisan key:generate

# ホットリリード開始
npm run watch

フォントアイコンを使う

今回はfontawesomeを使用する

vue-fontawesome

インストール

fontawesomeをインストール


    npm i @fortawesome/fontawesome-svg-core
    npm i @fortawesome/free-solid-svg-icons
    npm i @fortawesome/free-regular-svg-icons
    npm i @fortawesome/free-brands-svg-icons
    npm i @fortawesome/vue-fontawesome

Vueで使う準備

server\resources\js\app.jsを編集


    ...

+   /**
+    * fontawesome
+    * https://github.com/FortAwesome/vue-fontawesome
+    * http://l-lin.github.io/font-awesome-animation/
+    */

+   // コアのインポート
+   import { library } from "@fortawesome/fontawesome-svg-core";
+
+   // 無料で使えるフォントをインポート
+   import { fab } from "@fortawesome/free-brands-svg-icons";
+   import { far } from "@fortawesome/free-regular-svg-icons";
+   import { fas } from "@fortawesome/free-solid-svg-icons";

+   // コンポネントをインポート
+   import { FontAwesomeIcon, FontAwesomeLayers, FontAwesomeLayersText } from "@fortawesome/vue-fontawesome";

+   // ライブラリに追加
+   library.add(fas, far, fab);

+   // コンポーネントを名前を指定して追加
+   // 名前は自由にきめてOK
+   Vue.component("FAIcon", FontAwesomeIcon);
+   Vue.component('FALayers', FontAwesomeLayers);
+   Vue.component('FAText', FontAwesomeLayersText);
+   ...

使用例


    <!-- アイコンの指定 -->
    <FAIcon icon="coffee" />
    <FAIcon :icon="['fas', 'coffee']" />

    <!-- サイズ -->
    <!--  xs, sm, lg, 2x, 3x ... 10x  -->
    <FAIcon :icon="['fas', 'coffee']" size="xs" />
    <FAIcon :icon="['fas', 'coffee']" size="6x" />

    <!-- 等幅フォント -->
    <FAIcon :icon="['fas', 'coffee']" fixed-width />

    <!-- 回転 -->
    <FAIcon :icon="['fas', 'coffee']" rotation="90" />

    <!-- フリップ -->
    <FAIcon :icon="['fas', 'coffee']" flip="horizontal" />
    <FAIcon :icon="['fas', 'coffee']" flip="vertical" />
    <FAIcon :icon="['fas', 'coffee']" flip="both" />

    <!-- アニメーション -->
    <FAIcon icon="spinner" spin />
    <FAIcon icon="spinner" pulse />

    <!-- 枠線 -->
    <FAIcon :icon="['fas', 'coffee']" border />

    <!-- プル -->
    <FAIcon :icon="['fas', 'coffee']" pull="left" />
    <FAIcon :icon="['fas', 'coffee']" pull="right" />

    <!-- 色の反転 -->
    <FAIcon :icon="['fas', 'coffee']" inverse :style="{'background-color': 'black'}" />

    <!-- トランスフォーム Scaling -->
    <FAIcon :icon="['fas', 'coffee']" transform="shrink-8" />
    <FAIcon :icon="['fas', 'coffee']" />
    <FAIcon :icon="['fas', 'coffee']" transform="grow-8" />

    <!-- トランスフォーム Positioning -->
    <FAIcon :icon="['fas', 'coffee']" transform="shrink-8" :style="{'background-color': 'rgb(0 0 0 / 25%)'}" />
    <FAIcon :icon="['fas', 'coffee']" transform="shrink-8 up-3 left-3" :style="{'background-color': 'rgb(0 0 0 / 25%)'}" />
    <FAIcon :icon="['fas', 'coffee']" transform="shrink-8 down-3 right-3" :style="{'background-color': 'rgb(0 0 0 / 25%)'}" />

    <!-- トランスフォーム Positioning Flipping -->
    <FAIcon :icon="['fas', 'coffee']" transform="rotate-90" />
    <FAIcon :icon="['fas', 'coffee']" transform="rotate-180" />
    <FAIcon :icon="['fas', 'coffee']" :transform="{rotate: '270'}" />

    <!-- トランスフォーム Positioning Flipping -->
    <FAIcon :icon="['fas', 'coffee']" transform="flip-v" />
    <FAIcon :icon="['fas', 'coffee']" transform="flip-h" />
    <FAIcon :icon="['fas', 'coffee']" transform="flip-v flip-h" />

    <!-- マスキング -->
    <FAIcon :icon="['fas', 'coffee']" :mask="['fas', 'circle']" transform="shrink-8" size="2x" />

    <!-- レイヤリング -->
    <FALayers class="fa-2x">
        <FAIcon :icon="['fas', 'circle']" />
        <FAIcon :icon="['fas', 'times']" transform="shrink-3" inverse />
    </FALayers>

    <FALayers class="fa-2x">
        <FAIcon :icon="['fas', 'play']" transform="rotate--90 grow-2" />
        <FAIcon :icon="['fas', 'sun']" inverse transform="shrink-10 up-2" />
        <FAIcon :icon="['fas', 'moon']" inverse transform="shrink-11 down-4.2 left-4" />
        <FAIcon :icon="['fas', 'star']" inverse transform="shrink-11 down-4.2 right-3" />
    </FALayers>

    <FALayers class="fa-2x">
        <FAIcon icon="calendar"/>
        <FAText class="fa-inverse" transform="shrink-8 down-3.5" :value="30" />
    </FALayers>

    <FALayers class="fa-2x">
        <FAIcon icon="certificate"/>
        <FAText transform="shrink-11.5 rotate--30" value="NEW" :style="{'color': 'white'}" />
    </FALayers>

    <FALayers class="fa-4x">
        <FAIcon icon="envelope"/>
        <FAText counter value="21" />
    </FALayers>
    <FALayers class="fa-4x">
        <FAIcon icon="envelope"/>
        <FAText counter value="22" position="bottom-left" style="background:Tomato" />
    </FALayers>

複雑なアニメーションに対応

インストール

「font-awesome-animation」をインストール

font-awesome-animation


    npm i font-awesome-animation

Vueで使う準備

server\webpack.mix.jsに追加してpublic/css/app.cssを作成させる


    const mix = require('laravel-mix');

        mix.js("resources/js/app.js", "public/js")
        // 配列で渡したcssを「public/css/app.css」にまとめる
+       .styles([
+           'node_modules/font-awesome-animation/dist/font-awesome-animation.min.css'
+       ], 'public/css/app.css');

        ...

server\resources\views\index.blade.phpにスタイルシートを追加


    <!doctype html>
    <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ config('app.name') }}</title>
    <script src="{{ mix('js/app.js') }}" defer></script>
+   <link href="{{ mix('css/app.css') }}" rel="stylesheet">
+   <link href="{{ mix('css/main.css') }}" rel="stylesheet">
    </head>
    <body>
    <div id="app"></div>
    </body>
    </html>

「watch」で監視できないのでもう一度


    npm run watch

使用例

モーション

  • faa-wrench
  • faa-ring
  • faa-horizontal
  • faa-vertical
  • faa-flash
  • faa-bounce
  • faa-spin
  • faa-float
  • faa-pulse
  • faa-shake
  • faa-tada
  • faa-passing
  • faa-passing
  • faa-burst
  • faa-falling
  • faa-rising

モード

  • animated
  • animated-hover

スピード

  • faa-fast
  • faa-slow

親要素でホバー

  • faa-parent

    <!-- 基本 -->
    <FAIcon icon="coffee" class="faa-wrench animated" />

    <!-- 速度 -->
    <FAIcon icon="coffee" class="faa-wrench animated faa-slow" />
    <FAIcon icon="coffee" class="faa-wrench animated faa-fast" />

    <!-- ホバー -->
    <FAIcon icon="coffee" class="faa-wrench animated-hover" />

    <!-- 親要素でホバー -->
    <div class="faa-parent animated-hover">
        <FAIcon icon="coffee" class="faa-wrench" />coffee
    </div>

フォームを使いやすくする

インストール

Vue Formulateをインストール


    npm i @braid/vue-formulate

Vueで使う準備

server\resources\js\app.jsを編集


    ...

+   /**
+    * VueFormulate
+    * https://vueformulate.com/guide/
+    * https://vueformulate.com/guide/internationalization/#registering-a-locale
+    * https://vueformulate.com/guide/custom-inputs/#custom-types
+    */

+   // コアをインポート
+   import VueFormulate from "@braid/vue-formulate";

+   // 言語をインポート
+   import { en, ja } from "@braid/vue-formulate-i18n";

+   // 宣言
+   Vue.use(VueFormulate, {
+       // 使用するプラグイン
+       plugins: [en, ja],
+       // グローバルに使う独自ルール
+       rules: {
+           // ex
+           foobar: ({ value }) => ["foo", "bar"].includes(value)
+       }
+   });

    ...

テーマを入れる

デフォルトテーマを入れる https://vueformulate.com/guide/theming/#default-theme

あとでテーマをカスタマイズしたいのでserver\node_modules\@braid\vue-formulate\themes\snowフォルダをコピーして server\resources\sassの中に入れる

server\resources\sass\main.scssを作成してインポートする


        @import './snow/snow.scss';

server\webpack.mix.jsを編集


    const mix = require('laravel-mix');

        mix.js("resources/js/app.js", "public/js")
+         // 「public/css」に「/main.css」として作成
+         .sass('resources/sass/main.scss', 'public/css')
            // 配列で渡したcssを「public/css/app.css」にまとめる
            .styles([
                'node_modules/font-awesome-animation/dist/font-awesome-animation.min.css'
            ], 'public/css/app.css');

    ...

Api通信中のステータスをvuexで管理

ストアを作成する

server\resources\js\store\loading.jsとしてローディングストアを作成


    /*
     * ステート(データの入れ物)
     */
    const state = {
        status: false
    };

    /*
     * ミューテーション(同期処理)
     */
    const mutations = {
        setStatus(state, status) {
            state.status = status;
        }
    };

    /*
     * エクスポート
     */
    export default {
        namespaced: true,
        state,
        mutations
    };

ストアインデックスserver\resources\js\store\index.jsに追加


    import Vue from "vue";
    import Vuex from "vuex";

    // 各ストアのインポート
    import auth from "./auth";
    import error from "./error";
    import message from "./message";
+   import loading from './loading'

    Vue.use(Vuex);

    const store = new Vuex.Store({
        modules: {
            // 各ストアと登録
            auth,
            error,
            message,
+           loading,
        }
    });

    export default store;

通信を状態をわかるようにする

Api通信中のみステータスをtrueにするためserver\resources\js\bootstrap.jsを編集

ついでにLaravelのApiはすべて「/api/xxx」となるのでaxiosにbaseURLを設定しておく


    // クッキーを簡単に扱えるモジュールをインポート
    import Cookies from "js-cookie";
    // ストアをインポート
+   import store from "./store";

    /*
    * lodash
    * あると便利のなのでそのままおいておく
    */
    window._ = require("lodash");

    /*
    * axios
    * Ajax通信にはこれを使う
    */
    window.axios = require("axios");

    // Ajaxリクエストであることを示すヘッダーを付与する
    window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";

    // ベースURLの設定
+   window.axios.defaults.baseURL = 'api/';

    // requestの設定
    window.axios.interceptors.request.use(config => {

        // ローディングストアのステータスをTRUE
+       store.commit("loading/setStatus", true);

        // クッキーからトークンを取り出す
        const xsrfToken = Cookies.get("XSRF-TOKEN");
        // ヘッダーに添付する
        config.headers["X-XSRF-TOKEN"] = xsrfToken;
        return config;
    });

    // responseの設定
    // API通信の成功、失敗でresponseの形が変わるので、どちらとも response にレスポンスオブジェクトを代入
    window.axios.interceptors.response.use(
        // 成功時の処理
        response => {
            // ローディングストアのステータスをFALSE
+           store.commit("loading/setStatus", false);
            return response;
        },
        // 失敗時の処理
        error => {
            // ローディングストアのステータスをFALSE
+           store.commit("loading/setStatus", false);
            return error.response || error;
        }
    );

    ...

ベースURLを設定したのでaxiosのURLを修正

server\resources\js\components\Header.vueを修正


    ...

    export default {
        data() {
            ...
        },
        computed: {
            ...
        },
        storage: {
            ...
        },
        methods: {
            async logout() {
                ...
            },
            // 言語切替メソッド
            changeLang() {
                // ローカルストレージに「language」をセット
                this.$storage.set("language", this.selectedLang);
                // Apiリクエスト 言語を設定
-               axios.get(`/api/set-lang/${this.selectedLang}`);
+               axios.get(`set-lang/${this.selectedLang}`);
                // Vue i18n の言語を設定
                this.$i18n.locale = this.selectedLang;
            }
        },
        created() {
            // ローカルストレージから「language」を取得
            this.selectedLang = this.$storage.get("language");

            // サーバ側をクライアント側に合わせる

            // storageLangがない場合
            if (!this.selectedLang) {
                // ブラウザーの言語を取得
                const defaultLang = Helper.getLanguage();
                // ローカルストレージに「language」をセット
                this.$storage.set("language", defaultLang);
                // Apiリクエスト 言語を設定
-               axios.get(`/api/set-lang/${defaultLang}`);
+               axios.get(`set-lang/${defaultLang}`);
            }
            // ある場合はサーバ側をクライアント側に合わせる
            else {
-               axios.get(`/api/set-lang/${this.selectedLang}`);
+               axios.get(`set-lang/${this.selectedLang}`);
            }
        }
    };

server\resources\js\store\auth.jsを修正


    ...

    /*
     * アクション(非同期処理)
     */
    const actions = {
        /*
        * loginのアクション
        */
        async login(context, data) {
            // apiStatusのクリア
            context.commit("setApiStatus", null);

            // Apiリクエスト
-           const response = await axios.post("/api/login", data);
+           const response = await axios.post("login", data);

            ...
        },
        /*
        * registerのアクション
        */
        async register(context, data) {
            // apiStatusのクリア
            context.commit("setApiStatus", null);

            // Apiリクエスト
-           const response = await axios.post("/api/register", data);
+           const response = await axios.post("register", data);

            ...
        },
        /*
        * logoutのアクション
        */
        async logout(context) {
            // apiStatusのクリア
            context.commit("setApiStatus", null);

            // Apiリクエスト
-           const response = await axios.post("/api/logout", data);
+           const response = await axios.post("logout");

            ...
        },
        /*
        * forgotのアクション
        */
        async forgot(context, data) {
            // apiStatusのクリア
            context.commit("setApiStatus", null);

            // Apiリクエスト
-           const response = await axios.post("/api/forgot", data);
+           const response = await axios.post("forgot", data);

            ...
        },
        /*
        * resetのアクション
        */
        async reset(context, data) {
            // apiStatusのクリア
            context.commit("setApiStatus", null);

            // Apiリクエスト
-           const response = await axios.post("/api/reset", data);
+           const response = await axios.post("reset", data);

            ...
        },
        /*
        * カレントユーザのアクション
        */
        async currentUser(context) {
            // apiStatusのクリア
            context.commit("setApiStatus", null);

            // Apiリクエスト
-           const response = await axios.post("/api/user", data);
+           const response = await axios.get("user");

            ...
        }
    };
    ...

上記で作成したものを実装

コンポネントに埋め込み

server\resources\js\pages\Login.vueに埋め込んでいく


    <template>
-       <div class="container">
+       <div class="page">

+           <h1>{{ $t('word.login') }}</h1>

            <!-- tabs -->
            ...
            <!-- /tabs -->

            <!-- login -->
-           <section class="login" v-show="tab === 1">
+           <section class="login panel" v-show="tab === 1">
-               <h2>{{ $t('word.login') }}</h2>

                <!-- errors -->
                ...
                <!--/ errors -->

                <!-- @submitで login method を呼び出し -->
-               <form @submit.prevent="login">
-                   <div>{{ $t('word.email') }}</div>
-                   <div>
-                       <!-- v-modelでdataをバインド -->
-                       <input type="email" v-model="loginForm.email" />
-                   </div>
-                   <div>{{ $t('word.password') }}</div>
-                   <div>
-                       <input type="password" v-model="loginForm.password" />
-                   </div>
-                   <div>
-                       <button type="submit">{{ $t('word.login') }}</button>
-                   </div>
-               </form>

+               <FormulateForm name="login_form" v-model="loginForm" @submit="login">
+                   <FormulateInput
+                       name="email"
+                       type="email"
+                       :label="$t('word.email')"
+                       :validation-name="$t('word.email')"
+                       validation="required|email"
+                       :placeholder="$t('word.email')"
+                   />
+                   <FormulateInput
+                       name="password"
+                       type="password"
+                       :label="$t('word.password')"
+                       :validation-name="$t('word.password')"
+                       validation="required|min:8"
+                       :placeholder="$t('word.password')"
+                   />
+                   <FormulateInput type="submit" :disabled="loadingStatus">
+                       {{ $t('word.login') }}
+                       <FAIcon v-if="loadingStatus" :icon="['fas', 'spinner']" pulse fixed-width/>
+                   </FormulateInput>
+               </FormulateForm>

                <h2>{{ $t('word.Socialite') }}</h2>
-               <a class="button" href="/login/twitter" title="twitter">twitter</a>
+               <a class="button" href="/login/twitter" title="twitter">
+                       <FAIcon :icon="['fab', 'twitter']" size="2x" :class="['faa-tada', 'animated-hover']"/>
+               </a>

            </section>
            <!-- /login -->

            <!-- register -->
-           <section class="register" v-show="tab === 2">
+           <section class="register panel" v-show="tab === 2">
-               <h2>{{ $t('word.register') }}</h2>
                <!-- errors -->
                <div v-if="registerErrors" class="errors">
                    <ul v-if="registerErrors.name">
                        <li v-for="msg in registerErrors.name" :key="msg">{{ msg }}</li>
                    </ul>
                    <ul v-if="registerErrors.email">
                        <li v-for="msg in registerErrors.email" :key="msg">{{ msg }}</li>
                    </ul>
                    <ul v-if="registerErrors.password">
                        <li v-for="msg in registerErrors.password" :key="msg">{{ msg }}</li>
                    </ul>
                </div>
                <!--/ errors -->
-               <form @submit.prevent="register">
-                   <div>{{ $t('word.name') }}</div>
-                   <div>
-                       <input type="text" v-model="registerForm.name" />
-                   </div>
-                   <div>{{ $t('word.email') }}</div>
-                   <div>
-                       <input type="email" v-model="registerForm.email" />
-                   </div>
-                   <div>{{ $t('word.password') }}</div>
-                   <div>
-                       <input type="password" v-model="registerForm.password" />
-                   </div>
-                   <div>{{ $t('word.password_confirmation') }}</div>
-                   <div>
-                       <input type="password" v-model="registerForm.password_confirmation" />
-                   </div>
-                   <div>
-                       <button type="submit">{{ $t('word.register') }}</button>
-                   </div>
-               </form>

+               <FormulateForm name="register_form" v-model="registerForm" @submit="register">
+                   <FormulateInput
+                       name="name"
+                       type="text"
+                       :label="$t('word.name')"
+                       :validation-name="$t('word.name')"
+                       validation="required|max:50"
+                       :placeholder="$t('word.name')"
+                   />
+                   <FormulateInput
+                       name="email"
+                       type="email"
+                       :label="$t('word.email')"
+                       :validation-name="$t('word.email')"
+                       validation="required|email"
+                       :placeholder="$t('word.email')"
+                   />
+                   <FormulateInput
+                       name="password"
+                       type="password"
+                       :label="$t('word.password')"
+                       :validation-name="$t('word.password')"
+                       validation="required|min:8"
+                       :placeholder="$t('word.password')"
+                   />
+                   <FormulateInput
+                       name="password_confirmation"
+                       type="password"
+                       :label="$t('word.password_confirmation')"
+                       :validation-name="$t('word.password_confirmation')"
+                       validation="required|min:8"
+                       :placeholder="$t('word.password_confirmation')"
+                   />
+                   <FormulateInput type="submit" :disabled="loadingStatus">
+                       {{ $t('word.register') }}
+                       <FAIcon v-if="loadingStatus" :icon="['fas', 'spinner']" pulse fixed-width/>
+                   </FormulateInput>
+               </FormulateForm>
            </section>
            <!-- /register -->

            <!-- forgot -->
-           <section class="forgot" v-show="tab === 3">
+           <section class="forgot panel" v-show="tab === 3">
-               <h2>forgot</h2>
                <!-- errors -->
                <div v-if="forgotErrors" class="errors">
                    <ul v-if="forgotErrors.email">
                        <li v-for="msg in forgotErrors.email" :key="msg">{{ msg }}</li>
                    </ul>
                </div>
                <!--/ errors -->
-               <form @submit.prevent="forgot">
-                   <div>{{ $t('word.email') }}</div>
-                   <div>
-                       <input type="email" v-model="forgotForm.email" />
-                   </div>
-                   <div>
-                       <button type="submit">{{ $t('word.send') }}</button>
-                   </div>
-               </form>

+               <FormulateForm name="forgot_form" v-model="forgotForm" @submit="forgot">
+                   <FormulateInput
+                       name="email"
+                       type="email"
+                       :label="$t('word.email')"
+                       :validation-name="$t('word.email')"
+                       validation="required|email"
+                       :placeholder="$t('word.email')"
+                   />
+                   <FormulateInput type="submit" :disabled="loadingStatus">
+                       {{ $t('word.send') }}
+                       <FAIcon v-if="loadingStatus" :icon="['fas', 'spinner']" pulse fixed-width/>
+                   </FormulateInput>
+               </FormulateForm>
            </section>
            <!-- /forgot -->
        </div>
    </template>

    <script>
    export default {
        data() {
            ...
        },
        // 算出プロパティでストアのステートを参照
        computed: {
            // authストアのapiStatus
            apiStatus() {
                return this.$store.state.auth.apiStatus;
            },
            // authストアのloginErrorMessages
            loginErrors() {
                return this.$store.state.auth.loginErrorMessages;
            },
            // authストアのregisterErrorMessages
            registerErrors() {
                return this.$store.state.auth.registerErrorMessages;
            },
            // authストアのforgotErrorMessages
            forgotErrors() {
                return this.$store.state.auth.forgotErrorMessages;
            },
+           // loadingストアのstatus
+           loadingStatus() {
+               return this.$store.state.loading.status;
+           }
        },

        ...

        methods: {
                    clearForm() {
-              login form
-              this.loginForm.email = "";
-              this.loginForm.password = "";
-              register form
-              this.registerForm.name = "";
-              this.registerForm.email = "";
-              this.registerForm.password = "";
-              this.registerForm.password_confirmation = "";
-              forgot form
-              this.forgot.email = "";
+             this.$formulate.reset('login_form')
+             this.$formulate.reset('register_form')
+             this.$formulate.reset('forgot_form')
                        }
                }
    };
    </script>

    <template>
        <div class="container--small">
-           <h2>{{ $t('word.password_reset') }}</h2>
+           <h1>{{ $t('word.password_reset') }}</h1>

-           <div class="panel">
+           <section class="page" >
-               <form class="form" @submit.prevent="reset">
                    <!-- errors -->
                    <div v-if="resetErrors" class="errors">
                        <ul v-if="resetErrors.password">
                            <li v-for="msg in resetErrors.password" :key="msg">{{ msg }}</li>
                        </ul>
                        <ul v-if="resetErrors.token">
                            <li v-for="msg in resetErrors.token" :key="msg">{{ msg }}</li>
                        </ul>
                    </div>
                    <!--/ errors -->

-                   <div>
-                       <input type="password" v-model="resetForm.password" />
-                   </div>
-                   <div>
-                       <input type="password" v-model="resetForm.password_confirmation" />
-                   </div>
-                   <div>
-                       <button type="submit">{{ $t('word.reset') }}</button>
-                   </div>
-               </form>

+           <FormulateForm name="reset_form" v-model="resetForm" @submit="reset">
+               <FormulateInput
+                   name="password"
+                   type="password"
+                   :label="$t('word.password')"
+                   :validation-name="$t('word.password')"
+                   validation="required|min:8"
+                   :placeholder="$t('word.password')"
+               />
+               <FormulateInput
+                   name="password_confirmation"
+                   type="password"
+                   :label="$t('word.password_confirmation')"
+                   :validation-name="$t('word.password_confirmation')"
+                   validation="required|min:8"
+                   :placeholder="$t('word.password_confirmation')"
+               />
+               <FormulateInput type="submit" :disabled="loadingStatus">
+                   {{ $t('word.reset') }}
+                   <FAIcon v-if="loadingStatus" :icon="['fas', 'spinner']" pulse fixed-width />
+               </FormulateInput>
+           </FormulateForm>
-           </div>
+           </section>
        </div>
    </template>

    <script>
    import Cookies from "js-cookie";

    export default {
        // vueで使うデータ
        data() {
            ...
        },
        computed: {
            // authストアのapiStatus
            apiStatus() {
                return this.$store.state.auth.apiStatus;
            },
            // authストアのresetErrorMessages
            resetErrors() {
                return this.$store.state.auth.resetErrorMessages;
            },
+           // loadingストアのstatus
+           loadingStatus() {
+               return this.$store.state.loading.status;
+           }
        },

        ...
    };
    </script>

server\resources\js\components\Header.vueを修正


    <template>
-       <header>
-           <!-- リンクを設定 -->
-           <RouterLink to="/">{{ $t('word.home') }}</RouterLink>
-           <RouterLink v-if="!isLogin" to="/login">{{ $t('word.login') }}</RouterLink>
-           <!-- ログインしている場合はusernameを表示 -->
-           <span v-if="isLogin">{{username}}</span>
-           <!-- クリックイベントにlogoutメソッドを登録 -->
-           <span v-if="isLogin" @click="logout">logout</span>
-           <!-- 言語切替 -->
-           <select v-model="selectedLang" @change="changeLang">
-               <option
-                   v-for="lang in langList"
-                   :value="lang.value"
-                   :key="lang.value"
-               >{{ $t(`word.${lang.text}`) }}</option>
-           </select>
-       </header>

+       <header class="header">
+           <RouterLink to="/">
+               <FAIcon :icon="['fas', 'home']" size="lg" />
+               {{ $t('word.home') }}
+           </RouterLink>
+   
+           <RouterLink v-if="!isLogin" to="/login">
+               <FAIcon :icon="['fas', 'sign-in-alt']" size="lg" />
+               {{ $t('word.login') }}
+           </RouterLink>
+   
+           <span v-if="isLogin">
+               <FAIcon :icon="['fas', 'child']" size="lg" />
+               {{username}}
+           </span>
+   
+           <span v-if="isLogin" @click="logout" class="button">
+               <FAIcon :icon="['fas', 'sign-out-alt']" size="lg" />
+               {{ $t('word.logout') }}
+           </span>
+
+           <FormulateInput @input="changeLang" v-model="selectedLang" :options="langList" type="select" class="header-lang" />
        </header>
    </template>

        <script>
        ...

    data() {
        return {
            // 言語選択オプション
            langList: [
-              { value: "en", text: "english" },
+              { value: "en", label: "english" },
-              { value: "ja", text: "japanese" },
+              { value: "ja", label: "japanese" },
            ],
            // 選択された言語
            selectedLang: "en",
        };
    },

        ...

    methods: {

        ...

        // 言語切替メソッド
        changeLang() {
            // ローカルストレージに「language」をセット
            this.$storage.set("language", this.selectedLang);
            // Apiリクエスト 言語を設定
            axios.get(`set-lang/${this.selectedLang}`);

            // Vue i18n の言語を設定
            this.$i18n.locale = this.selectedLang;
        },
    },

    ...

    </script>

server\resources\js\pages\Home.vueを修正

    <template>
-   <div class="container">
+   <div class="page">

        <h1>{{ $t('word.home') }}</h1>

    </div>
    </template>

スタイルを追加

リセットスタイルserver\resources\css\reset.cssを作成する


    html,
    body {
        height: 100%;

    }

    /* Box sizingの定義 */
    *,
    *::before,
    *::after {
        box-sizing: border-box;
    }

    /* デフォルトのpaddingを削除 */
    ul[class],
    ol[class] {
        padding: 0;
    }

    /* デフォルトのmarginを削除 */
    body,
    h1,
    h2,
    h3,
    h4,
    p,
    ul[class],
    ol[class],
    li,
    figure,
    figcaption,
    blockquote,
    dl,
    dd {
        margin: 0;
        font-size: unset;
        font-weight: normal;
    }

    /* bodyのデフォルトを定義 */

    body {
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
            "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
            "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
        min-height: 100vh;
        scroll-behavior: smooth;
        text-rendering: optimizeSpeed;
        line-height: 1.5;
    }

    /* class属性を持つul、ol要素のリストスタイルを削除 */
    ul[class],
    ol[class] {
        list-style: none;
    }

    /* classを持たない要素はデフォルトのスタイルを取得 */
    a:not([class]) {
        text-decoration-skip-ink: auto;
    }

    /* img要素の扱いを簡単にする */
    img {
        max-width: 100%;
        display: block;
    }

    /* article要素内の要素に自然な流れとリズムを定義 */
    article > * + * {
        margin-top: 1em;
    }

    /* inputやbuttonなどのフォントは継承を定義 */
    input,
    button,
    textarea,
    select {
        font: inherit;
    }

    button {
        border: 0;
        outline: none;
    }

    /* 見たくない人用に、すべてのアニメーションとトランジションを削除 */

    @media (prefers-reduced-motion: reduce) {
        * {
            animation-duration: 0.01ms !important;
            animation-iteration-count: 1 !important;
            transition-duration: 0.01ms !important;
            scroll-behavior: auto !important;
        }
    }

    h1 {
        margin: 0;
        padding: 0;
        color: dodgerblue;
        border-bottom: solid thin dodgerblue;
        margin: 2rem 0;
        font-weight: bold;
    }

    .wrapper {
        width: clamp(600px, 80%, 1000px);
        margin: 5vh auto;
        border: solid 3px dodgerblue;
        min-height: 90vh;
    }

    .header {
        padding: 1rem 1rem 0;
        display: flex;
    }

    .header>* {
        margin: 1rem;
        text-decoration: none;
        color: gray;
    }

    .header>*:last-child {
        margin-left: auto;
    }

    .header .router-link-exact-active {
        pointer-events: none;
        color: dodgerblue;
    }

    .container {
        padding: 2rem;
    }

    .tab {
        padding: 0;
        display: flex;
        list-style: none;
        transform: translateY(1px);
    }

    .tab__item {
        background-color: lightgrey;
        border: 1px solid gray;
        border-radius: 0.3rem 0.3rem 0 0;
        padding: 0.5rem 1rem 0.4rem;
        margin-right: 0.5rem;
        cursor: pointer;
    }

    .tab__item--active {
        background-color: white;
        border-bottom: 1px solid white;
    }

    .panel {
        border: solid 1px gray;
        padding: 2rem;
    }

    .button {
        cursor: pointer;
    }

server\webpack.mix.jsを編集


    const mix = require('laravel-mix');

        mix.js("resources/js/app.js", "public/js")
        .sass('resources/sass/main.scss', 'public/js')
        // 配列で渡したcssを「public/css/app.css」にまとめる
        .styles([
+           'resources/css/reset.css',
+           'resources/css/style.css',
            'node_modules/font-awesome-animation/dist/font-awesome-animation.min.css'
        ], 'public/css/app.css');

    ...

これでUIが使いやすくなったと思います。

Laravel mix vue No.11 - AWS S3 - 画像ファイルのアップロード

okuda

Webdeveloper

Laravel mix vue No.10 - Vue Font Awesome, Vue Formulate, etc - UIの作り込み

お気軽に
お問い合わせください。

お問い合わせ