🍔

Nuxt.js(Composition API)で簡単ハンバーガーメニュー実装

2021/04/26に公開

今回の題材

Nuxt.js / Composition API / Tailwind CSS で架空のカフェサイトを題材にレスポンシブな ハンバーガーメニュー を実装してみます。

目次

1. 前提の確認
2. 新規プロジェクトの作成
3. プロジェクトのセットアップ
4. ナビゲーションのマークアップ
5. Tailwind CSSのユーティリティクラスの追加
6. Tailwind CSSのデフォルトのブレイクポイントの設定を変更
7. ハンバーガーメニューのSVG(3本線とバツボタン)の制御処理の実装
8. ハンバーガーメニューの開閉処理の実装


完成コード

https://github.com/Shigeyuki-fukuda/nuxt-nav-sample

完成アプリケーション

https://nuxt-nav-sample.netlify.app/

1. 前提の確認

  • Macであること
  • Node.jsがローカルで動作出来る環境にあること
  • Nuxt.jsは従来の Options API でなく Composition API で実装する

2. 新規プロジェクトの作成

新規プロジェクトを作成します。
UIフレームワークはTailwind CSSで、他の部分についてはお好みで大丈夫です。

$ npx create-nuxt-app nuxt-nav-sample

create-nuxt-app v3.6.0
✨  Generating Nuxt.js project in nuxt-nav-sample
? Project name: nuxt-nav-sample
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: Tailwind CSS
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: GitHub Actions (GitHub only)
? What is your GitHub username? shigeyuki-fukuda
? Version control system: Git
warning ../package.json: No license field
warning nuxt > @nuxt/babel-preset-app > core-js@2.6.12: core-js@<3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your de
pendencies to the actual version of core-js@3.
warning nuxt > @nuxt/webpack > webpack > watchpack > watchpack-chokidar2 > chokidar@2.1.8: Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependenci
es.
warning nuxt > @nuxt/webpack > webpack > watchpack > watchpack-chokidar2 > chokidar > fsevents@1.2.13: fsevents 1 will break on node v14+ and could be using insecure binarie
yarn run v1.22.10
warning ../package.json: No license field
$ eslint --ext ".js,.vue" --ignore-path .gitignore . --fix
✨  Done in 2.51s.

🎉  Successfully created project nuxt-nav-sample

  To get started:

	cd nuxt-nav-sample
	yarn dev

  To build & start for production:

	cd nuxt-nav-sample
	yarn build
	yarn start

  To test:

	cd nuxt-nav-sample
	yarn test


  For TypeScript users.

  See : https://typescript.nuxtjs.org/cookbook/components/

問題ないかサーバーを起動して確認します。

$ yarn run dev
yarn run v1.22.10
warning ../package.json: No license field
$ nuxt

   ╭───────────────────────────────────────╮
   │                                       │
   │   Nuxt @ v2.15.4                      │
   │                                       │
   │   ▸ Environment: development          │
   │   ▸ Rendering:   client-side          │
   │   ▸ Target:      static               │
   │                                       │
   │   Listening: http://localhost:3000/   │
   │                                       │
   ╰───────────────────────────────────────╯

ℹ Preparing project for development                                                                                                                                17:07:52
ℹ Initial build may take a while                                                                                                                                   17:07:52
ℹ Discovered Components: .nuxt/components/readme.md                                                                                                                17:07:52
✔ Builder initialized                                                                                                                                              17:07:52
✔ Nuxt files generated                                                                                                                                             17:07:52

✔ Client
  Compiled successfully in 3.28s

ℹ Waiting for file changes                                                                                                                                         17:07:56
ℹ Memory usage: 170 MB (RSS: 329 MB)                                                                                                                               17:07:56
ℹ Listening on: http://localhost:3000/                                                                                                                             17:07:56
No issues found.

http://localhost:3000/ にアクセスして以下の画面が表示出来ればOKです🙆‍♀️



Vue / Nuxtのバージョンを確認しておきます。
このチュートリアルを進める中で正しく動かない?などあればバージョンの差異かもしれないので、念の為🙏
バージョンは以下の通りです。

$ npm list vue
nuxt-nav@1.0.0 /Users/fuqda/dev/nuxt-nav-sample
├─┬ @nuxtjs/composition-api@0.23.3
│ ├─┬ @nuxt/vue-app@2.15.4
│ │ ├── vue@2.6.12 deduped
│ │ └─┬ vuex@3.6.2
│ │   └── vue@2.6.12 deduped
│ ├─┬ @vue/composition-api@1.0.0-rc.7
│ │ └── vue@2.6.12 deduped
│ └── vue@2.6.12
├─┬ @vue/test-utils@1.1.4
│ └── vue@2.6.12 deduped
├─┬ nuxt@2.15.4
│ └─┬ @nuxt/vue-renderer@2.15.4
│   └── vue@2.6.12 deduped
└─┬ vue-jest@3.0.7
  └── vue@2.6.12 deduped
$ npm ls typescript
nuxt-nav@1.0.0 /Users/fuqda/dev/nuxt-nav-sample
├─┬ @nuxt/typescript-build@2.1.0
│ ├─┬ ts-loader@8.2.0
│ │ └── typescript@4.2.4 deduped
│ └── typescript@4.2.4
├─┬ @nuxtjs/eslint-config-typescript@6.0.0
│ └─┬ @typescript-eslint/eslint-plugin@4.22.0
│   └─┬ tsutils@3.21.0
│     └── typescript@4.2.4 deduped
└─┬ ts-jest@26.5.5
  └── typescript@4.2.4 deduped

今回のプロジェクトディレクトリで使用するNodeのバージョンも念のため指定しておきましょう。

nodenv local バージョン を実行すると実行ディレクトリに .node-version というファイルが作成されます。

$ nodenv local 15.12.0

3. プロジェクトのセットアップ

@nuxtjs/composition-api のインストール

Nuxt.jsでComposition APIを利用するためにプラグインを追加します。

$ yarn add @nuxtjs/composition-api
yarn add v1.22.10
warning ../package.json: No license field
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
warning "@nuxt/types > sass-loader@10.1.1" has unmet peer dependency "webpack@^4.36.0 || ^5.0.0".
warning "@nuxt/typescript-build > ts-loader@8.2.0" has unmet peer dependency "webpack@*".
warning "@nuxtjs/eslint-config-typescript > @typescript-eslint/eslint-plugin > tsutils@3.21.0" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
warning "@nuxtjs/eslint-module > eslint-webpack-plugin@2.5.4" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning "@nuxtjs/tailwindcss > @nuxt/postcss8 > css-loader@5.2.4" has unmet peer dependency "webpack@^4.27.0 || ^5.0.0".
warning "@nuxtjs/tailwindcss > @nuxt/postcss8 > postcss-loader@4.2.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning " > @vue/test-utils@1.1.4" has unmet peer dependency "vue@2.x".
warning " > @vue/test-utils@1.1.4" has unmet peer dependency "vue-template-compiler@^2.x".
warning " > babel-core@7.0.0-bridge.0" has unmet peer dependency "@babel/core@^7.0.0-0".
warning " > babel-jest@26.6.3" has unmet peer dependency "@babel/core@^7.0.0".
warning "babel-jest > babel-preset-jest@26.6.2" has unmet peer dependency "@babel/core@^7.0.0".
warning "babel-jest > babel-preset-jest > babel-preset-current-node-syntax@1.0.1" has unmet peer dependency "@babel/core@^7.0.0".
warning "babel-jest > babel-preset-jest > babel-preset-current-node-syntax > @babel/plugin-syntax-bigint@7.8.3" has unmet peer dependency "@babel/core@^7.0.0-0".
warning "babel-jest > babel-preset-jest > babel-preset-current-node-syntax > @babel/plugin-syntax-import-meta@7.10.4" has unmet peer dependency "@babel/core@^7.0.0-0".
warning " > ts-jest@26.5.5" has unmet peer dependency "typescript@>=3.8 <5.0".
warning " > vue-jest@3.0.7" has unmet peer dependency "vue@^2.x".
warning " > vue-jest@3.0.7" has unmet peer dependency "vue-template-compiler@^2.x".
warning "@nuxtjs/composition-api > @vue/composition-api@1.0.0-rc.7" has unmet peer dependency "vue@>= 2.5 < 3".
warning " > @nuxtjs/composition-api@0.23.3" has unmet peer dependency "@nuxt/vue-app@^2.15".
warning " > @nuxtjs/composition-api@0.23.3" has unmet peer dependency "fs-extra@^8.1.0".
warning " > @nuxtjs/composition-api@0.23.3" has unmet peer dependency "vue@^2".
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
success Saved 5 new dependencies.
info Direct dependencies
└─ @nuxtjs/composition-api@0.23.3
info All dependencies
├─ @nuxtjs/composition-api@0.23.3
├─ @vue/composition-api@1.0.0-rc.7
├─ estree-walker@2.0.2
├─ magic-string@0.25.7
└─ sourcemap-codec@1.4.8
✨  Done in 5.66s.

nuxt.config.js への定義追加

nuxt.config.js にも定義を追加します。

buildModules: [
  // https://go.nuxtjs.dev/typescript
  '@nuxt/typescript-build',
+ '@nuxtjs/composition-api/module',
],

'@nuxtjs/composition-api と書くとエラーになるので注意です😅

https://github.com/nuxt-community/composition-api/issues/465

/pages/index.vue のデフォルトのテンプレートの各要素を削除

一旦、 /pages/index.vue から初期状態の無駄な要素を削除して真っ新な状態にします。

<template>
  <div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({})
</script>

4. ナビゲーションのマークアップ

ナビゲーション用のコンポーネントを追加します。

/components/Nav.vue

<template>
  <div>
    <header>
      <div>
        <div>
          <h1>
            <nuxt-link to="/">Nuxt Cafe</nuxt-link>
          </h1>
          <div>
            <!-- TODO: ハンバーガーメニュー実装 -->
          </div>
        </div>
        <nav>
          <ul>
            <li>
              <nuxt-link to="/shop">店舗情報</nuxt-link>
            </li>
            <li>
              <nuxt-link to="/menu">メニュー</nuxt-link>
            </li>
            <li>
              <nuxt-link to="/information">お知らせ</nuxt-link>
            </li>
          </ul>
        </nav>
      </div>
    </header>
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'

export default defineComponent({
  name: 'Nav',
  setup() {
    return {}
  },
})
</script>

/pages/index.vue

トップページに新規作成したコンポーネントを追加します。

<template>
  <Nav/>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import Nav from '@/components/Nav.vue'

export default defineComponent({
  components: { Nav }
})
</script>

今時点の画面がこんな感じです。



5. Tailwind CSSのユーティリティクラスの追加

テンプレートにTailwind CSSのユーティリティクラスを追加していきます。

/components/Nav.vue

<template>
  <div>
    <header class="w-full absolute md:static bg-black px-2 py-2 z-50">
      <div class="md:max-w-3xl mx-auto md:flex md:items-center">
        <div
          class="w-full md:flex mx-auto px-6 md:px-0 flex justify-between items-center h-16"
        >
          <h1>
            <nuxt-link
              class="text-white text-sm font-bold leading-relaxed inline-block mr-4 py-2 whitespace-no-wrap"
              to="/"
            >
              Nuxt Cafe
            </nuxt-link>
          </h1>
          <div class="text-white md:hidden">
            <button class="focus:outline-none">
              <!-- ハンバーガーメーニューのSVG:https://reffect.co.jp/html/tailwind-for-beginners-navigation-menu -->
              <svg class="h-6 w-6 fill-current" viewBox="0 0 24 24">
                <path
                  d="M24 6h-24v-4h24v4zm0 4h-24v4h24v-4zm0 8h-24v4h24v-4z"
                />
              </svg>
            </button>
          </div>
        </div>
        <nav
          class="w-full md:block absolute left-0 md:static bg-black md:bg-none z-20"
        >
          <ul class="md:flex md:justify-end md:items-end">
            <li class="w-full md:w-auto md:ml-5">
              <nuxt-link
                to="/shop"
                class="text-white sm:hover:bg-gray-600 md:block inline-block md:py-0 py-5 px-5 md:px-0 w-full"
                >店舗情報</nuxt-link
              >
            </li>
            <li class="w-full md:w-auto md:ml-5">
              <nuxt-link
                to="/menu"
                class="text-white sm:hover:bg-gray-600 md:block inline-block md:py-0 py-5 px-5 md:px-0 w-full"
                >メニュー</nuxt-link
              >
            </li>
            <li class="w-full md:w-auto md:ml-5">
              <nuxt-link
                to="/information"
                class="text-white sm:hover:bg-gray-600 md:block inline-block md:py-0 py-5 px-5 md:px-0 w-full"
                >お知らせ</nuxt-link
              >
            </li>
          </ul>
        </nav>
      </div>
    </header>
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'

export default defineComponent({
  name: 'Nav',
  setup() {
    return {}
  },
})
</script>

表示はこんな感じです。
モバイルサイズでの表示の場合にハンバーガーメニューのリンクだけでなく中身のメニューまで見えてしまっていますが、表示制御は後のセクションで行います。




6. Tailwind CSSのデフォルトのブレイクポイントの設定を変更

ハンバーガーメニューを表示する際にPCサイズとモバイルサイズで表示切り替えするためにTailwind CSSのブレイクポイントの設定をいじりたいので、設定ファイルを生成します。

$ npx tailwindcss init

   tailwindcss 2.1.2

   ✅ Created Tailwind config file: tailwind.config.js

tailwind.config.js

767px以下をスマホサイズ、 768px以上~1023px以下を通常のPCサイズ、1024px以上~1279px以下をモニターなどを利用した際の大きな画面サイズとして定義しておきます。

module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    screens: {
      sm: { max: '767px' },
      md: { min: '768px' },
      lg: { min: '1024px' },
      xl: { min: '1280px' },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

7. ハンバーガーメニューのSVG(3本線とバツボタン)の制御処理の実装

/components/Nav.vue

setup 関数内に表示制御用の値とメソッドを定義します。

  <script lang="ts">
+ import { defineComponent, ref } from '@nuxtjs/composition-api'

  export default defineComponent({
    name: 'Nav',
    setup() {
-     return {}
+     const showMenu = ref(false)
+     const toggleStatus = () => {
+       showMenu.value = !showMenu.value
+     }
+     return {
+       showMenu,
+       toggleStatus
+     }
    },
  })
  </script>

次に <template> 側も修正していきます。

  <div class="text-white md:hidden">
+   <button class="focus:outline-none" @click="toggleStatus">
-   <button class="focus:outline-none">
      <!-- ハンバーガーメーニューのSVG:https://reffect.co.jp/html/tailwind-for-beginners-navigation-menu -->
      <svg class="h-6 w-6 fill-current" viewBox="0 0 24 24">
        <path
+	  v-show="!showMenu"
          d="M24 6h-24v-4h24v4zm0 4h-24v4h24v-4zm0 8h-24v4h24v-4z"
        />
+       <path
+         v-show="showMenu"
+         d="M24 20.188l-8.315-8.209 8.2-8.282-3.697-3.697-8.212 8.318-8.31-8.203-3.666 3.666 8.321 8.24-8.206 8.313 3.666 3.666 8.237-8.318 8.285 8.203z"
+       />	
      </svg>
    </button>
  </div>

これでモバイルサイズでハンバーガーメニューをクリックした後にバツボタンに変わり、もう一度クリックすると通常のハンバーガーメニューに戻るようになりました。




8. ハンバーガーメニューの開閉処理の実装

/components/Nav.vue

モバイルサイズの時にデフォルトでメニュー内容が表示されないように sm:hiddennav 要素のclassに追加します。

  <nav
-   class="w-full md:block absolute left-0 md:static bg-black md:bg-none z-20"
+   class="w-full sm:hidden md:block absolute left-0 md:static bg-black md:bg-none z-20" 
  >

最後にモバイルサイズの時はメニュー内容を隠して、ハンバーガーメニュー押下時に内容を出すように三項演算子での分岐を追加します。

         <nav
-          class="w-full sm:hidden md:block absolute left-0 md:static bg-black md:bg-none z-20"
+          :class="(showMenu) ? 'sm:block' : 'sm:hidden'"
+          class="w-full md:block absolute left-0 md:static bg-black md:bg-none z-20"
         >

これで完成です🎉



Discussion