Laravel 6.x laravel/uiを利用してbootstrap 4を適用する


Laravel2 Advent Calendar 2019 – Qiita の15日目の記事になります。
(ちょうど空いていたのでぎりぎりですが参加させていただきました。)

Laravel 5.8以前ではフロントエンド フレームワークとしてbootstrapとVueがはじめから利用可能なように設定されていましたが、Laravel 6.xではデフォルトでは含まれなくなりました。

6.xでBootstrapを利用するには、フロントエンドのScafolding機能を提供するlaravel/uiのコマンドを使用してJavascriptのビルドとCSSのプリプロセッサ設定を生成する必要があります。

この記事では、Laravel 6.xのプロジェクトにlaravel/uiを利用してBootstrapをプロジェクトに導入する手順についてシンプルなアプリケーションを例に解説しています。
想定しているケースは、主にサーバー側のビューテンプレートで画面を実装する場合に、アプリケーション全体でBootstrap 4の基本的な機能を使ってマークアップと多少画面の動きをつけたい(DatePickerを利用したいなど)といった構成のアプリケーションです。
本格的にフロントエンド開発の環境を整えるには、Laravel Mixについても学ぶ必要がありますが、デフォルトのwebpack.mix.jsの設定でできる範囲にとどめて深堀りしません。

この記事のサンプルソースは Github リポジトリ https://github.com/hrendoh/laravel-ui-bootstrap-tasks に公開しています。記事の中で解説していないソースコードについてはこちらを参照ください。

laravel / uiとは

laravel/uiは、JavascriptのビルドとCSSのプリプロセッサ設定のScaffoldingの生成コマンドを提供するパッケージですが、コマンドオプション--authでフォーム認証用のビューテンプレートも生成することができるようになっています。(6.xでは、artisan make:authコマンドがなくなっているので、認証機能をlaravel/uiのコマンドオプション--authで生成します)
またlaravel/uiのリポジトリの認証のScaffoldingを参考に基本的なCRUDのScaffoldingオプションの追加などの開発もできそうな気もします。

作成するアプリケーションについて

この記事では、Bootstrap 4で開発を進めるにあたって必ず要るであろう以下の3点を設定し、それぞれを利用するシンプルなToDoアプリを作成してみます。

  • Bootstrap 4のスタイルの適用
  • Bootstrap 4で除外されたDatePickerの組み込み、Tempus Dominusを利用
  • アイコンの導入、Bootstarp 4が推奨しているライブラリのうちFontAwesomを導入

作成するのは、RailsなどのScaffoldが生成するようなCRUDアプリです。

index

create

前提条件

JavascriptとCSSのビルドはLaravel Mix つまり Webpackを利用しますので、node.jsのインストールが必用です。

プロジェクトの作成

適当に「laravel-ui-bootstrap-tasks」という名前のプロジェクトを作ってみます。

$ composer create-project --prefer-dist laravel/laravel laravel-ui-bootstrap-tasks

Bootstrap 4のインストール

まずは laravel/ui をインストールします。

$ composer require laravel/ui --dev

uiコマンドでbootstrap用のJavascriptビルドおよびCSSプリプロセッサsassの設定を含むScaffoldingを追加します。

$ php artisan ui bootstrap
Bootstrap scaffolding installed successfully.
Please run "npm install && npm run dev" to compile your fresh scaffolding.

コマンドの実行で追加された設定を確認してみます。

package.jsonには、以下のパッケージが追加されています。

    "devDependencies": {
        ...
        "bootstrap": "^4.0.0",
        ...
        "jquery": "^3.2",
        "popper.js": "^1.12",
        ...
    }

jsは、bootstrap.jspopper.jsの初期化処理とbootstrapのインポートが追加されました。

// resources/js/bootstrap.js
...
/**
 * We'll load jQuery and the Bootstrap jQuery plugin which provides support
 * for JavaScript based Bootstrap features such as modals and tabs. This
 * code may be modified to fit the specific needs of your application.
 */

try {
    window.Popper = require('popper.js').default;
    window.$ = window.jQuery = require('jquery');

    require('bootstrap');
} catch (e) {}
...

sass側は_variables.scssが追加され、app.scssにGoogleのWebフォント Nunito、追加されたvariablesbootstrapの読み込みが追加されています。

// resources/sass/app.scss
// Fonts
@import url('https://fonts.googleapis.com/css?family=Nunito');

// Variables
@import 'variables';

// Bootstrap
@import '~bootstrap/scss/bootstrap';

追加されたnpm パッケージをインストールしてビルドします。

$ npm install
$ npm run dev
 DONE  Compiled successfully in 6036ms                                                                   6:50:18 PM

       Asset      Size   Chunks             Chunk Names
/css/app.css   196 KiB  /js/app  [emitted]  /js/app
  /js/app.js  1.06 MiB  /js/app  [emitted]  /js/app

これでBootstrap 4を利用できる環境が整いました。
FontAwesomeとDatePickerは後ほど追加していきます。

テーブル(モデル)準備

早速以下のテーブルを用意していきます。

カラム名 マイグレーションの型 MySQLのデータ型
id bigIncrements bigint(20) unsigned
subject string varchar(255)
description text text
due_date date date
completed boolean tinyint(1)

モデル「Task」をマイグレーション オプションを付けて生成します。

$ php artisan make:model Task -m
Model created successfully.
Created Migration: 2019_12_11_151510_create_tasks_table

テーブル定義に従ってマイグレーションを作成します。

<?php
// database/migrations/2019_12_11_151510_create_tasks_table.php
...
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('subject');
            $table->text('description')->nullable();
            $table->date('due_date')->nullable();
            $table->boolean('completed')->nullable();
            $table->timestamps();
        });
    }
...

マイグレーションを適用します。

$ php artisan migrate
Migration table created successfully.
...
Migrating: 2019_12_11_151510_create_tasks_table
Migrated:  2019_12_11_151510_create_tasks_table (0.07 seconds)

ロジックを実装

リソースコントローラを生成して、対応するViewを追加します。(公式ドキュメント: Resource Controllers)

$ php artisan make:controller TaskController --resource --model=Task
Controller created successfully.

ルートはtasksにリダイレクトし、Taskのリソース ルーティングを追加します。

<?php
// routes/web.php
Route::get('/', function () {
    return redirect('tasks');
});

Route::resource('tasks', 'TaskController');

TaskControllerのリソース アクションに実装を追加します。

<?php

namespace App\Http\Controllers;

use App\Task;
use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function index()
    {
        $tasks = Task::paginate();
        return view('tasks.index', compact('tasks'));
    }

    public function create()
    {
        return view('tasks.create');
    }

    public function store(Request $request)
    {
        $inputs = $request->all();
        Task::create($inputs);

        return redirect()->route('tasks.index')->with('message', 'Task created successfully.');
    }

    public function show(Task $task)
    {
        return view('tasks.show', compact('task'));
    }

    public function edit(Task $task)
    {
        return view('tasks.edit', compact('task'));
    }

    public function update(Request $request, Task $task)
    {
        $inputs = $request->all();
        if (!isset($inputs['completed'])) $inputs['completed'] = false;
        $task->update($inputs);

        return redirect()->route('tasks.index')->with('message', 'Task updated successfully.');
    }

    public function destroy(Task $task)
    {
        $task->delete();

        return redirect()->route('tasks.index')->with('message', 'Task deleted successfully.');
    }
}

resources/viewsにレイアウト用のテンプレートを追加

// resources/views/layout.blade.php

resources/viewstasksディレクトリを追加して、ビュー’index’、’created’、’show’、’edit’を追加します。

$ mkdir resources/views/tasks
// resources/views/tasks/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">

  <!-- CSRF Token -->
  <meta name="csrf-token" content="{{ csrf_token() }}">

  <title>{{ config('app.name', 'Laravel') }}</title>

  <!-- Scripts -->
  <script src="{{ asset('js/app.js') }}" defer></script>

  <!-- Fonts -->
  <link rel="dns-prefetch" href="//fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">

  <!-- Styles -->
  <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>

<body>

  <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
    <a class="navbar-brand" href="#">{{ config('app.name', 'Laravel') }}</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
      aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav">
        <li class="nav-item"><a class="nav-link" href="{{route('tasks.index')}}">Tasks</a></li>
      </ul>
    </div>
  </nav>

  <div class="container">
    @if(session('message'))
    <div class="alert alert-success alert-dismissible" role="alert">
      <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">×</span>
      </button>
      {{@session('message')}}
    </div>
    @endif

    @yield('header')
    @yield('content')
  </div>

  @yield('scripts')
</body>

</html>

create.blade.php, show.blade.php, edit.blade.phpのテンプレートについてはGithubリポジトリのソースを確認して追加してください。

ここまでで/tasks/indexを表示してみると以下の用になります。

テンプレートにはアイコンのタグが含まれていますが、まだ表示されていません。

Font Awesomeの追加

次にFont Awesomeを導入してアイコンも表示されるようにしていきます。
Font Awesomeを選択している理由としては、Bootstrapのドキュメント Icons · Bootstrap で推奨しているライブラリの一番上に載っていたからというだけです。

npmで、@fortawesome/fontawesome-freefont-awesome-scssを追加します。

$ npm install @fortawesome/fontawesome-free --save-dev 
$ npm install font-awesome-scss --save-dev

resources/sass/app.scssにFontAwesomeのインポートを追加します。

// resources/sass/app.scss

// Font Awesome
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
@import '~@fortawesome/fontawesome-free/scss/solid';
@import '~@fortawesome/fontawesome-free/scss/regular';

この記事で作るアプリでは、「free」の「solid」と「regular」に含まれるアイコンのみを使用しています。
その他のアイコン(「blands」など)を使う場合は、必要に応じてインポートは追加してください。

ビルドし直します。

$ npm run dev

再度、/tasks/indexを表示してみると以下のようにアイコンも表示されるようになります。

参考: Laravel+FontAwesomeの使い方

DatePickerの追加

Bootstrap 4ではDatePickerが含まれなくなったので、Tempus Dominus bootstrap 4を利用します。
また、Tempus Dominusを動かすにはMoment.jsが必要なのでMoment.jsもインストールします。

$ npm install moment --save-dev
$ npm install tempusdominus-bootstrap-4 --save-dev

resources/sass/app.scssにTempus Dominusのscssのインポートを追加します。

// resources/sass/app.scss

// Tempus Dominus
@import '~tempusdominus-bootstrap-4/src/sass/tempusdominus-bootstrap-4-build';

resources/js/bootstrap.jsにmoment.jsとtempusdominus-bootstrap-4の読み込みを追加します。
moment.jsはグローバル変数にセットする必要があります。

// resources/js/bootstrap.js
/**
 * Import moment js
 */
import moment from 'moment';
window.moment = moment;

require('tempusdominus-bootstrap-4');
$('.datetimepicker').datetimepicker({
  icons: {
    // Font Awesome 5には「fa-clock-o」がなくなっているので指定する
    time: 'far fa-clock'
  },
  format: 'YYYY-MM-DD'
});

resources/views/tasks/create.blade.phpのDue Dateのマークアップは以下のように記述しています。

      <div class="form-group">
        <label for="due_date-field">Due Date</label>
        <div class="input-group date datetimepicker" id="due_date" data-target-input="nearest">
          <input type="text" name="due_date"  id="due_date-field" class="form-control datetimepicker-input" data-target="#due_date" />
          <div class="input-group-append" data-target="#due_date" data-toggle="datetimepicker">
            <div class="input-group-text"><i class="fa fa-calendar"></i></div>
          </div>
        </div>
      </div>

ポイントは、datepickerクラスを指定しているdiv要素のidの値「due_date」を、input-group-appendクラスのdiv要素のdata-target属性の値にセットするあたりです。

,