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

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

レイアウト機能を使うと共通化できますよ
レイアウト
前回ブックマーク一覧というページを作成しましたが、単一のページを表示するだけでした。実際のWebサイトでは通常ヘッダーやフッターなどの共通部分がありますが、それらを毎回個々のページに記述していたのでは、冗長ですし、変更があった場合にすべてのページを書き換えないといけません。
それで、大元のレイアウトページを作成し、個々のページはその親ページの子コンテンツという形で組み込んで表示させることで、共通しているヘッダーやフッター部分は一回記述するだけでよくなります。
ソースコード
こちらのソースコードはGitHubに掲載しています。
大元のレイアウトの作成
まず、ベースとなるレイアウトをVueコンポーネントとして作成します。「resources/js/Pages/Layout.vue」というファイル名として下記の内容で保存します。
<template>
<inertia-head :title="title" />
<header class="bg-gray-100">
<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>
import { InertiaLink, InertiaHead } from '@inertiajs/inertia-vue3'
export default {
components: {
InertiaLink,
InertiaHead,
},
props:{
title: String
},
}
</script>
inertia-headコンポーネントはHTMLのheadを書き換えてくれるInertiaのコンポーネントです。TitleタグやMetaタグを各ページから設定可能です。
headerタグで囲まれた領域でヘッダーとして、3つのページリンクを表示させます。Inertia-linkによって、XHRリクエストに変換されます。
articleタグの中にメインのコンテンツが配置されます。<slot />というタグがありますが、このタグがこのレイアウトコンポーネントを呼び出されたときに、指定されたコンテンツに置き換わります。
propsで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 { InertiaLink } from '@inertiajs/inertia-vue3'
import Layout from '@/Pages/Layout'
export default {
components: {
InertiaLink,
Layout,
},
props:{
bookmarks: {
type: Array,
}
},
}
</script>
2行目:layoutコンポーネントを呼び出しています。titleパラメータを渡していますので、これがウインドウタイトルになります。(Layoutコンポーネントのinertia-headコンポーネントのtitleによって)
そして、このlayoutタグの中身が、layoutコンポーネントのslotと置き換えられます。この場合h2タグやulリストがコンテンツとして埋め込まれるイメージです。
18行目:Layoutコンポーネントをインポートして使えるようにしています。パスは@をつけることで、「resources/js」配下からのパスになります。「../Layout」のような相対パスでも大丈夫です。
23行目:componentsで使用するコンポーネントの宣言をしています。
レイアウトの確認
「http://localhost/Bookmark」にアクセスして表示を確認してみましょう。上部にヘッダが表示されればOKです。
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 { InertiaLink } from '@inertiajs/inertia-vue3'
import Layout from '@/Pages/Layout'
export default {
components: {
InertiaLink,
// components:からLayoutを削除
//Layout,
},
//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 { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/inertia-vue3';
import { InertiaProgress } from '@inertiajs/progress';
import Layout from '@/Pages/Layout'
const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
//resolve: (name) => require(`./Pages/${name}.vue`),
// この下のように書き換え
resolve: name => {
const page = require(`./Pages/${name}`).default
page.layout = page.layout || Layout
return page
},
setup({ el, app, props, plugin }) {
return createApp({ render: () => h(app, props) })
.use(plugin)
.mixin({ methods: { route } })
.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,
ウインドウタイトルを設定
持続的レイアウトにしたらウインドウタイトルが表示されなくなってしまいました。これはレイアウトをコンポーネントとして呼び出していないためです。そこで、子コンポーネントのmountedを使用してタイトルを子コンポーネントから更新するようにします。
export default {
/*
(略)
*/
mounted: function(){
document.title = "ブックマーク一覧";
}
}
Comments