この記事では、ページレイアウトについて解説します。親レイアウトに子ページを組み込むことで、ヘッダーやフッターを共通化できます。
前回の記事ではInertia.jsで表示するVueページを指定してレスポンスを返す方法と、ページのデータの受け渡しや、名前付きルートの使い方について解説しました。今回はその続きですのでぜひ前回の記事と合わせてお読みください。
ページを追加するたびにヘッダーやフッターもコピペしているんですが、変更が大変です。いい方法はありませんか?
レイアウト機能を使うと共通化できますよ
レイアウト
前回ブックマーク一覧というページを作成しましたが、単一のページを表示するだけでした。実際のWebサイトでは通常ヘッダーやフッターなどの共通部分がありますが、それらを毎回個々のページに記述していたのでは、冗長ですし、変更があった場合にすべてのページを書き換えないといけません。
その場合、大元のレイアウトページを作成し、個々のページはその親ページの子コンテンツという形で組み込んで表示させることで、共通しているヘッダーやフッター部分は一回記述するだけでよくなります。
ソースコード
こちらのソースコードはGitHubに掲載しています。
大元のレイアウトの作成
まず、ベースとなるレイアウトをVueコンポーネントとして作成します。「resources/js/Pages/Layout.vue」というファイル名として下記の内容で保存します。
<script setup>
import { Link, Head } from '@inertiajs/vue3'
defineProps({ title: String })
</script>
<template>
<Head :title="title" />
<main>
<header class="bg-gray-100">
<Link href="/" class="text-blue-700 underline m-2">Home</Link>
<Link href="/HelloWorld" class="text-blue-700 underline m-2">HelloWorld</Link>
<Link :href="route('bookmark.index')" class="text-blue-700 underline m-2">ブックマーク一覧</Link>
</header>
<article>
<slot />
</article>
</main>
</template>
HeadコンポーネントはHTMLのheadを書き換えてくれるInertiaのコンポーネントです。TitleタグやMetaタグを各ページから設定可能です。
headerタグで囲まれた領域でヘッダーとして、3つのページリンクを表示させます。Linkによって、XHRリクエストに変換されます。
articleタグの中にメインのコンテンツが配置されます。<slot />というタグがありますが、このタグがこのレイアウトコンポーネントを呼び出されたときに、指定されたコンテンツに置き換わります。
propsでtitleがプロパティとして渡されることを指定しています。
子コンポーネントの作成
次に、大元のレイアウトに組み込む子ページをコンポーネントとして作成します。今回は、前回作成したブックマーク一覧ページを変更して、レイアウトに組み込むことにします。
<script setup>
import { Link } from '@inertiajs/vue3'
import Layout from '@/Pages/Layout.vue'
defineProps({ bookmarks: Array })
</script>
<template>
<Layout title="ブックマーク一覧">
<h2 class="text-lg border-b p-2">
<Link :href="route('bookmark.index')" class="text-blue-700 underline">
ブックマーク一覧
</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>
3行目:Layoutコンポーネントをインポートして使えるようにしています。パスは@をつけることで、「resources/js」配下からのパスになります。「../Layout.vue」のような相対パスでも大丈夫です。
9行目:layoutコンポーネントを呼び出しています。titleパラメータを渡していますので、これがウインドウタイトルになります。(LayoutコンポーネントのHeadコンポーネントのtitleによって)
そして、このlayoutタグの中身が、layoutコンポーネントのslotと置き換えられます。この場合h2タグやulリストがコンテンツとして埋め込まれるイメージです。
レイアウトの確認
「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」を次のように書き換えます。
<script>
import { Link } from '@inertiajs/vue3'
import Layout from '@/Pages/Layout.vue'
export default {
layout: Layout,
}
</script>
<script setup>
defineProps({ bookmarks: Array })
</script>
<template>
<div>
<h2 class="text-lg border-b p-2">
<Link :href="route('bookmark.index')" class="text-blue-700 underline">
ブックマーク一覧
</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>
6行目:使用するレイアウトを指定します。
15行目と26行目:<layout>コンポーネントタグで囲んでいた部分を<div>タグで囲むようにします。
layout: Layout,
これは、次の書き方の省略記法です。
layout: (h, page) => h(Layout, [page]),
これはアロー関数を使った書き方です。従来のfunctionを使った書き方になれている場合は次のようにも書けます。
layout: function(h, page){
return h(Layout, [page])
},
動作確認
デフォルトレイアウト
これまでのように子コンポーネントでレイアウトを指定するのではなく、デフォルトのレイアウトを指定しておいて、指定がない場合はそのデフォルトのレイアウトを使うということも可能です。
その場合、「resources/js/app.js」を次のように書き換えます。
import '../css/app.css';
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import Layout from '@/Pages/Layout.vue'
createInertiaApp({
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
let page = pages[`./Pages/${name}.vue`]
page.default.layout = page.default.layout || Layout
return page
},
setup({ el, App, props, plugin }) {
const app = createApp({ render: () => h(App, props) });
app.config.globalProperties.route = route;
app.use(plugin);
app.mount(el);
},
})
このように10行目でpageにlayoutが指定されていなければ、レイアウトとしてLayoutを使用するようにしています。
「Bookmark/Index.vue」でレイアウトを指定している部分を削除して、それでもレイアウトが効いていることを確認してみてください。
//コメントアウト
//import Layout from '@/Pages/Layout'
//layout: Layout,
デフォルトのレイアウトではないレイアウトを使いたい場合は、明示的に指定します。
//Layout2を使いたい場合
import Layout2 from '@/Pages/Layout2.vue'
layout: Layout2,
ウインドウタイトルを設定
持続的レイアウトにしたらウインドウタイトルが表示されなくなってしまいました。これはレイアウトをコンポーネントとして呼び出していないためです。そこで、子コンポーネントのmountedを使用してタイトルを子コンポーネントから更新するようにします。
export default {
/*
(略)
*/
mounted: function(){
document.title = "ブックマーク一覧";
}
}
Comments