Inertia.jsでページレイアウト Inertia入門#4

Laravel

この記事では、ページレイアウトについて解説します。親レイアウトに子ページを組み込むことで、ヘッダーやフッターを共通化できます。

前回の記事ではInertia.jsで表示するVueページを指定してレスポンスを返す方法と、ページのデータの受け渡しや、名前付きルートの使い方について解説しました。今回はその続きですのでぜひ前回の記事と合わせてお読みください。

Laraveler
Laraveler

ページを追加するたびにヘッダーやフッターもコピペしているんですが、変更が大変です。いい方法はありませんか?

シップ
シップ

レイアウト機能を使うと共通化できますよ

レイアウト

前回ブックマーク一覧というページを作成しましたが、単一のページを表示するだけでした。実際のWebサイトでは通常ヘッダーやフッターなどの共通部分がありますが、それらを毎回個々のページに記述していたのでは、冗長ですし、変更があった場合にすべてのページを書き換えないといけません。

それで、大元のレイアウトページを作成し、個々のページはその親ページの子コンテンツという形で組み込んで表示させることで、共通しているヘッダーやフッター部分は一回記述するだけでよくなります。

大元のレイアウトの作成

まず、ベースとなるレイアウトをVueコンポーネントとして作成します。「resources/js/Pages/Layout.vue」というファイル名として下記の内容で保存します。

<template>
    <header class="bg-gray-200">
        <inertia-link href="/" class="text-blue-700 underline m-2">Home</inertia-link>
        <inertia-link href="/HelloWorld" class="text-blue-700 underline m-2">HelloWorld</inertia-link>
        <inertia-link :href="route('bookmark.index')" class="text-blue-700 underline m-2">ブックマーク一覧</inertia-link>
    </header>
    <article>
        <slot />
    </article>
</template>

<script>
  export default {
    props: {
      title: String,
    },
    watch: {
      title: {
        immediate: true,
        handler(title) {
          document.title = title
        },
      },
    },
  }
</script>

headerタグで囲まれた領域でヘッダーとして、3つのページリンクを表示させます。Inertia-linkによって、XHRリクエストに変換されます。

articleタグの中にメインのコンテンツが配置されます。<slot />というタグがありますが、このタグがこのレイアウトコンポーネントを呼び出されたときに、指定されたコンテンツに置き換わります。

propsでtitleがプロパティとして渡されることを指定しています。

watchで、titleが変更されたときに、ウインドウタイトルを更新するようにしています。

子コンポーネントの作成

次に、大元のレイアウトに組み込む子ページをコンポーネントとして作成します。今回は、前回作成したブックマーク一覧ページを変更して、レイアウトに組み込むことにします。

<template>
    <layout title="ブックマーク一覧">
        <h2 class="text-lg border-b p-1">
            <inertia-link :href="route('bookmark.index')" class="text-blue-700 underline">
            ブックマーク一覧
            </inertia-link>
        </h2>
        <ul class="list-disc list-inside p-2">
            <li v-for="bookmark in bookmarks">
                <a :href="bookmark.url" target=_blank class="text-blue-700" >{{ bookmark.title }}</a>
            </li>
        </ul>
    </layout>
</template>

<script>
    import Layout from '@/Pages/Layout'

    export default {
        components: {
            Layout,
        },
        props:{
            bookmarks: {
                type: Array,
            }
        },
    }
</script>

2行目:layoutコンポーネントを呼び出しています。titleパラメータを渡していますので、これがウインドウタイトルになります。(LayoutコンポーネントのWatchによって)
そして、このlayoutタグの中身が、layoutコンポーネントのslotと置き換えられます。子の場合h2タグやulリストがコンテンツとして埋め込まれるイメージです。

17行目:Layoutコンポーネントをインポートして使えるようにしています。パスは@をつけることで、「resources/js」配下からのパスになります。「../Layout」のような相対パスでも大丈夫です。

20行目:componentsで使用するコンポーネントの宣言をしています。

レイアウトの確認

「http://localhost/Bookmark」にアクセスして表示を確認してみましょう。上部にヘッダが表示されればOKです。

表示例

Home HelloWorld ブックマーク一覧

ブックマーク一覧
チャレンジ

HelloWorldコンポーネントも、今回作成したLayoutコンポーネントに組み込んでみましょう

持続的レイアウト

これまでのレイアウトの場合、子レイアウトを読み直した場合(他のページへ移動した場合)に、大元のレイアウトも再作成されますので、何か表示が変更されていても、元に戻ってしまいます。それを避けたい場合、つまり大元のレイアウトは表示を維持したまま、その中の子レイアウトの部分だけを読み込みたいときに持続的レイアウトという機能を使用します。

こんなとき
  • メニューの開閉状態を維持したまま、コンテンツを切り替えたい。
  • 音楽や動画の再生を続けたまま、コンテンツを切り替えたい
  • フォームに入力したものを維持したまま、別のコンテンツを読み込みたい

今回は、ヘッダーに検索ボックスを設置してみます。通常ですと、テキストボックスに何か文字を入力した状態でページを切り替えると、入力したテキストボックスの文字は消えてしまいます。持続的レイアウトを使うと、その部分は再作成されないためページを切り替えてもそのまま残ります。

ヘッダーに検索ボックスを設置

検索機能はまだつけていないため動作はしませんが、検索ボックスだけ表示させてみます。「Layout.vue」の「<header>」内に下記のコードを追加してみましょう。

    <input type=text name=search size=15 class="p-1 m-1 text-sm" />
    <button type=submit class="border border-gray-400 m-1 p-1 text-sm">ブックマーク検索</button>

この段階で一度表示させてみて、ページを切り替えると入力した文字が消えることを確認してみてください。

次に、「Bookmark/Index.vue」を次のように書き換えます。

<template>
    <div>
        <h2 class="text-lg border-b p-1">
            <inertia-link :href="route('bookmark.index')" class="text-blue-700 underline">
            ブックマーク一覧
            </inertia-link>
        </h2>
        <ul class="list-disc list-inside p-2">
            <li v-for="bookmark in bookmarks">
                <a :href="bookmark.url" target=_blank class="text-blue-700" >{{ bookmark.title }}</a>
            </li>
        </ul>
    </div>
</template>

<script>
    import Layout from '@/Pages/Layout'

    export default {
        /*
        ↓この部分を削除
        components: {
            Layout,
        },
        */
        //この行を追加
        layout: Layout,
        props:{
            bookmarks: {
                type: Array,
            }
        },
    }
</script>

2行目と13行目:<layout>コンポーネントタグで囲んでいた部分を<div>タグで囲むようにします。
23行目から25行目:Layoutコンポーネントを定義する部分を削除します。
27行目:使用するレイアウトを指定します。

layout: Layout,

これは、次の書き方の省略記法です。

layout: (h, page) => h(Layout, [page]),

これはアロー関数を使った書き方です。従来のfunctionを使った書き方になれている場合は次のようにも書けます。

layout: function(h, page){
            return h(Layout, [page])
        },

動作確認

デフォルトレイアウト

これまでのように子コンポーネントでレイアウトを指定するのではなく、デフォルトのレイアウトを指定しておいて、指定がない場合はそのデフォルトのレイアウトを使うということも可能です。

その場合、「resources/js/app.js」を次のように書き換えます。

require('./bootstrap');

// Import modules...
import { createApp, h } from 'vue';
import { App as InertiaApp, plugin as InertiaPlugin } from '@inertiajs/inertia-vue3';
import { InertiaProgress } from '@inertiajs/progress';
import Layout from '@/Pages/Layout'

const el = document.getElementById('app');

createApp({
    render: () =>
        h(InertiaApp, {
            initialPage: JSON.parse(el.dataset.page),
            // resolveComponent: (name) => require(`./Pages/${name}`).default,
            // この下のように書き換え
            resolveComponent: name => import(`./Pages/${name}`)
            .then(({ default: page }) => {
                if (page.layout === undefined) {
                    page.layout = Layout
                }
                return page
            }),
            
        }),
})
    .mixin({ methods: { route } })
    .use(InertiaPlugin)
    .mount(el);

InertiaProgress.init({ color: '#4B5563' });

このように17-23行目でpageにlayoutが指定されていなければ、レイアウトとしてLayoutを使用するようにしています。

「Bookmark/Index.vue」でレイアウトを指定している部分を削除して、それでもレイアウトが効いていることを確認してみてください。

//コメントアウト
//import Layout from '@/Pages/Layout'

//layout: Layout,

デフォルトのレイアウトではないレイアウトを使いたい場合は、明示的に指定します。

//Layout2を使いたい場合
import Layout from '@/Pages/Layout2'

layout: Layout2,

ウインドウタイトルを設定

持続的レイアウトにしたらウインドウタイトルがundefinedになってしまったと思います。これはレイアウトをコンポーネントして呼び出していないためです。そこで、子コンポーネントのmountedを使用してタイトルを子コンポーネントから更新するようにします。

    export default {
        /*
        (略)
        */
        mounted: function(){
            document.title = "ブックマーク一覧";
        }
    }
今回のまとめ
  • レイアウト機能を使ってヘッダーを共通化できます。
  • 持続的レイアウトを使うと、子コンテンツ部分だけを更新できます。

次回は、Inertia linkの詳しい使い方を取り上げます。

Inertia.js入門記事一覧
Inertia.jsでシンプルにSPAを構築する Inertia入門#1
Inertia.jsを使うとLaravelやRubyonRailsなどのフレームワーク上でAjax用のAPIやコントローラーを作成しなくても、通常のビューを使う要領でSPA(シングルページアプリケーション)が構築できます。
Laravel+Inertia.jsのインストールとHello World Inertia入門#2
この記事ではLaravelにInertia.jsをインストールしてHelloWorldを出力する方法までを解説します。
Inertia.jsのルーティングとレスポンス作成 Inertia入門#3
Laravel8でjetstreamを入れるとLimewireかInertiaかを選べます。この記事ではInertiaを使ってルーティングやInertiaレスポンスの返し方を解説します。
Inertia.jsでページレイアウト Inertia入門#4
この記事では、ページレイアウトについて解説します。親レイアウトに子ページを組み込むことで、ヘッダーやフッターを共通化できます。
Inertia linkの使い方 Inertia.js入門 #5
この記事では、Inertia linkについて解説します。
InertiaでXHRリクエスト Inertia.js入門 #6
この記事では、Inertia.jsで非同期通信を行うInertia.visit()について解説します。
Inertia.jsでのフォームとリダイレクト Inertia.js入門 #7
この記事では、Inertia.jsでのフォーム送信とリダイレクトについて解説します。
Inertiaでのバリデーション Inertia.js入門 #8
この記事では、Inertia.jsを使った際のバリデーションエラーの表示について解説します。

Comments

タイトルとURLをコピーしました