20201113のvue.jsに関する記事は10件です。

[Vue.js] ルーティング による画面遷移

はじめに

Vue.js には URL の移動を伴う画面遷移の手段として ルーティング という仕組みが用意されている。
本記事では簡単な例を元に以下の 2点について見ていく。

前提

  • Vue CLI でプロジェクトを作成していること
  • 単一ファイルコンポーネントであること( 1 )
  • 動作確認は $ npm run serve で起動した環境で行なっている

環境

Version 備考
Vue 2.6.11 公式はこちら
Vue CLI 4.1.1 公式はこちら
Vue Router 3.0.7 Vue.js の公式ルータ, 公式はこちら
Buefy 0.9.2 Vue.js 用の UI コンポーネント, MIT ライセンス, 公式はこちら

事前準備

vue-router の追加

package.json に vue-router を追加して npm install でモジュールを追加する。
今回の記事で扱ったバージョンは 3.0.7

package.json
// 省略
{
  "dependencies": {
    "vue-router": "3.0.7",  // コレを追加
  }
}
// 省略
vue-routerを追加
$ npm install
added 1 package from 1 contributor and audited 1299 packages in 6.541s

基本

この項目で扱う内容は次の 3点。

  • ルーティングで扱いたいページの追加
  • ルーティングを管理するファイルの追加
  • Vue.js 上でルーティングを扱うための 2つ のファイル修正

で、表にまとめると以下のようになる。

ファイル 新規 / 修正 備考
*.vue 新規 ルーティングで扱いたいページ
router.js 新規 ルーティングを管理する
main.js 修正 ルーティングを扱うために修正
App.vue 修正 同上

では上から順に見ていく。

ページ( *.vue )の追加

ルーティングで扱いたいページを実装する。
とは言っても、ここで扱う内容に特別なものはない。単純に 単一コンポーネントでページを実装しただけである。

追加したページ
<template>
  <div :class="$style.parent">
    <div :class="$style.child">
      <h1>Routing Test Page</h1>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Routing'
}
</script>

// スタイルは割愛

router.js ファイルを新規作成

こちらはルーティングを管理するための JavaScript ファイル。

router.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import Routing from './views/Routing.vue'
//
// 他のコンポーネントは省略
//

Vue.use(Router)

export default new Router({
  // デフォルトの挙動では URL に `#` が含まれる.
  // URL から hash を取り除くには `mode:history` を指定する
  mode: 'history',                  
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    //
    // 省略
    //
    {
      path: '/routing',
      name: 'routing',
      component: Routing 
    }
  ]
})

短いコードではあるがポイントがいくつかある。以下、順に見ていく。

  1. vue-routerimport

    • 事前準備 で導入した vue-routerimport している
    • ここは特筆すべきことはなく、単純に vue-router を使うための宣言である
  2. vue-router の登録

    • import した vue-router を Vue で使えるように Vue.use() で指定する
    • これにより Vue アプリ上で vue-router を使ったルーティングが可能になる
  3. vue-router のインスタンス生成と export

    • vue-router のインスタンスを生成し、それを export している
    • これにより 各コンポーネントでは 本 JS ファイル(router.js ) を import することなく this.$router で参照 することができる
  4. ルーティングの設定

    • routes プロパティを配列で定義し、その中に以下を設定することで path に応じたコンポーネントが呼び出される
    • ここの内容でルーティングのためのルールを設定することになる。 公式の Vue Router を充分に理解しておきたい
項目 説明 備考
path URL の path <router-link> で指定する文字列を間違えないように注意すること
name <router-link>router.push で指定する名前 両者を使わないのであれば設定不要。だが設定しておいた方が良いと思う
component 呼び出されるコンポーネント <router-view /> が配置された箇所でレンダリングされる

main.js の修正

このファイルの修正は単純で、 アプリケーション上でルーティングを行うために、前項で作成した

  • router.jsimport
  • Vue インスタンス生成時に router.js のインスタンスのセット

の 2点 を行っている。

main.js
import Vue from 'vue'
import App from './App.vue'

// ルーティングのために追加
import router from './router'

Vue.config.productionTip = false

new Vue({
  router, // ルーティングのために追加
  render: h => h(App),
}).$mount('#app')

App.vue の修正

こちらは前項で挙げた main.js

main.js
new Vue({
  router, // ルーティングのために追加
  render: h => h(App),
}).$mount('#app')

の部分で読み込まれていて、アプリケーションの起点となるファイルである。
この App.vue では、テンプレート部分で router.js で定義したルーティングルールとの紐付けを行っている。

App.vue
<template>
  <div id="app">
    <div 
      id="nav" 
      class="tab-area-base">
      <ul class="tab-menu-base">
     <!-- `router.js` で定義したルーティングルールとの紐付けを行っている -->
        <li><router-link to="/">Home</router-link></li>
        <!-- *** -->
        <!-- 省略 -->
        <!-- *** -->
        <li><router-link to="/routing">Rounting Test</router-link></li>
      </ul>
    </div>
    <router-view />
  </div>
</template>

// ▼ ここから削除 ( ルーティングによって不要となるため )
//<script>
//import HelloWorld from './components/HelloWorld.vue'
//
//export default {
//  name: 'App',
//  components: {
//    HelloWorld
//  }
//}
//</script>
// ▲ ここまで削除

// スタイルは割愛

このファイルもいくつかポイントがあるので順に見ていく。

  1. <template> 部分
    • <router-link>
      • 遷移先の設定を行っている
      • ここで to= で設定しているのが router.js で設定 したパスとなる
      • 指定する文字列を間違うと リンクをクリックしても遷移しない うえに、コンソール上にエラーも出ない ため、意外と不具合の原因を探しづらいので注意されたい
    • <router-view />
      • ここの部分に <router-link> から router.js を経由して呼び出されたコンポーネントの内容が描画される
      • これが無いと コンポーネントを呼び出しているの描画されない ということになるので注意
  2. <script> 部分
    • コード中のコメントにもあるとおり、ルーティングを使うことで本ファイルでのコンポーネントの呼び出しは不要になった

動作確認

ここまでルーティングの基本について見てきた。
では実際にどう動くのかをキャプチャをもとに見ていく。

ルーティング前

App.vue の修正前 のコードにあるように、HelloWorld.vue がそのまま表示されている。

スクリーンショット 2020-09-13 1.13.51.png

ルーティング後

  1. 初期表示 or Home タブを選択
    ルーティングの実装後。
    画面にはタブが表示され、 Home タブの URL である http://localhost:8080 には初期画面として ルーティング前 と同じ HelloWorld.vue の画面が表示されている。
    スクリーンショット 2020-09-21 20.52.17.png

  2. Routing Test タブを選択
    Routing Test タブの URL である http://localhost:8080/routing では、新たに実装したページである Routing.vue の画面が表示されている。
    706e67.png

以上、ルーティングの基礎について見てきたが、決められたルールに沿うことで、少ない手間で URL を指定したページ遷移が実現できることがわかった。
次の項目では ルーティング時にパラメータの受け渡しを行う 方法について見ていく。

進んだ使い方

vue-router を使ったルーティングでは 任意の値をパラメータとして受け渡す ために

  • URL のパターンマッチングによるページ遷移
  • router.push を使ったページ遷移

といった方法が用意されている。

以下、それぞれの方法について見ていく。

パターンマッチングによるページ遷移

URL のパターンマッチングによってパラメータの受け渡しを行うケース。ここで挙げる例は以下のとおり。

  • ユーザ情報の詳細を持つコンポーネント UserDetail.vue がある
  • そのコンポーネントに対してユーザIDを URL で指定する
  • 指定したユーザ情報の詳細を表示するページに遷移する

基本 の項目でルーティングのための準備はできているので、本項では router.js への追記とユーザ情報の詳細を持つコンポーネント UserDetail.vue について扱う。

router.js への追記

パターンマッチングでのページ遷移の実現にあたり、まずは router.jsURL でのパラメータの受け渡し のための設定を記述する。

具体的には以下のとおり。

router.js
import Router from 'vue-router'
import UserDetail from './views/UserDetail.vue'

Vue.use(Router)

export default new Router({
    {
      path: '/users/:id',
      name: 'user-detail',
      component: UserDetail 
    }
  ]
})

ポイントは path: '/users/:id' の部分。 path 内の URL に : を使用する ことでパターンマッチングを実現する。
コンポーネントでは $route.params から、ここで指定したパラメータ名と 同じ名前でアクセス することで値を取得できる。

UserDetail.vue

UserDetail.vuerouter.js で URL によるパターンマッチングでの遷移を行う際に指定されたコンポーネント。

前述の説明のとおり、下記のコードでは $route.params.id とすることで '/users/:id' で指定した id の値を取得 している。

UserDetail.vue
<template>
  <div :class="$style.component">
    <h1>This page is user detail.</h1>
    <div
      :class="$style.userinfo">
      <table>
        <th :class="$style.item">
          ITEM
        </th>
        <th :class="$style.value">
          VALUE
        </th>
        <!-- users のリストにアクセスする際、インデックスは 0 からなので受け取った id の値から `-1` する -->
        <tr
          v-for="(value, name) in users[$route.params.id - 1]"
          :key="name">
          <td :class="$style.item">
            {{ name }}
          </td>
          <td :class="$style.value">
            {{ value }}
          </td>
        </tr>
      </table>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserDetail',
  data: function () {
    // 返却するオブジェクト users は本コンポーネントで表示するユーザ情報
    // 本来ならば DB 等で保持するのだが、今回は記事用のサンプルコードということでリストで持たせている
    return {
      users: [
        {
          id: 1,
          name: 'hogehoge',
          live: 'Japan Tokyo',
          phone: 'NNN-XXXX-HHHH',
          gender: 'male',
          mail: 'hogehoge@mail.com'
        },
        {
          id: 2,
          name: 'barbar',
          live: 'Japan Kanagawa',
          phone: 'NNN-XXXX-BBBB',
          gender: 'male',
          mail: 'barbar@mail.com'
        },
        {
          id: 3,
          name: 'piypiyo',
          live: 'Japan Kanagawa',
          phone: 'NNN-XXXX-PPPP',
          gender: 'female',
          mail: 'piypiyo@mail.com'
        },
        {
          id: 4,
          name: 'fugafuga',
          live: 'Japan Chiba',
          phone: 'NNN-XXXX-FFFF',
          gender: 'male',
          mail: 'fugafuga@mail.com'
        },
        {
          id: 5,
          name: 'varvar',
          live: 'Japan Saitama',
          phone: 'NNN-XXXX-VVVV',
          gender: 'female',
          mail: 'varvar@mail.com'
        }
      ],
    }
  }
}
</script>

// スタイルは割愛

動作確認

動作確認の結果が以下のキャプチャ。

URL に

  • http://localhost:8080/users/1 を指定することで id: 1 のユーザ情報詳細
  • http://localhost:8080/users/4 を指定することで id: 4 のユーザ情報詳細

が、ぞれぞれ表示されることが確認できた。

スクリーンショット 2020-11-07 12.10.50.png

スクリーンショット 2020-11-07 12.11.14.png

ボタンアクションによるページ遷移

前掲の パターンマッチングURL上でパラメータがセットされたケース の例について触れた。
ここではボタンアクションによる パラメータの受け渡しを伴うページ遷移 について扱う。

ここで挙げる例は以下のとおり。

  • ユーザのリストを表示するコンポーネント UserList.vue と、その親コンポーネント Users.vue がある
  • コンポーネント UserList.vue でテーブルから任意のレコードを選択して Show more seleted... ボタンをクリックすることで
  • 指定したユーザ情報の詳細を表示するコンポーネント UserDetail.vue を呼び出す( 遷移する )

コードを見る前に

上記 3 つのコンポーネントの概要を表にすると以下のとおり。

コンポーネント 概要 子コンポーネント 備考
Users.vue User 情報を持つ UserList.vue
UserLive.vue User 情報をテーブルで表示する なし
UserDetails 指定された User 情報の詳細を表示する なし 前項 で扱っているのでここではコードを扱わない

このうち Users.vueUserDetail.vue は同じデータオブジェクトをそれぞれのコンポーネント内で定義している。
本来ならばこれらのデータは DB なり localStorage で持つなり、なんらかの手段でデータの共有化を図るべきなのだけれども、今回は ルーティングが主題 であるためデータの持ち方については考慮外とした。

Users.vue

Users.vue はユーザの一覧をオブジェクトとして持ち、画面描画時に子コンポーネントである UserList.vue にユーザ情報を渡すだけの単純なもの。

  • コンポーネントの親子関係
  • 親コンポーネント → 子コンポーネントへのデータ授受

についても少し見てみたいと思い試してみた。
( コンポーネント間のデータのやりとりについては、別途記事を設けて見ていきたい )

Users.vue
<template>
  <div :class="$style.component">
    <UserList :properties="properties" />
  </div>
</template>

<script>
// @ を指定することで `/src` の代替となる
import UserList from '@/components/UserList.vue'

export default {
  name: 'Users',

  components: {
    UserList
  },
  data: function () {
    // ここで返却するデータは子コンポーネント `UserList.vue` で表示するユーザ情報
    // 本来ならば DB 等で保持するのだが、今回は記事用のサンプルコードということでリストで持たせている
    return {
      properties: {
        users: [
          {
            id: 1,
            name: 'hogehoge',
            live: 'Japan Tokyo',
            phone: 'NNN-XXXX-HHHH',
            gender: 'male',
            mail: 'hogehoge@mail.com'
          },
          {
            id: 2,
            name: 'barbar',
            live: 'Japan Kanagawa',
            phone: 'NNN-XXXX-BBBB',
            gender: 'male',
            mail: 'barbar@mail.com'
          },
          {
            id: 3,
            name: 'piypiyo',
            live: 'Japan Kanagawa',
            phone: 'NNN-XXXX-PPPP',
            gender: 'female',
            mail: 'piypiyo@mail.com'
          },
          {
            id: 4,
            name: 'fugafuga',
            live: 'Japan Chiba',
            phone: 'NNN-XXXX-FFFF',
            gender: 'male',
            mail: 'fugafuga@mail.com'
          },
          {
            id: 5,
            name: 'varvar',
            live: 'Japan Saitama',
            phone: 'NNN-XXXX-VVVV',
            gender: 'female',
            mail: 'varvar@mail.com'
          }
        ]
      }
    }
  }
}
</script>

// スタイルは割愛

UserList.vue

UserList.vue は親コンポーネントである Users.vue から受け取ったデータをテーブルで表示するコンポーネント。

テーブルの実現には Buefy の Table を使用している。

UserList.vue
<template>
  <div>
    <h1>This page is user list.</h1>
    <div :class="$style.userlist">
      <!-- Buefy のテーブルを使って実現 -->
      <!-- https://buefy.org/documentation/table/ -->
      <b-table
        :data="properties.users"
        :columns="columns"
        :striped="true"
        :hoverable="true"
        :selected.sync="selected" />
    </div>
    <div :class="$style.showmore">
      <b-button
        type="is-info"
        @click="showMoreInformation">
        Show more selected...
      </b-button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserList',
  props: {
    properties: {
      type: Object,
      'default': () => { return null },
    }
  },
  data: function() {
    return { 
      // `selected`, `columns` は Buefy のテーブルを使用する際に必要なパラメータ
      // https://buefy.org/documentation/table/
      selected: null,
      columns: [
        {
          field: 'id',
          label: 'ID',
          width: '50',
          numeric: true
        },
        {
          field: 'name',
          label: 'NAME',
          width: '400',
          centered: true
        },
        {
          field: 'mail',
          label: 'MAIL',
          width: '400',
          centered: true
        },
      ]
    }
  },
  methods: {
    showMoreInformation: function() {
      // アロー関数で定義すると `this` で `selected` が参照できない。
      // 詳細は https://qiita.com/_Keitaro_/items/d48733a19c10889e2365 を参照のこと。
      if (!this.selected) {
        alert('No data selected...')
        return false
      }
      const selected = this.selected
      this.$router.push({
        name: 'user-detail',
        params: { id: selected['id'] }
      })
    }
  }
}
</script>

// スタイルは割愛

本コンポーネントのポイントはボタンクリック時に実行されるメソッドである showMoreInformation
レコードを選択した状態で showMoreInformation が実行されると UserDetail.vue コンポーネントが表示され、選択されたユーザ情報の詳細が表示される。

その動きを実現しているのが下記の部分。

      const selected = this.selected
      this.$router.push({
        name: 'user-detail',
        params: { id: selected['id'] }
      })

this.$router.push の引数に nameparams プロパティをセットすることでルーティングを実現させている。

ここで nameparams.idパターンマッチング の router.js で設定した

    {
      path: '/users/:id',
      name: 'user-detail',
      component: UserDetail 
    }

の部分とリンクしている。
それぞれの相関を表にすると次のとおり。

UserList.vue のコード router.js のコード
name name: 'user-detail' name: 'user-detail'
id params: { id: selected['id'] } path: '/users/:id'

こうした name プロパティを利用したルーティングを 名前付きルート というらしい。

動作確認

動作確認の結果が以下のキャプチャ。

  • User List タブで表示されたテーブルから id: 3 のレコードを選択して Show more selected... ボタンをクリック
  • UserDetail タブに遷移して id: 3 のユーザ情報詳細が表示される

ことが確認できた。

スクリーンショット 2020-11-07 11.46.55.png

スクリーンショット 2020-11-07 11.47.11.png

まとめ

基本

  • ルーティングには vue-router を使うと簡単に実現できる
  • ルーティングを管理するために router.js を実装して main.jsimport することで利用する
  • App.vue では以下を行ってうことで画面遷移を実現する
    • router-link で遷移先の指定
    • router-view で遷移先のページの表示

進んだ使い方

  • ルーティング時に任意の値をパラメータとして渡すことができる
  • そのための手段として以下を用いる
    • URL パラメータとして指定する
    • router.push 時にパラメータとして指定する
  • router.push 時に遷移先として name を指定することを 名前付きルート という

ソースコード

今回の記事で動作確認に使用したコードは下記にアップしております。
ご参考まで。( 以下は ブランチのリンクですが、 master にもマージ済みです )

参考

公式


  1. Vue.js のコンポーネントを単独のファイルとして作成する機能
    拡張子「.vue」のファイルのことで<template>, <script>, <style> のブロックで構成されている。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js+Firebaseで2回目の新しいプロジェクトを作る

Nuxt.js+Firebaseを勉強しています。

プロジェクトを作成し、Webサービスをデプロイすることができました。本記事は、もう一つプロジェクトを作成してSNS認証を実装するところまでのメモです。

環境

  • Firebase 8.0.1
  • Vue 2.6.11
  • Nuxt 2.10.2
  • 開発環境 WindowsPC

実施手順

Firebaseコンソール で新規プロジェクトを作成

  • 「このプロジェクトで Google アナリティクスを有効にする」を有効
    • 「Google アナリティクスの構成」はデフォルトを選択

作成が完了し、プロジェクトのコンソールを見れるようになった。

Firebaseコンソールでウェブアプリを作成

プロジェクトのコンソールで開始するアプリ(ウェブ)を追加する。

  • 「アプリのニックネーム」は自由に決める。
  • Firebase Hostingの設定は後からでもできるとのことなので、このタイミングではしない。

「アプリを登録」ボタンを押下する。

Nuxtでプロジェクトを作成する

Nuxtでプロジェクトを作成するコマンド
npx create-nuxt-app (プロジェクト名)

コマンドを実行したディレクトリに指定したプロジェクト名のディレクトリが作られる。

Vuetifyで簡単に作りたかったので、設定はこんな感じ。

- ? Project name: (プロジェクト名)
    - デフォルトでいいので未入力で Enter
- ? Programming language: (Use arrow keys)
    - > JavaScript
    -   TypeScript
- ? Package manager: (Use arrow keys)
    -   Yarn
    - > Npm
- ? UI framework: (Use arrow keys)
    -   None
    -   Ant Design Vue
    -   Bootstrap Vue
    -   Buefy
    -   Bulma
    -   Chakra UI
    -   Element
    -   Framevuerk
    -   iView
    -   Tachyons
    -   Tailwind CSS
    -   Vuesax
    - > Vuetify.js
- ? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
    - >( ) Axios
    -  ( ) Progressive Web App (PWA)
    -  ( ) Content
    - ※ 選択無しでEnter
- ? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
    - >( ) ESLint
    -  ( ) Prettier
    -  ( ) Lint staged files
    -  ( ) StyleLint
    -  ( ) Commitlint
    - ※ 選択無しでEnter
- ? Testing framework: (Use arrow keys)
    - > None
    -   Jest
    -   AVA
    -   WebdriverIO
- ? Rendering mode: (Use arrow keys)
    -   Universal (SSR / SSG)
    - > Single Page App
- ? Deployment target: (Use arrow keys)
    - > Server (Node.js hosting)
    -   Static (Static/JAMStack hosting)
- ? Development tools: (Press <space> to select, <a> to toggle all, <i> to invertselection)
    - >( ) jsconfig.json (Recommended for VS Code if you're not using typescript)
    -  ( ) Semantic Pull Requests
    -  ( ) Dependabot (For auto-updating dependencies, GitHub only)
    - ※ 選択無しでEnter
- ? What is your GitHub username? 
    - GitHubのユーザー名を入力する。
- ? Version control system: (Use arrow keys)
    -   Git
    - > None

ビルド実行

>npm run build

(中略)

? Are you interested in participation? (Y/n)

n にした。

Firebase SDKのインストールとプロジェクトへの紐付け

作成したプロジェクトにFirebase SDKをインストールする。
今回はnpmで--saveでインストールする。(-gではなく)

cd (プロジェクト名のフォルダ)
npm install firebase --save

インストールしたfirebaseのバージョンを確認する。

>npm list firebase
`-- firebase@8.0.1

テスト環境で動かしてみよう

実行コマンドは次の通り。

npm run dev

http://localhost:3000/ にアクセスすると、Vuetifyの画面が出る。

プロジェクトをFirebase初期設定する

1.Firebaseコンソールでリージョンを設定

Firebaseコンソール にログインし、当該プロジェクトのSettingsを開く。
[全般] 中程の リソース ロケーション を任意のロケーションに指定する。
今回は東京リージョン asia-northeast1 を選択する。

2.Cloud Firestore の作成

Cloud Firestoreのページで、「データベースの作成」ボタンを押下する。

3. firebase init を実行

NuxtでFirebaseを使いたいので、create-nuxt-appで作成したNuxtプロジェクトディレクトリのトップでコマンド firebase init を実行する。

 to select features, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection)
>( ) Database: Deploy Firebase Realtime Database Rules
 (*) Firestore: Deploy rules and create indexes for Firestore
 ( ) Functions: Configure and deploy Cloud Functions
 (*) Hosting: Configure and deploy Firebase Hosting sites
 ( ) Storage: Deploy Cloud Storage security rules
 ( ) Emulators: Set up local emulators for Firebase features
? Please select an option: (Use arrow keys)
> Use an existing project
  Create a new project
  Add Firebase to an existing Google Cloud Platform project
  Don't set up a default project

Firestore Rulesについてはデフォルトのまま Enter

? What file should be used for Firestore Rules? (firestore.rules)

Firestore indexesについてもデフォルトのまま Enter

? What file should be used for Firestore indexes? (firestore.indexes.json)
? What do you want to use as your public directory? (public)

SPAについての設定も y で。

? Configure as a single-page app (rewrite all urls to /index.html)? (y/N)

firebase-tools のアップデート

   ╭───────────────────────────────────────────╮
   │                                           │
   │     Update available 8.4.3 → 8.15.1       │
   │   Run npm i -g firebase-tools to update   │
   │                                           │
   ╰───────────────────────────────────────────╯
>npm i -g firebase-tools

firebase.config の作成

firebase.configファイルをプロジェクトのディレクトリに作成して、 Firebase SDK snippet を書き込む。
Firebase SDK snippet は、Firebaseコンソールの「設定」-「全般」を参照する。

export default {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
    measurementId: ""
}

firebase.configファイルは、gitignoreに設定する。

# others
firebase.config

vuex(ビューエックス)をインストール

vuexを利用するので、インストールする。

npm install vuex --save

firebaseuiをインストール

npm install firebaseui --save

FirebaseUIのcssを読み込ませる

FirebaseUIのcssは nuxt.config.js で読み込んでおく。

~/nuxt.config.js
  css: [
    "firebaseui/dist/firebaseui.css",
  ],

Firebaseの初期化処理のコーディング

Firebaseの初期化処理をpluginsに書く。firebase.configの読み込み、SNS認証の初期設定、フィルターの読み込み。
pluginsフォルダで実装したjsは、「root Vue.jsアプリケーションがマウント」される前に実行される。

フィルターは、金額をカンマ区切りで表示したい場合等に利用する。
~/filter/filters.js にファイルを作成し、pluginsで読み込ませることで、どのページでも利用できるようになる。

~/plugins/firebase.js
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
import config from '~/firebase.config';
import Vue from 'vue';
import * as filters from '~/filter/filters';

if (!firebase.apps.length) {
  firebase.initializeApp(config);
  firebase.analytics();
}

// initialize authorization
export const authProviders = {
  // 使うものだけ定義しておきましょう
  Email: firebase.auth.EmailAuthProvider.PROVIDER_ID,
  Google: firebase.auth.GoogleAuthProvider.PROVIDER_ID,
  Facebook: firebase.auth.FacebookAuthProvider.PROVIDER_ID,
  Twitter: firebase.auth.TwitterAuthProvider.PROVIDER_ID,
  Github: firebase.auth.GithubAuthProvider.PROVIDER_ID
};

export const auth = firebase.auth();

// initialize Firestore
const db = firebase.firestore();
export { db };

// import filters
Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key]);
});
~/filter/filters.js
// 数字の3桁ごとにカンマ区切りをつけるフィルタ
export function number_format (value) {
  if (value === 0) { return 0; }
  if (value === "0") { return 0; }
  if (! value) { return 0; }
  return value.toString().replace( /([0-9]+?)(?=(?:[0-9]{3})+$)/g , '$1,' );
}

// yyyymm文字列を"yyyy年mm月"文字列にするフィルタ
export function yyyymm_format (value) {
  if (! value) { return false; }
  return value.substr( 0, 4 ) + "年" + value.substr(4) + "月";
}

プラグインをNuxt.jsで使えるように設定ファイル nuxt.config.js を編集する。

~/nuxt.config.js
  plugins: [
    '~/plugins/firebase',
  ],

SNS認証のログイン/ログアウトのステータスを保存する

~/store/auth.js
import Vue from 'vue';
import { auth } from '~/plugins/firebase';

export const state = () => ({
        user: {},
        status: ""
    });

export const mutations = {
        setUser(state, user) {
            state.status = "loggedIn";
            state.user = user;
        },
        logout(state) {
            state.status = "loggedOut";
            state.user = {};
        }
    };

export const getters = {
        isLoggedIn: (state) => {
          return state.status === "loggedIn";
        },
        getUsername: (state) => state.user.displayName,
        getUid: (state) => state.user.uid
    };

export const actions = {
        gotUser({ commit }, user) {
            commit("setUser", user);
        },
        logout({ commit }) {
            auth.signOut().then(() => {
                commit("logout");
            })
        },
    };

ログイン/ログアウトの認証情報による処理の追加

middlewareはページがレンダリングされる前に実行される。
(すべての画面遷移の前に実行される)

middlewareに、ログイン/ログアウトなどの認証情報の変化で発火するonAuthStateChangedを設定しておく。

さらに、 firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION); を追加する。
これによって、認証情報をローカルストレージに保存することができるようになる。

~/middleware/authenticated.js
import firebase from 'firebase';
import { auth } from '~/plugins/firebase';

export default function ({ route, store, redirect }) {
  auth.setPersistence(firebase.auth.Auth.Persistence.SESSION);
  auth.onAuthStateChanged((user) => {
    if (user) {
      store.dispatch("auth/gotUser", user);
    } else {
      // ログアウトせずにタブを消して、ブラウザを閉じずにまたURLから画面を開いたとき、
      // firebase.authのuserがnullなのに、ローカルストレージの情報が残っていてstoreのauthはログイン状態になっている。
      // そこで、ここで一度ログアウトを実行しておく。
      store.dispatch("auth/logout");
      // もしログインしていなかったらログインページにリダイレクト
      if(route.name !== "login") redirect("/login");
    }
  });
}
~/middleware/notAuthenticated.js
export default function ({ store, redirect }) {
  if (store.getters["auth/isLoggedIn"]) {
    return redirect('/');
  }
}

ログインボタンを表示するUI部分を作成

下記はPUGを利用したHTML記述方法になっている。
firebaseuiのimportはエラーが出たため、requireを利用することにした。

~/components/FirebaseAuth.vue
<template>
  <div id="firebaseui-auth-container"></div>
</template>

<script>
import { auth, authProviders } from '~/plugins/firebase';

export default {
  name: 'FirebaseAuth',
  mounted() {
    auth.onAuthStateChanged(user => {
      if (!user) {
        const firebaseui = require('firebaseui');

        const ui = firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(auth);

        const config = {
          signInOptions: [
            // authProviders.Email,
            authProviders.Google,
            // authProviders.Facebook,
          ],
          callbacks: {
          },
          signInSuccessUrl: '/',
          signInFlow: 'popup', // ログインフロー設定。Nuxtのローカルサーバーで起こるCORSエラーがあるのでpopupがオススメです。
        };

        ui.start('#firebaseui-auth-container', config);
      }
    });
  }
}
</script>

signInFlowで設定しているログインフロー設定とは、サインアップフローをポップアップにするか、リダイレクトにするかなどを設定するためのものです。

ログインページの作成

pagesディレクトリに、login.vueを作る。とりあえず中身はない。

~/pages/login.vue

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

<script>
import { mapState, mapGetters, mapActions } from "vuex";

export default {
  middleware: 'notAuthenticated',
  name: 'Login',
}
</script>

ヘッダーの作成

ログイン/ログアウトをヘッダーに実装する。

~/components/Header.vue

<template>
  <v-app-bar
    :clipped-left="clipped"
    fixed
    app
  >
    <v-app-bar-nav-icon v-if="isLoggedIn" @click.stop="drawer = !drawer" />
    <v-btn
      icon
      v-if="isLoggedIn"
      @click.stop="miniVariant = !miniVariant"
    >
      <v-icon>mdi-{{ `chevron-${miniVariant ? 'right' : 'left'}` }}</v-icon>
    </v-btn>
    <v-toolbar-title></v-toolbar-title>
    <v-spacer />
    <v-btn text='' v-if="isLoggedIn" @click="logout"> Logout </v-btn>
    <FirebaseAuth/>
  </v-app-bar>
</template>

<script>
import FirebaseAuth from '@/components/FirebaseAuth.vue';
import { mapGetters, mapActions } from "vuex";

export default {
  components: {
    FirebaseAuth
  },
  computed: {
    ...mapGetters("auth", [
      "isLoggedIn",
      "getUsername",
      "getUid"
    ]),
  },
  methods: {
    ...mapActions("auth", ["logout"])
  }
}
</script>

フッターの作成

~/components/Footer.vue

<template>
  <v-footer>
    <div class="flex-grow-1"></div>
    <div v-if="getUid"><p class="grey--text" style="font-size: small;">
      UID: {{getUid}}
    </p></div>
  </v-footer>
</template>

<script>
import { mapGetters } from "vuex";

export default {
  computed: {
    ...mapGetters("auth", [
      "getUid"
    ]),
  },
}
</script>

FirebaseコンソールでSNS認証の利用を開始

  • Firebaseコンソール の「Authentication」で「始める」ボタンを押下する。
  • 「Sign-in method」で Google を有効にする。

Firebase Hostingの初期設定

以下のコマンドを実行する。

firebase init hosting

publicディレクトリは dist を指定する。

? What do you want to use as your public directory? dist
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
? Set up automatic builds and deploys with GitHub? No
? File dist/index.html already exists. Overwrite? No

ローカル環境でテスト

これでSNS認証によるログインができるようになった。。はず

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js 3 : 複数項目のInputで、最後のInputが入力されたら自動的に次のInputを追加する

フォームを作っていると、次のように、入力された数が増えた時に、入力欄を自動で追加してほしいことがあります。

image.png

これを Vue.js でやる場合のコードを紹介します。

コード

  <form action='/send_email' method='post'>
    <p>
      メールを送ります。
      送信先のメールアドレスを入力してください。
    </p>
    <div id="email_list">
      <div class="row" v-for="(email, index) in emails">
        {{index + 1}}: <input type="email" name="email[]" v-model="email.value">
      </div>
    </div>
    <button type="submit">Submit</button>
  </form>
  <script>
    Vue.createApp({
      data() {
        return {
          emails: [{value: ''}]
        }
      },
      beforeUpdate() {
        const emails = this.$data.emails
        if (emails[emails.length - 1].value.trim()) emails.push({value:''})
      }
    }).mount('#email_list')
  </script>

beforeUpdate を使って入力値の状態をチェックして入力欄を増やしています。

beforeUpdate は、 データが変更されるとき、DOM が適用される前に呼ばれます。

環境

  • Vue.js 3.0.2
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

axiosでエラー情報を取得

概要

axiosでAPI通信を行い、APIの方でエラーが発生した場合のエラー情報の取得方法についてまとめていきたい。

catchで取得したエラーの中身をみていく
axios
axios.get('https://example.com', params)
.then(function(response) {

    //正常時にAPIから返却されるレスポンスデータ
    return response;
  })
  .catch(function(error) {

    //エラー時にAPIから返却されるレスポンスデータ
    error.response.data();
  });

キャッチしたHTTPステータスコードをフロント側で処理することも可能そうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue component の 配列データを更新したのに値がviewに反映されない。

LTするまでもないが、簡単なのにVue ですげーハマった話。

問題

  • vue component の Array data に対して、代入更新を行ったのに、template に反映されない。

解決

直接代入を行っても、vue側のレンダリングトリガーを引くことができないため、規定のレンダリングトリガーに対応したmethod を call する必要がある。

push, splice, shift… など。

これが起きた時には、 this.$nexttik は有効に働かないので、上記のトリガーを引かせる。
直接代入はやめようぜって話だった。

参考になったlink
- https://aqua-engineer.com/vue-js-reactve/
- https://jp.vuejs.org/2016/02/06/common-gotchas/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VSCodeで使えるNuxt ×TypeScript × CompositionAPIの便利スニペッツ

VSCode上でNuxt,TypeScript,CompositionAPIで開発するときに毎回同じ内容を書くのが面倒だなと思ったのでスニペットを作ってみました。誰かの参考になれば嬉しいです:)

vue.json
{
    "nuxt3+ts template": {
        "prefix": "nuxt3+ts template",
        "body": [
            "<template>",
            "",
            "</template>",
            "",
            "<script lang=\"ts\">",
            "import { defineComponent } from '@nuxtjs/composition-api'",
            "export default defineComponent({",
            "  name: '$1',",
            "  setup(){}",
            "})",
            "</script>",
            "",
            "<style scoped></style>"
        ]
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue】メソッドを外部のjsファイルに移動する方法。外部ファイルの処理結果をvueに戻す。

メソッドを外部のjsファイルに移動する方法について。

変数を外部ファイル(.js)に切り出してexport defaultでインポートする方法はこちら

今回は応用編として、.vue内のメソッドを外部ファイルに切り出す方法。

流れと考え方

操作の流れは、vueのデータをjsに渡して、処理後に戻す作業になる。

(1).vue -> (2).js -> (3).vue



(1) .vueの処理
・vueのメソッドを実行
・メソッド内で変数を定義し、jsファイルにプロパティ名でデータを渡す。

(2) .jsの処理
・vueのデータを引数としてプロパティで受け取り処理を実行後、戻り値で返す。

(3) .vueの処理
.jsから戻ってきた値を、vueの値(this.xxx)に代入する。



▼考え方
Inputする(渡す)データとOutputする(戻す)データの受け渡しを意識することが大切。

.vueの処理(概念)
methods:{
    vueのメソッド名(){
      const {受け取るデータ} = jsファイルのコマンド名({
        渡すデータ
      })

      vueのデータ = 受け取るデータ
    }
  } 
.jsの処理(概念)
const メソッド名 = ({受け取るデータ}) => {
    処理
  return {
    渡すデータ
  }
}

export default 渡すメソッド

jsファイルに引数としてデータを渡し、処理後にreturnでvue側の変数名と一致するデータを返す。

最後に、受け取ったデータをvueのデータに入れて受け取り完了となる。


実例

実例で見るとよりわかりやすい。

.vue(scriptの中身)
//外部ファイル(transformationJs.js)からメソッドをjsMultiとしてインポート
import jsMulti from "./transformationJs"

export default {
  data(){
    return{
      A: 1,
      B: 2,
      x: 100,
      y: 200,
    }
  },

  //ここが重要!jsファイルのメソッドとデータをやりとりする記述
  methods:{
    btnClicked(){
      //処理結果をいれるプロパティを用意
      const {A, B} = jsMulti({
        //引数内でプロパティ名を指定してデータを渡す
        A: this.A,   
        B: this.B,
        x: this.x,
        y: this.y
      })

      //vueのデータにjsファイルの処理結果を代入
      this.A = A
      this.B = B
    }
  }
}

読み込んだのはテンプレートではなくメソッドなので、components:{}は不要

.js
//メソッドを定義
const jsMulti = ({A, B, x, y}) => {
  //処理結果を代入する変数を定義
  const _A = A * x
  const _B = B * y

  return {  //ここが重要!
    //vue側で設定したプロパティにデータを入れる
    A: _A,
    B: _B
  }
}

//外部ファイルに渡すメソッド
export default jsMulti

・.js側で処理済みの値はわかりやすいように_を付けたが、これは別に何でもいい。(Aのままでもいいし、他の新しい変数名でも可)

・.jsのメソッドは最後にreturnをプロパティとして返す。

A = _Aはエラーになる。(※プロパティの形になっていない)

Aのみで値を入れない場合は、空のデータが返る。 (※AはA:Aの省略表記となるため、変数Aを定義していない場合は空のプロパティを返したことになる。)

・A:Aの省略表記Aをshort propertyと呼ぶ


処理結果事例とフルコード

上記の実例は、ボタンをクリックする毎に、A × 100、B × 200をする処理となる。

image.png

↓ クリック

image.png

↓ クリック

image.png

▼フルコード

.vue
<template>
  <div>
   <!-- クリックイベントでbtnClickedを発火 -->
    <button
      @click="btnClicked"
    >【Click Me】</button>

    <p>・A: {{A}}</p>
    <p>・B: {{B}}</p>

  </div>
</template>

<script>
import jsMulti from "./transformationJs"

export default {
  data(){
    return{
      A: 1,
      B: 2,
      x: 100,
      y: 200,
    }
  },
  methods:{
    btnClicked(){
      const {A, B} = jsMulti({
        A: this.A,
        B: this.B,
        x: this.x,
        y: this.y
      })

      this.A = A
      this.B = B
    }
  }
}
</script>
transformationJs.js
const jsMulti = ({A, B, x, y}) => {
  const _A = A * x
  const _B = B * y

  return {
    A: _A,
    B: _B
  }
}

export default jsMulti


配列の中のコマンドを実行する例

v-forで表示した配列として格納されているボタンに対し、外部ファイルの処理を実行する場合の例。(*流れのみ)

(1)ボタンタグをクリックするとメソッドが発火する。
このとき、引数でクリックされたボタンの要素を渡す。

@click="onClickButton(command)

(2)ボタンクリックで発火させる処理
command.onClickでJSファイル内のプロパティonClickを実行している。

.vue
<script>
import cellCommands from "./commands"

export default {
  methods:{
    onClickButton(command){

      const {
        //js内で処理後の値を格納する変数を用意(output用)
        rows,
        selectedCells,
      } = command.onClick({  //jsファイルに渡すデータ(input用)。プロパティ名: 値 。returnで返す。
        selectedCells: this.selectedCells,
        rows: this.innerValue,
        maxColNum: this.maxColNum,
        thPosition: this.thPosition,
      })

      //jsの出力をvueに渡す(output)
      this.innerValue = rows
      this.selectedCells = selectedCells
    },
  }
}
</script>

各要素のプロパティonClickの中に記述された処理が実行される。

commands.js
const commands = [
  {
    cmd: 'clearSelects',
    text: "選択解除",
    icon: 'mdi-cancel',
    isActive: anySelectedCells,
    onClick: ({
      rows,
    }) => {
      return {
        selectedCells:[],
        rows,
      }
    }
  },
  {
    cmd: 'addColumnBefore',
    text:"左に列追加",
    icon: 'mdi-table-column-plus-before',
    isActive: (params) => notThColandSelectedFirstCol(params) && singleSelectedCell(params),
    onClick: ({
      selectedCells,
      rows,
      thPosition
    }) => {

      if(selectedCells.length != 1) {
        return {
          selectedCells,
          rows,
        }
      }

      //変換後の出力の値は別の変数に代入。一時的として、_変数名とする。
      const _rows = rows.map((tr, index) => {
        let newCell
        if(thPosition == 'row' && index === 0){
          newCell = initTh()
        }else{
          newCell = initTd()
        }
        tr.article_items = [
          ...tr.article_items.slice(0, selectedCells[0].cellIndex),
          ...[newCell],
          ...tr.article_items.slice(selectedCells[0].cellIndex),
        ]
        return tr
      })

      const _selectedCells = selectedCells.map((cell)=>{
        const {cellIndex: cCellIndex} = cell
        if(cCellIndex >= selectedCells[0].cellIndex){
          return {
            ...cell,
            cellIndex: cCellIndex + 1
          }
        } else {
          return cell
        }
      })

      //vueに戻すoutput用
      return {
        selectedCells: _selectedCells,
        rows: _rows,
      }
    }
  },
  {
    cmd: 'addColumnAfter',
    text:"右に列追加",
    icon: 'mdi-table-column-plus-after',
    isActive: singleSelectedCell,
    onClick: ({
      selectedCells,
      rows,
      thPosition,
    }) => {
      if(selectedCells.length != 1) {
        return {
          selectedCells,
          rows,
        }
      }

      const _rows = rows.map((tr, index) => {
        let newCell
        if (thPosition == 'row' && index === 0) {
          newCell = initTh()
        } else {
          newCell = initTd()
        }

        return {
          ...tr,
          article_items: [
            ...tr.article_items.slice(0, selectedCells[0].cellIndex + 1),
            ...[newCell],
            ...tr.article_items.slice(selectedCells[0].cellIndex + 1),
          ]
        }
      })

      const _selectedCells = selectedCells.map((cell)=>{
        const {cellIndex: cCellIndex} = cell
        if(cCellIndex > selectedCells[0].cellIndex){
          return {
            ...cell,
            cellIndex: cCellIndex + 1
          }
        }else {
          return cell
        }
      })

      return {
        rows: _rows,
        selectedCells: _selectedCells
      }
    }
  },
  {
    cmd: 'removeColumn',
    text:"列削除",
    icon: 'mdi-table-column-remove',
    color: 'error',
    isActive: (params) => anySelectedCells(params) && notAllColumnsSelected(params),
    onClick: ({
      selectedCells,
      rows,
      maxColNum,
    }) => {
      const cellIndicesToRemove = new Set(
        selectedCells.map((cell) => cell.cellIndex)
      )

      if(cellIndicesToRemove.size == maxColNum) {
        return
      }

      const _rows = rows.map((row) => {
        return {
          ...row,
          article_items: row.article_items.filter((cells, cellIndex)=> 
            !cellIndicesToRemove.has(cellIndex)
          )
        }
      })

      return {
        rows: _rows,
        selectedCells: []
      }
    },
  },
  {
    cmd: 'addRowAfter',
    text:"下に行追加",
    icon: 'mdi-table-row-plus-after',
    isActive: singleSelectedCell,
    onClick: ({
      selectedCells,
      rows,
      maxColNum,
      thPosition,
    }) => {
      if(selectedCells.length != 1) {
        return {
          selectedCells,
          rows,
        }
      }

      const _rows = [
        ...rows.slice(0, selectedCells[0].rowIndex + 1),
        ...[getTr( {maxColNum, thPosition} )],
        ...rows.slice(selectedCells[0].rowIndex + 1),
      ]

      const _selectedCells = selectedCells.map((cell) => {
        const { rowIndex: cRowIndex } = cell
        if (cRowIndex > selectedCells[0].rowIndex){
          return {
            ...cell,
            rowIndex:  cRowIndex + 1,
          }
        } else {
          return cell
        }
      })

      return {
        rows: _rows,
        selectedCells: _selectedCells
      }
    },
  },
  {
    cmd: 'addRowBefore',
    text:"上に行追加",
    icon: 'mdi-table-row-plus-before',
    isActive: (params) => singleSelectedCell(params) && notThColandSelectedFirstRow(params),
    onClick: ({
      selectedCells,
      rows,
      maxColNum,
      thPosition,
    }) => {

      if(selectedCells.length != 1) {
        return {
          selectedCells,
          rows,
        }
      }

      const _rows = [
        ...rows.slice(0, selectedCells[0].rowIndex),
        ...[getTr( {maxColNum, thPosition} )],
        ...rows.slice(selectedCells[0].rowIndex),
      ]

      const _selectedCells = selectedCells.map((cell) => {
        const { rowIndex: cRowIndex } = cell
        if (cRowIndex >= selectedCells[0].rowIndex){
          return {
            ...cell,
            rowIndex:  cRowIndex + 1,
          }
        } else {
          return cell
        }
      })

      return {
        rows: _rows,
        selectedCells: _selectedCells
      }
    }
  },
  {
    cmd: 'removeRow',
    text:"行削除",
    icon: 'mdi-table-row-remove',
    color: 'error',
    isActive: (params) => anySelectedCells(params) && notAllRowsSelected(params),
    onClick: ({
      selectedCells,
      rows,
    }) => {
      const rowIndicesToRemove = new Set(
        selectedCells.map((cell)=> cell.rowIndex)
      )

      if(rowIndicesToRemove.size == rows.length) return

      const _rows = rows.filter((row, rowIndex)=> 
        !rowIndicesToRemove.has(rowIndex)
      )

      return {
        rows: _rows,
        selectedCells: []
      }
    },
  }
]

export default commands

commands.jsの中身はかなり長いので、vueファイルから切り出すことでコードを見やすくすることができる。


参考リンク

export defaultとimport fromの使い方
複数の変数やメソッドを渡す方法
MDN公式 export default

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】コンポーネントを使うとうまく表示されない時の解決策

はじめに

Vue.jsを学習している最中にコンポーネントの使い方について躓いたところがあったので、
記録かつ同じ所で躓いた方のためになればと思い、投稿します。

コンポーネントとは?

公式サイト引用

Vue においては、「コンポーネント」は本質的にはあらかじめ定義されたオプションを持つ Vue インスタンスです。

簡単に言い換えると独自のHTMLタグを再利用したいときに使用するものですかね。

使ってみる

jsファイルに"user-item"というコンポーネントを作成。

sample.js
Vue.component('user-item', {
  props: ['user']
  template: '<li>{{ user.name }}</li>'
})

new Vue({
  el: "app-user"
  data: {
    users: [
      {id: 001, name: "Sato"},
      {id: 002, name: "Tanaka"},
      {id: 003, name: "Suzuki"},
    ]
  } 

HTML側で"user-item"タグを使用する。

sample.html
    <div id="app-user">
      <ol>
        <user-item
          v-for="user in users"
          v-bind:user="user"
          v-bind:key="user.id"
        ></user-item>
      </ol>
    </div>

画面での表示はこうなります。
1. Sato
2. Tanaka
3. Suzuki

躓いたポイント

上記のようにV-forを使ってテーブルでも同じことがしたいと思い試したところ、うまくいきませんでした。
試したコード

table.js
Vue.component('user-table', {
  props: ['user'],
  template: '\
    <tr>\
      <td>{{ user.id }}</td>\
      <td>{{ user.name }}</td>\
    </tr>\
    '
})

new Vue({
  el: "#app-table",
  data: {
    users: [
      {id: 001, name: "Sato"},
      {id: 002, name: "Tanaka"},
      {id: 003, name: "Suzuki"},
    ]
  }
});
table.html
    <div id="app-table">
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>NAME</th>
          </tr>
        </thead>
        <tbody>
          <user-table
            v-for="user in users"
            v-bind:user="user"
            v-bind:key="user.id"
          ></user-table>
        </tbody>
      </table>
    </div>

画面での表示
1Sato
2Tanaka
3Suzuki
ID NAME

ヘッダーが一番最後にきてしまっています。

解決策

HTMLにはタグの入れ子にルールがあり、なんでも入れれるわけではありません。
tbodyタグに関してMDN調べると、

許可されている内容 : 0 個以上の tr 要素

つまり、今回のuser-tableタグはHTMLのルールに引っかかっていることが分かりました。

そんな場合にVueではis属性というものが提供されています。
is属性を使用したHTMLコード

table.html(修正後)
    <div id="app-table">
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>NAME</th>
          </tr>
        </thead>
        <tbody>
          <tr is="user-table"
            v-for="user in users"
            v-bind:user="user"
            v-bind:key="user.id"
          ></tr>
        </tbody>
      </table>
    </div>

画面での表示
table.png

うまく表示できました。

まとめ

HTMLの入れ子タグにはルールがあるので、is属性を使って回避しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsの$refsがundefinedになってしまう問題

console.log(this.$refs.hoge)

としても、undefinedが返ってしまってくる場合について、ハマったのでまとめておきます。

二つの原因

  • createdでthis.$refsの処理をしている
  • v-showではなく、v-ifを使っている

createdでthis.$refsの処理をしている

index.vue
<template>
  <div ref="hoge">
    ~
  </div>
</template>
<script>
export default{
  created(){
    const hoge = this.$refs.hoge
    // 何らかの処理
  }
}
</script>

createdではなく、mountedを使うとうまくいきました。
基本的に、createdはDOM処理未完了、mountedはDOM処理完了の状態ですが、refを使う場合は該当のDOMが読み込まれていないといけません。
ページ描写時にrefの処理をするときは、mountedに記載しておいたほうがよさそうです。
createdでもうまくいくことがありましたが、ページの重さによるようです。

v-showではなく、v-ifを使っている

前述したとおりDOMが生成されていないとrefの処理ができませんが、v-ifを使うとDOMがそもそも生成されません。
v-showならば、DOMを生成しつつdisplay:none;にしてくれるので、refの処理が通ります。

通らない例

index.vue
<template>
  <div ref="hoge" v-if="isHogeExist">
    ~
  </div>
  <p v-else>hogeはないよ</p>
  <button @click="isHogeExist = !isHogeExist"></button>
</template>
<script>
export default{
  data(){
    return{
      isHogeExist:false
    }
  }

  mounted(){
    const hoge = this.$refs.hoge
    // 何らかの処理
  }
}
</script>

通る例

index.vue
<template>
  <div ref="hoge" v-show="isHogeExist">
    ~
  </div>
  <p v-show="!isHogeExist">Hogeはないよ</p>
  <button @click="isHogeExist = !isHogeExist"></button>
</template>
<script>
export default{
  data(){
    return{
      isHogeExist:false
    }
  }

  mounted(){
    const hoge = this.$refs.hoge
    // 何らかの処理
  }
}
</script>

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【1分解決】Vue.jsで小技を用いてフォームを作ってみる

はじめに

本記事へのアクセスありがとうございます。
投稿主はプログラミング初心者であり、この方法が「最適解」かは分かりません。
しかし、動作は検証済みです。

こんなやり方もできるんだ〜程度に見てもらえれば幸いです。

さっそくスタート

この記事では下記の環境のフォームを作成します。

・インプットタグでenterキーを押すと、次のインプットタグへ遷移する
・2つ目のタグ内には半角数字のみを出力する

 <div>
    <input
      type="text"
      v-model="formOne"
      @keypress.enter="focusWord"
      autofocus
    />
    <input
      type="text"
      ref="focusWord"
      v-model="formTwo"
    />
  </div>
data() {
    return {
      formOne: "",
      formTwo: "",
    };
  },
  watch: {
    formTwo: function (v) {
      this.formTwo = v.replace(/[A-Za-z0-9]/g, function (s) {
        return String.fromCharCode(s.charCodeAt(0) - 65248);
      });
      this.formTwo = v
        .replace(/[^0-90-9]/g, "")
        .replace(/[0-9]/g, (s) =>
          String.fromCharCode(s.charCodeAt(0) - 0xfee0)
        );
    },
  },
  methods: {
    firstWord() {
      this.$refs.focusWord.focus();
    },
  },

おわり

結論として、watchを利用してリアルタイムに文字判断をしてfocusメソッドを用いて遷移させましょう。

少しでも役に立ったと思う方がいましたらLGTMをお願いします?‍♂️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む