今回達成すること
- アプリ全体で使用する基本レイアウトファイル
- ヘッダに表示するツールバーコンポーネント
- 検索キーワードを入力するフォームコンポーネントを作成します。
レイアウト・コンポーネントファイル設計
検索フォームを作成するにあたって、以下のファイルを作成します。
-
「layouts」ディレクトリ
-
app.vue => 固定ページに使用する基本のレイアウトファイル。固定ページとは、動的なルートを生成しない以下のページファイルを指します。
- ホーム ...
index.vue - プライバシーポリシー ...
privacy.vue - 検索結果表示ページ ...
search.vue
- ホーム ...
-
-
「components」ディレクトリ
以下の設計でディレクトリを作成します。
- 「App」ディレクトリ => アプリ全体で使用するコンポーネントを格納するディレクトリ。
AppToolbar.vue => レイアウトファイルで使用するツールバーの部品を集約したコンポーネント。AppToolbarAppBar.vue => ツールバーの部品。
- 「Ui」ディレクトリ => アプリのUI部品コンポーネントを格納するディレクトリ。
UiSearchForm.vue => ツールバーに表示する検索フォーム。
- 「App」ディレクトリ => アプリ全体で使用するコンポーネントを格納するディレクトリ。
検索キーワードのデータ設計
エンドユーザーが入力する検索キーワードをdata()
に保管した場合、そのコンポーネントが破棄されるまで値が初期化されません。
つまりページ遷移を行なっても値が残ってしまいます。
そこでどのページファイルからでも値を初期化できるように、検索キーワードはVuexに保管します。
検索フォームコンポーネントファイルを作成する
% mkdir components/Ui && touch $_/UiSearchForm.vue
検索フォームの実装は後で行うので、とりあえず仮編集でOKです。
components/Ui/UiSearchForm.vue
<template>
<div>
UiSearchForm.vue
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
@Component
export default class UiSearchFormComponent extends Vue {
}
</script>
ツールバーコンポーネントを作成する
「components/App」ディレクトリに
AppToolbar.vue とAppToolbarAppBar.vue
を作成します。
% mkdir components/App && touch $_/{AppToolbar.vue,AppToolbarAppBar.vue}
AppToolbar.vueの構築
<app-toolbar-app-bar />
を呼び出しましょう。
components/App/AppToolbar.vue
<template>
<div>
<app-toolbar-app-bar />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
@Component
export default class AppToolbarComponent extends Vue {
}
</script>
AppToolbarAppBar.vueの構築
<v-app-bar>
を使用してツールバーを構築します。
components/App/AppToolbarAppBar.vue
<template>
<v-app-bar
app
tite
elevate-on-scroll
color="white"
>
<v-toolbar-title>
<nuxt-link
to="/"
class="text-body-2 text-decoration-none text--primary font-weight-bold"
>
{{ appName }}
</nuxt-link>
</v-toolbar-title>
<v-spacer />
<ui-search-form />
</v-app-bar>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
@Component
export default class AppToolbarAppBarComponent extends Vue {
appName: string = this.$config.appName
}
</script>
-
<v-app-bar>
-
app
... ツールバーをヘッダに固定する。 -
tite
... 角を角張ったツールバーに。 -
elevate-on-scroll
... スクロール時にツールバーに影を付ける。
-
-
<v-spacer />
... コンテンツを両端に寄せたい時に使用するVuetifyのコンポーネント。
基本レイアウトファイルを作成する
レイアウトファイルの
特にカスタマイズが必要のないページには、この
% touch layouts/app.vue
<app-toolbar />
を呼び出します。
layouts/app.vue
<template>
<v-app>
<app-toolbar />
<v-main>
<nuxt />
</v-main>
</v-app>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
@Component
export default class AppLayout extends Vue {
}
</script>
レイアウトファイルを呼び出そう
固定ページの
index.vue とprivacy.vue から
nuxt-property-decorator
では、layout()
関数を呼び出すことで、レイアウトファイルを指定することができます。
pages/index.vue
@Component
export default class IndexPage extends Vue {
// 追加
layout (): string {
return 'app'
}
...
}
pages/privacy.vue
@Component
export default class PrivacyPage extends Vue {
// 追加
layout (): string {
return 'app'
}
...
}
いい感じに表示されました。
Vuexに検索キーワードを用意する
state
にsearchQuery
を用意します。
ここにはユーザーが入力した検索キーワードが保存されます。
store/index.ts
export const state = () => ({
...
// 追加
// 検索クエリ
searchQuery: '' as string
})
export const mutations = mutationTree(state, {
...
// 追加
setSearchQuery (state: RootState, payload: string): void {
state.searchQuery = payload
}
})
export const actions = actionTree({ state, getters, mutations }, {
...
// 追加
// 検索クエリのセット
getSearchQuery ({ commit }, query: string) {
commit('setSearchQuery', query)
}
})
OK!これで全ての準備は整いました。
検索フォームを作成する
v-model
を使って、ユーザーがフォームに入力するたびにVuexのstate
に検索キーワードを保存します。
components/Ui/UiSearchForm.vue
<template>
<div>
<!-- test -->
{{ query }}
<v-form
@submit.prevent="onSubmit"
>
<v-text-field
v-model="setQuery"
dense
hide-details
rounded
clearable
placeholder="キーワードを入力"
:solo="isFocused"
:filled="!isFocused"
:style="{ minWidth: '180px' }"
@focus="focus"
@blur="blur"
/>
</v-form>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
@Component
export default class AppSearchFormComponent extends Vue {
isFocused: boolean = false
get query (): string {
return this.$accessor.searchQuery
}
get setQuery (): string {
return this.query
}
set setQuery (newVal: string) {
this.$accessor.getSearchQuery(newVal)
}
// 入力必須 && 空白以外 && 現在のQueryとの変化があった場合にtrueを返す
get isValid (): boolean {
return !!this.query &&
!/^\s+$/.test(this.query) &&
this.$route.query.q !== this.query
}
// フォーカス時
focus (_event: object): void {
this.isFocused = true
}
// フォーカスが離れた時
blur (_event: object): void {
this.isFocused = false
}
// submitイベント
onSubmit () {
if (this.isValid) {
this.$router.push({ path: '/search', query: { q: this.query } })
}
}
}
</script>
<v-form>
@submit.prevent
... エンターキーでメソッドを実行するためのイベント。prevent
修飾子をつけることで、ページリロードが行われず、クライアント上でページ遷移を行うことが可能になる。
<v-text-field>
dense
... フォームを細く。hide-details
... フォーム下に表示される余白(バリデーションメッセージ用)を非表示にする。rounded
... フォームに丸みを付ける。clearable
... フォームに値が入力されたときに「×」のクリアボタンを表示する。:solo="isFocused"
... フォームにフォーカスされた時にsolo
のスタイルを適用。:filled="!isFocused"
... フォームからフォーカスが外れた時にfilled
のスタイルを適用。@focus
... フォーカスされた時に発火するイベント。@blur
... フォーカスが外れた時に発火するイベント。
get query ()
... Vuexのstate
に用意したsearchQuery
の値を呼び出している。
Vuexのstateの値にv-modelを使用したい場合
v-model
に直接Vuexの値を渡すとエラーになります。
一番スマートな書き方は、JavaScriptのゲッターとセッターを使用することです。
- ゲッターには現在の値を
return
で返し、 - セッターの引数で新しい値を取得できるので
- その値をVuexのアクションメソッドに渡します。
get setQuery (): string {
return this.query
}
set setQuery (newVal: string) {
this.$accessor.getSearchQuery(newVal)
}
この書き方を行えば、Vuexのstate
の値をv-model
に渡すことができます。
submitイベント
submitイベントでは、onSubmit()
メソッドを呼び出しています。
バリデーションがtrue
を返す場合に$router.push()
を使用して、
$router.push()
は、クライアントでページ遷移するメソッドで、<nuxt-link>
と同じ挙動を行います。
ページ遷移時にはquery
プロパティにより、検索キーワードを
onSubmit () {
if (this.isValid) {
this.$router.push({ path: '/search', query: { q: this.query } })
}
}
この
検索フォームに入力してみよう
表示された検索フォームに値を入力すると、query
の値がリアルタイムで変化していることがわかります。
これはVuexのstate
の値が、v-model
に連動して変化している証拠です。
確認が取れたらテストコードは削除してください。
components/Ui/UiSearchForm.vue
<template>
<div>
<!-- 削除 -->
<!-- {{ query }} -->
...
</template>
今回の作業は以上です。
% git add -A
% git commit -m "Add search form & app toolbar component"
まとめと次回
今回は基本レイアウトファイル
次回はページ遷移先である