20200211のvue.jsに関する記事は12件です。

Vue.jsでのdeep selectorの書き方

久しぶりの投稿です。
備忘を兼ねて簡単に書きます。

deep selector?

Vue.js でscoped付でstyle を書いている場合に、そのコンポーネントの子コンポーネントにもstyleを追加したい場合に使用するcss のセレクターの書き方です。

書き方

.hoge >>> .huga{...}

>>>または/deep/で出来るとなっています。
(参考:https://vue-loader-v14.vuejs.org/ja/features/scoped-css.html)

が、効かない場合がある。その時は?

原因を調べていないのですが、Versionか構成かで上記の書き方だと効かない場合があります。
調べてみたところ、次の書き方だと意図したとおりになりました。

.hoge ::v-deep .huga{...}

どうも、>>>/deep/が解釈されなかったようです。
もしdeep selectorが解釈されていない場合があったら、::v-deepを試してください。

参考

Deep Selectors
https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors

Vue LoaderのDeep Selectorの::v-deep
https://blog.mahoroi.com/posts/2019/03/scoped-css-v-deep/

スコープ付き CSS
https://vue-loader-v14.vuejs.org/ja/features/scoped-css.html

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

Vue.js + Docker Compose で ホットリロードしながら開発する環境をつくる

はじめに

個人の開発用に Vue.js の動作環境を Docker に移行してみたら、とても捗ったのでメモがてら投稿します。

手順

前提

  • Vue.js のプロジェクトはできている
  • npm run serve で、 Vue.js が立ち上がるように設定されている

1. Dockerfile を作成

プロジェクトの直下に作成します。

FROM node:lts-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 8080

CMD ["npm", "run", "serve"]

これで、 Vue.js を動作させる Docker Image を作成することができます。

2. docker-compose.yml を作成

こちらもプロジェクト直下に作成します。

docker-compose.yml
version: "3"
services:
  app:
    container_name: web
    build: 
      context: .
      dockerfile: Dockerfile
    ports:
        - 8080:8080
    volumes: 
        - .:/app

volues でプロジェクトを Docker コンテナにマウントさせることで、 npm run serve の Hot Road が利くようになります。

起動する

プロジェクト直下で下記コマンドを実行します。

docker-compose up

localhost:8080 へアクセスすると、 Vue.js が起動しているのが確認できると思います。

終わりに

Dockerはありがたいですね。
少しでもあなたの為になれば幸いです。

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

Dynamic select-box with php using Vue.js

Introduction

There might be a demand that people would like to develop dynamic select box using Vue.js.
And I was the one of them.

Then, I coded like below, and It can be also available to use old value after post from form or back.

Code

index.php
<?php
    $selected_parent_category_id = ''; 
    $selected_child_category_list = ''; 
    if($_POST['parent_category_id']){
        $selected_parent_category_id = $_POST['parent_category_id'];
    }   
    if($_POST['child_category_id']){
        $selected_child_category_id = $_POST['child_category_id'];
    }   
?>       
<!DOCTYPE html>
<html lang="ja">
    <head>
        <title>Select box with Vue.js</title>
        <meta charset="utf-8">
        <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
        <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
    </head>
    <body>
        <div id="select-category">
            <form method="POST" action="./">
                <select v-on:change="selectParent" v-model="value['parent']" name="parent_category_id">
                    <option v-for="(value, key) in parent_list" v-text="value" v-bind:value="key"></option>
                </select>
                <select v-model="value['child']" name="child_category_id">
                    <option v-for="(value, key) in child_list" v-text="value" v-bind:value="key"></option>
                </select>
                <input type="submit" value="SUBMIT">
            </form>
        </div>
        <div style="display:none">
            <?php if($selected_parent_category_id): ?>
                <span id="selected_parent_category_id"><?php echo $selected_parent_category_id; ?></span>
            <?php endif; ?>
            <?php if($selected_child_category_id): ?>
                <span id="selected_child_category_id"><?php echo $selected_child_category_id; ?></span>
            <?php endif; ?>
        </div>
    </body>
    <script>
        var select_prefecture = new Vue ({
            el : '#select-category',
            data: {
                value:{
                    'parent': $('#selected_parent_category_id').text(),
                    'child': $('#selected_child_category_id').text(),
                },
                parent_list:{
                    0: "選択してください",
                    1: "日本料理",
                    2: "中華料理",
                    3: "イタリアン"
                },
                child_list:{},
                child_candidate_list:{
                    1: {
                        0: "選択してください",
                        1: "天ぷら",
                        2: "寿司",
                        3: "うどん",
                    },
                    2: {
                        0: "選択してください",
                        1: "回鍋肉",
                        2: "麻婆豆腐",
                        3: "焼売",
                    },
                    3: {
                        0: "選択してください",
                        1: "パスタ",
                        2: "ピザ",
                        3: "リゾット",
                    },
                },
            },
            methods: {
                selectParent() {
                    this.child_list = this.child_candidate_list[this.value['parent']];
                },
            },
            mounted() {
                if(this.value['parent']){
                    this.selectParent();
                }
            }
        });
    </script>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

はじめてのVue.js

はじめてこういう記事を書くのでお手柔らかにお願いします!!!

今回はVue.jsを使ってHelloWorldの出力までしていきましょう!!
今後回数を重ねてもっと深堀りしていく記事を書いていきますのでよろしくお願いします!

1. Vue.jsを使ってみよう!

Vue.jsを使うにあたって、scriptタグでライブラリを読み込みます。

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

これでVue.jsを使用することができるようになったので、次に進みます!

2. インスタンスを作ろう!

Vueは、Vue関数でVueインスタンスを作成することによって起動されます、これを実行するためには?
JavaScriptの特性として、new演算子を使えばインスタンスを作ることができますのでやってみましょう!

インスタンスが分からないという方はこちら見れば理解できるかも・・・?→https://wa3.i-3-i.info/word1118.html

var vm = new Vue({

})

これで準備ができました!

3.データを作ろう!

先ほどのインスタンス、あれを作る時に引数にオプションを渡すことでデータを定義することができます!

ここでは試しにお決まりの、Hello World!が入ったHelloWorldって名前のデータを定義してみましょう!

var vm = new Vue({
  data:{ HelloWorld: 'Hello World!'}
})

こちらdataオプションというものを使って、インスタンスの中のデータを定義しました!
ほぼ主要のオプションだと思うので是非覚えてください!
data: {プロパティ名: 値}

4.HTMLテンプレートを作ってみよう!

では早速先ほどHelloWorldに格納したHello World!を表示するための準備をしましょう!
その為には、HTMLテンプレートを作る必要があります。
Vue.jsでは({{ }}) ←を使ってインスタンスのデータを参照することができます。
では実際にやってみましょう!

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>

<body>
    <div id="test">
        <p>{{HelloWorld}}</p>
    </div>
    <script>
        var vm = new Vue({
            data: {
                HelloWorld: 'Hello world!'
            }
        });
    </script>

これでひとまずHTMLテンプレートの作成は終了です。
次にテンプレートとインスタンスを紐付けしていく方法を紹介します!

5.テンプレートとインスタンスを紐付けよう!

先ほどの順序でやったのに、あれ?HelloWorld!表示されないんだけど・・・ってなったと思います。
それは何故かというとテンプレートとインスタンスがくっついてなかったからなんです!バグやミスではありません!

そもそも、Vueの処理って、Vインスタンスを作ってどの要素と繋げるか(このことをマウントと呼ぶ)をしないと始まりません。
このマウントするためのオプションがel(elementの略)で指定した要素がマウントの対象になるわけです。
elを使ってHelloWorld!を表示させていきましょう~

var vm = new Vue({
            el: '#test',
            data: {
                HelloWorld: 'Hello world!'
            }
        });

このようにしたらブラウザを更新して確認してみてください!
HelloWorld!と表示されているはずです。

またこのelオプションはCSS、セレクタ、DOM要素を指定することができます。今回はidのtestを持つ要素を指定しました!

6.最後に

ここまでお疲れさまでした!
拙いかもしれませんが、できる限り分かりやすいように書いてみました!
こうして自分でアウトプットすることで頭に残る感覚がありますね・・・!
これを機にこれからもQiitaの執筆をしていきたいと思います´▽`

ここまで読んでいただきありがとうございました!:relaxed:

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

[Vuex] mutations, actions, gettersの引数について

きっかけ

Vuexではcontextからcontext.commitなどを実行できますが
contextはどこで使えて、第何引数に入るのかなどわからなかったため
調べてまとめようと思いました。

mutaitons, actionsの引数について、
公式ドキュメントを参考にしながらまとめていきます。

↓公式ドキュメント↓
https://vuex.vuejs.org/ja/api/#vuex-store

state

引数を話題にしたいので割愛します。公式ドキュメントを見てください。

mutations

ストアにミューテーションを登録します。ハンドラ関数は第一引数に state を常に受け取り(モジュール内で定義されていれば、モジュールのローカルステートを受け取り)、指定されていれば第二引数に payload を受け取ります。

第1引数:state
第2引数:payload(普通の引数)

書式:ex_mutation(state, payload)

mutations: {
  ex_mutation(state, payload) {
    処理
  }
}

actions

ストアにアクションを登録します。ハンドラ関数は次のプロパティを持つ context オブジェクトを受け取ります。(後述します)
そして、第 2 引数の payload があれば、それを受け取ります。

第1引数:context
第2引数:payload(普通の引数)

書式:ex_action(context, payload)

acitons: {
  ex_action(context, payload) {
    処理
  }
}

contextとは

※以降、上記で紹介したcontextはVuexのcontextという意味でvuexContextと表記させてもらいます。
というのも、Nuxt.jsでもcontextが独自で定義されており、それはvuexContextとは異なるものです。
ここでcontextと書いてしまうとVuexのcontextなのかNuxtのcontextなのかわからなくなる、と考えての対応です。
(余談ですがNuxt.jsのcontext は Nuxt から Vue コンポーネントへ追加のオブジェクト/パラメータを提供するもので、
nuxtServerInitやasyncDataなどで使用されます)


というわけで、vuexContextは、actionsの中でのみ使えることがわかりました。
そして、vuexContextは様々なオプションを持つらしいです。以下公式から抜粋です。


ハンドラ関数は次のプロパティを持つ context オブジェクト(vuexContext)を受け取ります。

{
  state,      // `store.state` と同じか、モジュール内にあればローカルステート
  rootState,  // `store.state` と同じ。ただしモジュール内に限る
  commit,     // `store.commit` と同じ
  dispatch,   // `store.dispatch` と同じ
  getters,    // `store.getters` と同じか、モジュール内にあればローカルゲッター
  rootGetters // `store.getters` と同じ。ただしモジュール内に限る
}


上記の上から3つ目のcommitを例にとります。
actionsの中でvuexContext.commitとすると、それすなわちstore.commitと同じ、ということです。
そして、VueComponentの内部ではstoreはthis.$storeとして参照しますので、
vuexContext.commit = store.commit ≒ this.$store.commitとなります。
(これちょっと例が悪かったですね。commitはVuexのactionsの中で参照するもので、Vueのcomponentの中で参照するものではないです。
VueComponent内だとthis.$store.dispatchをよく使うかと思います)


store/index.js(Nuxt使ってる前提)
state: {
  sentence: ""
}

mutations: {
  <!-- 第1引数:state, 第2引数:payload -->
  EX_MUTATION(state, payload) {
    state.sentence = payload
  }
}

actions: {
  <!-- 第1引数:context(=vuexContext), 第2引数:payload -->
  ex_action(vuexContext, payload) {
    vuexContext.commit("EX_MUTATION", payload)
  }
}

getters

ストアにゲッターを登録します. ゲッター関数は次の引数を受け取ります:

state,     // モジュール内で定義されていればモジュールのローカルステート
getters    // store.getters と同じ

第1引数:state
第2引数:getters

書式:ex_getter(state, getters)

  getters: {
    <!-- 第1引数:state のみ -->
    ex_getter: state => {
      return state.todos.filter(todo => todo.done)
    },

    <!-- 第1引数:state, 第2引数:getters -->
    <!-- gettersに登録されているex_getterを呼び出せる -->
    ex_getter_length: (state, getters) => {
      return getters.ex_getter.length
    }
  }

※モジュールでgettersを使う場合

モジュールで定義されたときの仕様

state,       // モジュールで定義された場合、モジュールのローカルステート
getters,     // 現在のモジュールのモジュールのローカルゲッター
rootState,   // グローバルステート
rootGetters  // 全てのゲッター

第1引数:state
第2引数:getters
第3引数:rootState
第4引数:rootGetters

書式:mod_getter(state, getters, rootState, rootGetters)

※詳しくはこちら
https://vuex.vuejs.org/ja/guide/modules.html#%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93%E4%BB%98%E3%81%8D%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%A7%E3%81%AE%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%82%A2%E3%82%BB%E3%83%83%E3%83%88%E3%81%B8%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9

まとめ

mutations:
 ex_mutation(state, payload)
actions:
 ex_action(vuexContext, payload)
getters:
 ex_getter(state, getters)【モジュール内ではない場合】
 mod_getter(state, getters, rootState, rootGetters)【モジュール内の場合】

余談

Vueで使えるオプション:
data(), methods, computed

Vuexで使えるオプション:
state, mutations, actions, getters

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

ゴリラでもわかるVue + nodemailerでメールフォーマットを作る

今回は、nuxt.jsfirebase Cloud Functionを用いて簡単なメールフォームを作ってみたいと思います。
nuxtfirebaseの簡単なインストール方法等はこちらからどうぞ。

対象者

nuxt.jsにまだあまり慣れてはいないが、メールフォームを作りたい人。
Laravelなどを使わずに、nuxtだけでメール機能を使いたい人。

では早速、firebase Cloud Functionsを初期化していきましょう。

firebase Cloud Functionの導入

今回はすでに、firebaseは入っているものとして考えさせてもらいます。

ターミナル.
firebase init -functions

まずは、firebase Cloud Functionsを目的のプロジェクトに作成します。

ターミナル.
? What language would you like to use to write Cloud Functions? (Use arrow keys)

❯ JavaScript 
  TypeScript 

Cloud Functionsに使う言語を聞かれるので、今回はJavaScriptを選択。

ターミナル.
? Do you want to use ESLint to catch probable bugs and enforce style? (y/N) 

ESLintの仕様の有無を聞かれます。
今回は使わないので、Nを選択。

ターミナル.
Do you want to install dependencies with npm now? (Y/n) 

依存関係について聞かれます。
ここは後で、めんどくさくなるのでYを選択。

これら全てを選択すると、インストールが始まります。

ターミナル.
✔  Firebase initialization complete!

これが表示されると、firebase Cloud Functionsの初期化に成功しています。

では早速、プロジェクトファイルの中身をみていきましょう。
スクリーンショット 2020-02-10 18.31.08.png

実際にプロジェクトの中身を見てみると、確かにプロジェクトディレクトリ直下に、/functionsができています。

今回のメール送信機能など、詳しい内容は基本的にfunctiuons/index.jsのなかに記述していきます。

nodemailerとは

node.jsからメールの送信などを可能にするモジュールです。
今回はこれを用いて、管理者に自動でメールを送るような仕様にしていきたいと思います。
公式はこちらからどうぞ。

nodemailerのインストール

ターミナル.
cd functions
npm install nodemailer --save

これだけで準備はオッケーです。

では早速メール送信機能を実装していきましょう。

nodemailer実装の下準備

nodemailerを実装する前に、送受信用のメールアドレスが必要になります。

ターミナル.
firebase functions:config:set gmail.email="送信用メールアドレス" gmail.password="送信用メールアドレスのパスワード" admin.email="受信用メールアドレス"

これを直接ターミナルに書き込んでおいてください。

念の為、設定できているか確認してみましょう。

ターミナル.
firebase functions:config:get
{
  "gmail": {
    "password": "送信用メールアドレスのパスワード",
    "email": "送信用メールアドレス"
  },
  "admin": {
    "email": "受信用メールアドレス"
  }

以上の様に表示されていれば、設定は完了しています。

注意点としては、送受信用のメールアドレスはgmailが望ましいです。他のものだと、追加料金が発生したり送受信できなかったりする場合があります。
また、受信用のメールアドレスは普段使っているものを使用してもいいですが、送信用のメールアドレスはfirebase Cloud Functionsから干渉を受けるため、セキュリティレベルを下げる必要があります。

Googleアカウントのセキュリティレベルの変更

Googleのトップページから、アカウントを選択。
スクリーンショット 2020-02-11 10.15.23.png
画面右のドロワー内のセキュリティを選択。
その後、安全性の低いアプリのアクセスをオンにしておく。
スクリーンショット 2020-02-11 10.15.41.png

これで送信用のメールアドレスの設定は終了です。

では、実際に実装をしていきましょう。

メール送信機能の実装

/functions/index.js
const functions = require('firebase-functions');
const nodemailer = require("nodemailer");
const gmailEmail = functions.config().gmail.email;
const gmailPassword = functions.config().gmail.password;
const adminEmail = functions.config().admin.email;

//SMTPサーバーの設定
const mailTransport = nodemailer.createTransport({
  service: "gmail",
  auth: {
    user: gmailEmail,
    pass: gmailPassword
  }
});

//メールの内容
const adminContents = function(data){
  return `ホームページにお問い合わせがありました。

お名前:
${data.name}

メールアドレス:
${data.email}

内容:
${data.contents}
`;
};

//実際の機能
exports.sendMail = functions.https.onCall(async function(data, context) {
//送信元、送信先、題名、内容を1つの変数にまとめる
  let adminMail = {
    from: gmailEmail,
    to: adminEmail,
    subject: "ホームページお問い合わせ",
    text: adminContents(data)
  };

//nodemailerのsendMail機能で、メールを送信する。
//変数は送信元などをまとめたもの
  try {
    await mailTransport.sendMail(adminMail);
   } catch (e) {
    return e.message;
   }
});

以上で、メール送信機能は完成です。

次にフォームを仕上げていきます。

メールフォームの実装

今回はvuetifyを使います。
完成形はこちら。

スクリーンショット 2020-02-11 10.42.37.png

/pages/contact.vue
<template>
    <v-content>
      <v-container>
        <v-col cols="80%">
          <v-card
            title="お問い合わせ"
            textarea="canter"
            class="card-background"
          >
            <v-col cols="100%">
              <v-col cols="80%" >
                <h3 class="card-title">お問い合わせ</h3>
              </v-col>
            <v-col cols="60%">
              <v-form
                ref="form"
                v-model="valid"
                lazy-validation
              >

        //入力中のみの表示
              <div v-bind:class="{ delatearea: inputdata }">
                <v-text-field
                  v-model="form.name"
                  :rules="nameRules"
                  label="お名前"
                  required
                ></v-text-field>
                <v-text-field
                  v-model="form.email"
                  :rules="emailRules"
                  label="メールアドレス"
                  required
                ></v-text-field>
                <v-textarea
                  v-model="form.contents"
                  outlined
                  auto-grow
                  label="お問い合わせ内容"
                  rows="10"
                  row-height="30"
                  class="contents-area"
                ></v-textarea>
              </div>

        //確認中のみの表示
              <div v-bind:class="{ delatearea: makesure }">
                <div class="sure-item">
                  <small>お名前</small>
                  <p>{{this.form.name}}</p>
                </div>
                <div class="sure-item">
                  <small>メールアドレス</small>
                  <p>{{this.form.email}}</p>
                </div>
                <div class="sure-item">
                  <small>お問い合わせ内容</small>
                  <p>{{this.form.contents}}</p>
                </div>
              </div>

        //入力中のみの表示
              <div v-bind:class="{ delatearea: inputdata }">
                <v-btn
                  color="error"
                  class="mr-4"
                  @click="reset"
                >
                  リセット
                </v-btn>
                <v-btn
                  :disabled="!valid"
                  color="success"
                  class="mr-4"
                  @click="validate"
                >
                  確認
                </v-btn>
              </div>

        //確認中のみの表示
              <div v-bind:class="{ delatearea: makesure }">
                <v-btn
                  color="error"
                  class="mr-4"
                  @click="back"
                >
                  戻る
                </v-btn>
                <v-btn
                  color="success"
                  class="mr-4"
                  @click="sendMail()"
                >
                  送信
                </v-btn>
              </div>

              </v-form>
            </v-col>
          </v-col>
        </v-card>
      </v-col>
    </v-container>
  </v-content>
</template>


<script>
import firebase from "~/plugins/firebase.js";

export default {
  data() {
    return {
      valid: true,

      //名前が入力されていないときにエラー文を表示させる
      nameRules: [
        v => !!v || 'お名前を入力してください。',
      ],
      emailerror: false,

      //メールアドレスが入力されていないときにエラー文を表示させる
      emailRules: [
        v => !!v || 'メールアドレスが適切ではありません。',
      ],

      inputdata: false,
      makesure: true,
      form: {
        name: '',
        email: '',
        contents: '',
      }
    }
  },
  methods: {
    validate () {

      //メールアドレスの形を確認する
      if (this.form.email.match(/.+@.+\..+/)) {
        this.inputdata = true
        this.makesure = false
      }else{
        this.emailerror = true;
        this.form.email = "";
      }
    },
    reset () {
      console.log(this.$refs.form.validate())
      this.form.name = "",
      this.form.email = "",
      this.form.contents = ""
      this.$refs.form.reset();
    },
    back () {
      this.inputdata = false;
      this.makesure = true
    },
    sendMail () {
      const tr = this.$router
      const form = this.form

      //ここで、functions/index.jsでexportsした、sendMailを呼ぶ
      const send = firebase.functions().httpsCallable("sendMail");
      send(form)
      .then(function(data) {
        alert("送信しました。");
        tr.push("/");
      })
      .catch(function(error) {
        alert("送信に失敗しました。");
      })
    }
  },
}
</script>


<style>
.card-title {
  color: #9c9797;
  text-align: center;
}
.card-background {
  width: 100%;
  background-color: white;
}
.delatearea {
  position: absolute;
  left: -9999px;
}
.sure-item > p {
  border-bottom: 1px solid #d2d5d2;
}
.contents-area {
  padding-top: 10px !important;
}
</style>

少し長くなりましたが、以上でメールのバリデーション・メールの送信ができる環境は整いました。

では実際に動かしていきましょう。

動作確認

まずはfirebaseにログインしておきましょう。

ターミナル.
firebase login

次にfirebase Cloud Functionsのデプロイを行います。

ターミナル.
firebase deploy --only functions
=== Deploying to 'プロジェクト名'...

i  deploying functions
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (26.72 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: creating Node.js 8 function sendMail(us-central1)...
✔  functions[sendMail(us-central1)]: Successful create operation. 
Function URL (sendMail): https://*****************.cloudfunctions.net/sendMail

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/*********/overview

この様に、最初にdeployした時だけFunction URLが表示されますが、二回目以降は表示されませんので、ご注意ください。

あとは実際に動作を確認してみたください。

ターミナル.
npm run dev

タイトルなし.gif

問題なく送れている様です。

何か間違いがあればご指摘いただけると幸いです。

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

Nuxt + nodemailerでメールフォーマットを作る

今回は、nuxt.jsfirebase Cloud Functionを用いて簡単なメールフォームを作ってみたいと思います。
nuxtfirebaseの簡単なインストール方法等はこちらからどうぞ。

対象者

nuxt.jsにまだあまり慣れてはいないが、メールフォームを作りたい人。
Laravelなどを使わずに、nuxtだけでメール機能を使いたい人。

では早速、firebase Cloud Functionsを初期化していきましょう。

firebase Cloud Functionの導入

今回はすでに、firebaseは入っているものとして考えさせてもらいます。

ターミナル.
firebase init -functions

まずは、firebase Cloud Functionsを目的のプロジェクトに作成します。

ターミナル.
? What language would you like to use to write Cloud Functions? (Use arrow keys)

❯ JavaScript 
  TypeScript 

Cloud Functionsに使う言語を聞かれるので、今回はJavaScriptを選択。

ターミナル.
? Do you want to use ESLint to catch probable bugs and enforce style? (y/N) 

ESLintの仕様の有無を聞かれます。
今回は使わないので、Nを選択。

ターミナル.
Do you want to install dependencies with npm now? (Y/n) 

依存関係について聞かれます。
ここは後で、めんどくさくなるのでYを選択。

これら全てを選択すると、インストールが始まります。

ターミナル.
✔  Firebase initialization complete!

これが表示されると、firebase Cloud Functionsの初期化に成功しています。

では早速、プロジェクトファイルの中身をみていきましょう。
スクリーンショット 2020-02-10 18.31.08.png

実際にプロジェクトの中身を見てみると、確かにプロジェクトディレクトリ直下に、/functionsができています。

今回のメール送信機能など、詳しい内容は基本的にfunctiuons/index.jsのなかに記述していきます。

nodemailerとは

node.jsからメールの送信などを可能にするモジュールです。
今回はこれを用いて、管理者に自動でメールを送るような仕様にしていきたいと思います。
公式はこちらからどうぞ。

nodemailerのインストール

ターミナル.
cd functions
npm install nodemailer --save

これだけで準備はオッケーです。

では早速メール送信機能を実装していきましょう。

nodemailer実装の下準備

nodemailerを実装する前に、送受信用のメールアドレスが必要になります。

ターミナル.
firebase functions:config:set gmail.email="送信用メールアドレス" gmail.password="送信用メールアドレスのパスワード" admin.email="受信用メールアドレス"

これを直接ターミナルに書き込んでおいてください。

念の為、設定できているか確認してみましょう。

ターミナル.
firebase functions:config:get
{
  "gmail": {
    "password": "送信用メールアドレスのパスワード",
    "email": "送信用メールアドレス"
  },
  "admin": {
    "email": "受信用メールアドレス"
  }

以上の様に表示されていれば、設定は完了しています。

注意点としては、送受信用のメールアドレスはgmailが望ましいです。他のものだと、追加料金が発生したり送受信できなかったりする場合があります。
また、受信用のメールアドレスは普段使っているものを使用してもいいですが、送信用のメールアドレスはfirebase Cloud Functionsから干渉を受けるため、セキュリティレベルを下げる必要があります。

Googleアカウントのセキュリティレベルの変更

Googleのトップページから、アカウントを選択。
スクリーンショット 2020-02-11 10.15.23.png
画面右のドロワー内のセキュリティを選択。
その後、安全性の低いアプリのアクセスをオンにしておく。
スクリーンショット 2020-02-11 10.15.41.png

これで送信用のメールアドレスの設定は終了です。

では、実際に実装をしていきましょう。

メール送信機能の実装

/functions/index.js
const functions = require('firebase-functions');
const nodemailer = require("nodemailer");
const gmailEmail = functions.config().gmail.email;
const gmailPassword = functions.config().gmail.password;
const adminEmail = functions.config().admin.email;

//SMTPサーバーの設定
const mailTransport = nodemailer.createTransport({
  service: "gmail",
  auth: {
    user: gmailEmail,
    pass: gmailPassword
  }
});

//メールの内容
const adminContents = function(data){
  return `ホームページにお問い合わせがありました。

お名前:
${data.name}

メールアドレス:
${data.email}

内容:
${data.contents}
`;
};

//実際の機能
exports.sendMail = functions.https.onCall(async function(data, context) {
//送信元、送信先、題名、内容を1つの変数にまとめる
  let adminMail = {
    from: gmailEmail,
    to: adminEmail,
    subject: "ホームページお問い合わせ",
    text: adminContents(data)
  };

//nodemailerのsendMail機能で、メールを送信する。
//変数は送信元などをまとめたもの
  try {
    await mailTransport.sendMail(adminMail);
   } catch (e) {
    return e.message;
   }
});

以上で、メール送信機能は完成です。

次にフォームを仕上げていきます。

メールフォームの実装

今回はvuetifyを使います。
完成形はこちら。

スクリーンショット 2020-02-11 10.42.37.png

/pages/contact.vue
<template>
    <v-content>
      <v-container>
        <v-col cols="80%">
          <v-card
            title="お問い合わせ"
            textarea="canter"
            class="card-background"
          >
            <v-col cols="100%">
              <v-col cols="80%" >
                <h3 class="card-title">お問い合わせ</h3>
              </v-col>
            <v-col cols="60%">
              <v-form
                ref="form"
                v-model="valid"
                lazy-validation
              >

        //入力中のみの表示
              <div v-bind:class="{ delatearea: inputdata }">
                <v-text-field
                  v-model="form.name"
                  :rules="nameRules"
                  label="お名前"
                  required
                ></v-text-field>
                <v-text-field
                  v-model="form.email"
                  :rules="emailRules"
                  label="メールアドレス"
                  required
                ></v-text-field>
                <v-textarea
                  v-model="form.contents"
                  outlined
                  auto-grow
                  label="お問い合わせ内容"
                  rows="10"
                  row-height="30"
                  class="contents-area"
                ></v-textarea>
              </div>

        //確認中のみの表示
              <div v-bind:class="{ delatearea: makesure }">
                <div class="sure-item">
                  <small>お名前</small>
                  <p>{{this.form.name}}</p>
                </div>
                <div class="sure-item">
                  <small>メールアドレス</small>
                  <p>{{this.form.email}}</p>
                </div>
                <div class="sure-item">
                  <small>お問い合わせ内容</small>
                  <p>{{this.form.contents}}</p>
                </div>
              </div>

        //入力中のみの表示
              <div v-bind:class="{ delatearea: inputdata }">
                <v-btn
                  color="error"
                  class="mr-4"
                  @click="reset"
                >
                  リセット
                </v-btn>
                <v-btn
                  :disabled="!valid"
                  color="success"
                  class="mr-4"
                  @click="validate"
                >
                  確認
                </v-btn>
              </div>

        //確認中のみの表示
              <div v-bind:class="{ delatearea: makesure }">
                <v-btn
                  color="error"
                  class="mr-4"
                  @click="back"
                >
                  戻る
                </v-btn>
                <v-btn
                  color="success"
                  class="mr-4"
                  @click="sendMail()"
                >
                  送信
                </v-btn>
              </div>

              </v-form>
            </v-col>
          </v-col>
        </v-card>
      </v-col>
    </v-container>
  </v-content>
</template>


<script>
import firebase from "~/plugins/firebase.js";

export default {
  data() {
    return {
      valid: true,

      //名前が入力されていないときにエラー文を表示させる
      nameRules: [
        v => !!v || 'お名前を入力してください。',
      ],
      emailerror: false,

      //メールアドレスが入力されていないときにエラー文を表示させる
      emailRules: [
        v => !!v || 'メールアドレスが適切ではありません。',
      ],

      inputdata: false,
      makesure: true,
      form: {
        name: '',
        email: '',
        contents: '',
      }
    }
  },
  methods: {
    validate () {

      //メールアドレスの形を確認する
      if (this.form.email.match(/.+@.+\..+/)) {
        this.inputdata = true
        this.makesure = false
      }else{
        this.emailerror = true;
        this.form.email = "";
      }
    },
    reset () {
      console.log(this.$refs.form.validate())
      this.form.name = "",
      this.form.email = "",
      this.form.contents = ""
      this.$refs.form.reset();
    },
    back () {
      this.inputdata = false;
      this.makesure = true
    },
    sendMail () {
      const tr = this.$router
      const form = this.form

      //ここで、functions/index.jsでexportsした、sendMailを呼ぶ
      const send = firebase.functions().httpsCallable("sendMail");
      send(form)
      .then(function(data) {
        alert("送信しました。");
        tr.push("/");
      })
      .catch(function(error) {
        alert("送信に失敗しました。");
      })
    }
  },
}
</script>


<style>
.card-title {
  color: #9c9797;
  text-align: center;
}
.card-background {
  width: 100%;
  background-color: white;
}
.delatearea {
  position: absolute;
  left: -9999px;
}
.sure-item > p {
  border-bottom: 1px solid #d2d5d2;
}
.contents-area {
  padding-top: 10px !important;
}
</style>

少し長くなりましたが、以上でメールのバリデーション・メールの送信ができる環境は整いました。

では実際に動かしていきましょう。

動作確認

まずはfirebaseにログインしておきましょう。

ターミナル.
firebase login

次にfirebase Cloud Functionsのデプロイを行います。

ターミナル.
firebase deploy --only functions
=== Deploying to 'プロジェクト名'...

i  deploying functions
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (26.72 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: creating Node.js 8 function sendMail(us-central1)...
✔  functions[sendMail(us-central1)]: Successful create operation. 
Function URL (sendMail): https://*****************.cloudfunctions.net/sendMail

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/*********/overview

この様に、最初にdeployした時だけFunction URLが表示されますが、二回目以降は表示されませんので、ご注意ください。

あとは実際に動作を確認してみたください。

ターミナル.
npm run dev

タイトルなし.gif

問題なく送れている様です。

何か間違いがあればご指摘いただけると幸いです。

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

Vue composition-api の テスタビリティを考える

はじめに

Vue composition-api を触っている人も多くいると思うが、これの利用方法についてテスト容易性の観点から考える。

View と ロジックを分離するからテストしやすい

単一ファイルコンポーネント (SFC) には、コンポーネントの表示に関するTemplete (HTML) と CSS そして、ロジックに関する Script (Javascript) が含まれている。
これは、興味を1ファイルに閉じ込めることができるため、直感的にわかりやすく、好きな人が多いと思う。

その一方で、ロジック部分をテストするだけでも vue-test-utils でコンポーネントをマウントするか、this コンテキストを明示して method をコールする必要があった。

import options from "./test-target.vue"

/**
 * this.count++ するようなメソッドの試験
 */
const state1 = {
  count: 1
}
options.methods.increment.bind(state1).call()

厳密には、メソッドなどのロジックは別ファイルに分けることができたが、リアクティブな値は this から値を参照する必要があった。

一方で composition-api はリアクティブな値を含めてコンポーネントと独立してロジックを記述することができる。

import { ref, computed } from "@vue/composition-api"
export default (initValue = 0)=> {
  const _count = ref(initValue)
  const count = computed(()=> _count)
  const increment = ()=> _count.value++
  return { count, increment }
}

テストもしやすい

テスト例
import composition from "./composition"
describe("test", ()=> {
   test("increment", ()=> {
      const {count, increment} = composition(100)
      expect(count.value).toBe(100)
      increment()
      expect(count.value).toBe(101)
   })
})

テスト容易性を高めるための提案

ここまでを踏まえて、テスト容易性を高める提案を2つする。

1. Vue コンポーネントでは依存性の注入のみを行う

composition は setup メソッドで実行して、そこからコンポーネントで使う値を return するが、この setup メソッドには原則としてロジックを書かないことにする。
これによって、もともとテストしにくい Vue コンポーネントにロジックを書かないようになる。
更に依存関係をコンポーネントで解決し、必要な値は composition に対して引数として注入するようになる。
これによってさらに composition のテスト容易性が高まる。

<template>
  <div>Hoge</div>
</template>
<script>
import { inject } from "@vue/composition-api"
import composition from "./composition"
export default {
  props: {
    foo: {
      type: String
    }
  },
  setup(props, { emit }){
     const state = inject('state')
     // props, inject や emit などを解決して注入
     return composition(props, emit, state)
  }
}
</script>

テストをするときは、モックオブジェクトなどを注入すればいい

テスト例
import composition from "./composition"
describe("test", ()=> {
   test("composition", ()=> {
      const mockProps = { foo: "foo" }
      const mockEmit = jest.fn()
      const mockState = { state: "value" }
      const cmp = composition(mockProps, mockEmit, mockState)
      // テストの記述...
   })
})

2. ライフサイクル・ウォッチャー 登録関数も注入する

ライフサイクル登録関数は @vue/composition-api から公開されている。
だが、ライフサイクル登録関数は setup 内で実行される必要があるため、これを composition で使用すると composition 単体で試験するのが難しくなる。

なので、あえて Vue コンポーネントから注入する。

composition例
export default (onMounted)=> {
  const count = ref(0)
  // initialize
  onMounted(()=> count.value = 1)
  return { count }
}

テストするときはモックを注入する

test例
import composition from "./composition"
describe("test", ()=> {
   test("onMounted", ()=> {
      const onMounted = jest.fn()
      const { count } = composition(onMounted)
      expect(count.value).toBe(0)
      onMounted.mock.calls[0][0]()
      expect(count.value).toBe(1)
   })
})

ウォッチャー登録関数はライフサイクル登録関数と違い、 setup で実行する必要はないが、コールバックの処理についてはテストが難しくなることが多いので、登録関数を外から注入する。

composition例
export default (watch)=> {
  const count = ref(0)
  const count2 = ref(0)
  const  = watch(()=> count.value, (newVal)=> {
    count2.value = newVal * 2
    stopWatch()
  })
  const increment = ()=> count.value++
  return { increment, count, count2 }
}

watch 停止関数を含めてモックする

test例
import composition from "./composition"
describe("test", ()=> {
   test("watch", ()=> {
      const watchMock = jest.fn()
      const watchStopMock = jest.fn()
      mock.mockReturnValue(watchStopMock);
      const { count, count2 } = composition(watchMock)
      expect(count2.value).toBe(0)
      // watch コールバックを実行
      watchMock.mock.calls[0][1](3)
      // 実行内容の検証
      expect(watchStopMock).toHaveBeenCalled();
      expect(count2.value).toBe(6)
   })
})

ところで Component の試験はどうするの?

そもそも、template に宣言的に記述してあるイベントハンドラやディレクティブに対してどれほど試験が必要でしょう?
レビューだけでは不十分でしょうか?

仮に、テンプレートの内部に値の変換などのロジックが含まれているようならそれを composition に移すべきです。

その上で、表示に対しての検証が必要であれば、それは Storybook などで確認すれば良いと思います。
変更の検知が必要なら、snapshot テストなどをすれば良いと思います。

テストツールとしての Storybook の利用については以下に考えを記述しています。
https://qiita.com/sterashima78/items/8db32368289e4859480b

更にその先の試験となれば Cypress などで E2E の試験をすることになると思います。

まとめ

composition のテスト容易性を高めたければ、 reactive value の生成とその変更関数のみを composition 内で実装し、それ以外は外から注入するようにしよう。

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

Vue.js初心者がNuxt.jsでポートフォリオサイトを作成するまでにやった勉強

人生初のQiita投稿です。

この記事を書こうと思った背景

昨年Nuxtとfirebaseで下記のポートフォリオサイトを作成しました。
https://hideokaizuka.com/

このポートフォリオサイトを作成する時にVue.jsやらNuxt.jsの勉強するのですが、色々と悩みました。

「何から始めればいいかわからない。」
「まだVue.js勉強するよりもJavaScriptの基礎固めた方がいいのではないか?」
「どうやらVuexというのもあるらしいが・・・」
「どんな教材が今の自分に合っているのか?」

などなど。

そこで、私がVue.jsとNuxt.jsを勉強する上で使った教材の感想やどういう流れで勉強していくのが一番効率的か?と言うのをまとめていきます。これからVue.jsやらNuxt.jsを勉強しようかなという方の参考になれば幸いです。

対象読者

Vue,Nuxtを勉強しようと思っている方

結論

Vue,Nuxt勉強.png
どんな流れで勉強していくのが最適かというと、上記の図が結論です。

自分はES6をわからないまま、Vue.jsを勉強しVueを完全にわかった気になり、
Vuexを知らないままNuxt.jsを勉強するという感じで進めてしまいました。

そのため、Nuxtを勉強する時に使った本に書いてあることが全然わからなかったのです。書いてある通りに写経しても意味がよくわかっておらず、時間を無駄にしてしまったかなぁと思っております。

当たり前ですが、自分に合ったレベル感から始めることは大切かなぁと思います。

JavaScriptの勉強で使った教材

JavaScript自体は仕事で使っていたので、Vue.jsやNuxt.jsのための勉強に使ったわけではありませんが、最初に勉強した時には下記の本を使いました。

改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで

JavaScriptの基礎的なところはこの本でも問題ないかなと思います。
ただプログラミング初心者だと厳しいかもしれないので、他言語学んだ方がJavaScriptを学ぶのにオススメです。

ES6の勉強で使って教材

ES6とは?
簡単にいうとJavaScriptの新しいバージョンといったところです。
色々新しい書き方があります。

Vue、ReactやっているとES6での記法がよく出てくるので、サラッと何か勉強しておいた方がいいかと思います。上記の本でも解説はしてますが、私は別途勉強しました。

私は下記の講座をやりました。
講座が長すぎず、コンパクトにまとまっているところがメリットかなと思います。

ECMAScript(ES6)基礎講座

文法や書き方が中心の講座というのは長いとモチベーション維持が難しいので、
長すぎないということも意識して選びました。

Vueの勉強で使った教材

次にVue.jsでの勉強に使った講座を2つ紹介します。

1.Vue JS 入門決定版!jQuery を使わない Web 開発 - 導入からアプリケーション開発まで体系的に動画で学ぶ

こちらの講座は本当に入門者向けです。かなり入門者向けを意識されているようなので、挫折率も低いかなぁと思っています。
ただ、Vuexについては解説がほとんどないので他にも勉強する必要があります。

2.超Vue JS 2 入門 完全パック - もう他の教材は買わなくてOK! (Vue Router, Vuex含む)

こちらの講座はVuexまで含まれています。タイトルの通りVueの網羅度はかなり高いです。
ただ少し早口で聞き取りにくいところが少し気になります。少し長いので、心が折れかかるとともあります。

Nuxtの勉強で使った教材

次にNuxtの勉強に使った教材を紹介します。

1.Nuxt JS入門決定版!Vue.jsのフレームワークNuxt JSの基本からFirebaseと連携したSPAの開発まで

Vueの講座で紹介した「Vue JS 入門決定版!jQuery を使わない Web 開発 - 導入からアプリケーション開発まで体系的に動画で学ぶ」と同じ講師の方なのですが、こちらも初心者向けを意識されている感じです。内容的には少しあっさりし過ぎかなぁという印象です。

ただ、現時点ではUdemyのNuxtの日本語の講座がこれしかない・・・・

2.Nuxt.jsビギナーズガイド―Vue.js ベースのフレームワークによるシングルページアプリケーション開発

この本はVueもVuexもある程度理解した方を対象とされている印象です。Vueの教材の1で紹介した講座をやってすぐにこの本に入った私はあまり理解できなかったですし、その状態でこの本を参考に自分のポートフォリオの作成をしようと試みるも全然ダメでしたw
ただ、Vue,Vuexの理解が進んでから読み直してみると理解が進みました。
Nuxtの内容はこの本がよくまとまっていると思います。

まとめ

自分がVue,Nuxtのために勉強した中であれば、

1.ECMAScript(ES6)基礎講座

2.超Vue JS 2 入門 完全パック - もう他の教材は買わなくてOK! (Vue Router, Vuex含む)

3.Nuxt.jsビギナーズガイド―Vue.js ベースのフレームワークによるシングルページアプリケーション開発

の順番が一番効率よかったかなぁと振り返って思います。

他にも有名な猫本とかもあるので、ここで紹介してなくても自分に合った勉強法で勉強されるといいかと思います。

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

Vuetify(v2.1)で画面サイズに合わせて、置くアイテム数を調整(グリッドシステムを使用)

はじめに

Vuetifyで横にアイテムを並べたい。
並べたアイテムをウィンドウサイズに合わせて、置く個数を調整したい。(置くアイテムが多いので複数行必要)
上記が簡単にできないか調べていたが、見つからなかったので作成した。

対応後の画面

今回の対応を実施すると以下の画面ができます。
Screenshot from 2020-02-10 22-14-17.png

*本来は色々なIDが表示されますが、サンプルのため私のID(tflare)にしています。
*この画面は作成中で、他の箇所が完成していません。

やったことを一言で言うと

ウインドウサイズを見て(Breakpointを使用)、1行に置くアイテムの数を決め、
グリッドシステムの行定義と列定義の両方でループをうまくやった。

Breakpointsの説明はVuetify.jsの該当ページ参照

環境

"firebase": "^7.8.1",
"vue": "^2.6.10",
"vue-router": "^3.1.3",
"vuefire": "^2.2.1",
"vuetify": "^2.1.0"

全コードはこちら

コードの全体を見たい方はGithub当ページの一番下を参照ください。
以下では分割して説明いたします。

Vueテンプレート部分

<template>
  <v-container fluid>
    <v-row v-for="(row, key) in rowCount" :key="key">
      <div v-for="(attendance, key2) in itemCountInRow(row)" :key="key2">
        <v-col>{{attendance.userID}}</v-col>
        <v-col><v-btn small color="primary">出席</v-btn><v-btn small color="error">欠席</v-btn></v-col>
        <v-divider/>
      </div>
    </v-row>
  </v-container>
</template>

以下からコードを分割して説明していきます。

v-container

グリッドシステムの全体定義
これから出てくるv-row(行定義)、v-col(列定義)の関係は以下の図の通りです。
Screenshot from 2020-02-11 09-24-44.png
fluidなしだと、中央に配置されますが、fluidありだと、それがなくなります。

v-row

グリッドシステムの行定義

レイアウトを定義する都合上、行数を計算します。
例を出すと、20個表示するアイテムがあり、1行10列の場合、2行になります。
20個表示するアイテムがあり、1行9列の場合、3行になります。
つまり、以下で行数が定義できます。

行定義
表示するアイテムの全体数 ÷ 1行に表示するアイテム = 行数(小数点以下があった場合は繰り上げ)

コードの該当箇所は以下

行定義
      rowCount:function(){
        return Math.ceil(this.attendances.length / this.colNumber);
      },
    },

v-col

グリッドシステムの列(カラム)定義

レイアウトを定義する都合上、アイテムのどこからどこまでを表示するのか計算します。
例を出すと、20個表示するアイテムがあり、1行10列の場合、
1列目は1個目から10個目まで表示すればよい
2列目は11個目から20個目まで表示すればよい

1行目は0から始まる(Array.prototype.slice([begin[, end]])のbeginは0 から始まるインデックス0)
2行目は列数から始まる 3行目は列数×2から始まる
これを数式にすると以下になる。

列定義(開始)
(行数-1) × 列数

列定義(終了)は単純に行数×列数で定義できます。
Array.prototype.slice([begin[, end]])にendが指定されたときは、slice は end 自体は含めず、その直前まで取り出すためです。

列定義(終了)
行数×列数

列定義(開始)と列定義(終了)をコードにすると

列定義
    methods:{
      itemCountInRow:function(row){
        return this.attendances.slice((row - 1) * this.colNumber, row * this.colNumber)
      }
    }
  }

ウィンドウサイズ毎の列数設定(Breakpointsを使用)

    computed: {
      colNumber: function() {
        let number;
        switch (this.$vuetify.breakpoint.name) {
          case 'xs': number = 2; break;
          case 'sm': number = 4; break;
          case 'md': number = 6; break;
          case 'lg': number = 8; break;
          case 'xl': number = 10; break;
        }
        return number;
      },

Breakpointsを利用して、ウィンドウサイズごとに1行に表示する列の数を決める。

Breakpointsを使用するとレスポンシブデザイン(PC、スマートフォンなど、異なるウインドウサイズでも表示できるようにするためのデザイン)が実現するのが楽になります。
5つのウインドウサイズが定義されており、またウインドウサイズの定義はカスタマイズもできます。

attendancesの定義

attendancesに表示対象のアイテム(配列)が定義されています。
今回の説明に関係するのはattendancesだけです。

<script>
  import firebase from 'firebase';

  export default {
    name: 'EventUser',
    data () {
      return {
        attendances: []
      }
    },

    firestore() {
      return {
        // firestoreのattendanceコレクションを参照
        attendances: firebase.firestore().collection('attendance').where("eventID", "==", Number(this.$route.params.eventID))

      }
    },

ソースコード全体

<template>
  <v-container fluid>
    <v-row v-for="(row, key) in rowCount" :key="key">
      <div v-for="(attendance, key2) in itemCountInRow(row)" :key="key2">
        <v-col>{{attendance.userID}}</v-col>
        <v-col><v-btn small color="primary">出席</v-btn><v-btn small color="error">欠席</v-btn></v-col>
        <v-divider/>
      </div>
    </v-row>
  </v-container>
</template>

<script>
  import firebase from 'firebase';

  export default {
    name: 'EventUser',

    data () {
      return {
        attendances: []
      }
    },

    firestore() {
      return {
        // firestoreのattendanceコレクションを参照
        attendances: firebase.firestore().collection('attendance').where("eventID", "==", Number(this.$route.params.eventID))

      }
    },

    computed: {
      colNumber: function() {
        let number;
        switch (this.$vuetify.breakpoint.name) {
          case 'xs': number = 2; break;
          case 'sm': number = 4; break;
          case 'md': number = 6; break;
          case 'lg': number = 8; break;
          case 'xl': number = 10; break;
        }
        return number;
      },

      rowCount:function(){
        return Math.ceil(this.attendances.length / this.colNumber);
      },
    },

    methods:{
      itemCountInRow:function(row){
        return this.attendances.slice((row - 1) * this.colNumber, row * this.colNumber)
      }
    }
  }

</script>

参考

https://jsfiddle.net/z11fe07p/421/
rowCountとitemCountInRowは上記を参考にしています。

感想

もっと簡単にできる方法があるのではないかと考えています。
なにかありましたらコメントをお願いいたします。

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

Vue.js勉強会に参加してみた

はじめに

WantedlyでVue.jsの勉強会を見つけたので、参加してきました。
Vue.jsは前々から勉強しようと思っていたのですが、後回しになっていたので、これを機会に本腰を入れよう!

環境構築

今回、事前に環境構築をしてきてとお願いされたのは以下のもの

  • Visual Studio Code
  • 拡張機能でVetur(Vue.js用の拡張機能)もインストール
  • githubアカウント
  • Node.js
    • versionは10以上
  • git
    • versionは2.20以上

このうち、(僕のパソコンはWindowsなこともあり)Node.jsとgitのインストールを行いました。

nodistのインストール

WindowsでNode.jsのバージョンを管理するには、nodistを使うらしい。
インターン先ではnodebrewを使っていたので、「あれ?」と思ったが、nodebrewは基本的にmac環境で使っているそうです。

以下のサイトの手順通りに進めました。
[Node.js] Node.js の導入(Windows編)

1. 公式ページにアクセス

2. インストーラーをダウンロード

実行時のversionは0.9.1

3. インストーラーに従ってインストール

ここでインストール終了時に「PATH not updated, original length 1183 > 1024」というエラーが。
取り敢えず、「OK」を押すと、何事もなかったかのように「Install Finished」と出る。

しかしコマンドプロンプトを開いて、$ nodist -vと打っても

'nodist'は、内部ファイルまたは~

というエラーが返ってくる。これはPATHが通ってないときに出てくるやつですね。
まぁ、さっきの時点でそんな気はしましたが、、、

4. nodistのPATHを通す

こちらの方の記事を参考にしたら、うまくいきました!
node.js インストール備忘録(windows7)

5. nodistのversion確認

$ nodist -v
0.9.1

無事インストールできました!

Node.jsのインストール

続いてNode.jsをインストールしていきます。

1. インストール可能なNode.jsのバージョンを確認

$ nodist dist

めちゃくちゃ出てきてビックリ(笑)。

2. 任意のバージョンをインストール

「偶数バージョンの方がいい」みたいなことを聞いたことがあったので、調べてみると以下のような記事が。
node.jsのバージョンごとの違い

今回は安全に使いたいので、偶数バージョンの最新版(12.14.1)をインストール。

$ nodist + v12.14.1

3. インストールされている Node.js を確認

$ nodist
  (x64)
> 12.14.1

4. Node.js のバージョンを切り替える

$ nodist v12.14.1

5. 現在使用可能なNode.jsのバージョンを確認

$ node -v
v12.14.1

6. npmのアップデート

npmはNode.jsのパッケージを管理するもの。

$ npm -v
$ npm install npm -g

以上でNode.jsのインストールは終了!

Gitのインストール

今までGitを使いたいときはSourseTreeを使っていたのですが、インターン先ではGitをコマンドで打つので、自分のパソコンにも入れたいと思いつつ、後回しにしていた。。。
今回やっとGitをインストール!

以下の記事を参考にしました。
Git インストール for Windows

特に問題はなく成功

$ git --version
git version 2.25.0.windows.1

Vueとは

  • オープンソース
  • WebアプリケーションのUIを構築
  • SPAを高速に構築することが可能
  • 学習コスト低い

Gitを使ってみる

ここは今回のメインではないので、箇条書きで進めます

  1. GitHubでリポジトリを作る
  2. リモートに適当なディレクトリを作る
  3. README.mdを作成し、# Hello, GitHub! と入力して保存
  4. git init をして、gitの初期化
  5. git remote add origin git@github.com:xxxx/yyyy.git
    • git remote -v でリモートを確認できる
  6. git add . をして、先ほど編集した差分をstage (commit待ちのファイル群) に乗せる
  7. git status で、stageに乗ったか確認 (緑色になってたらstaged)
  8. git commit -m "first commit" で、差分をコミット
  9. git push origin master で、作ったコミットをoriginリモートのmasterブランチ (デフォルトのブランチ) にpush

Vue.jsについて

Vue.jsはドキュメントがとても優秀、わからなくなったらここを読めばほとんど全て理解できる
- https://jp.vuejs.org/v2/guide/index.html
- そのドキュメントの日本語がよく分からず後回しにした人間がここに1人

Vueで書いてみよう

今回はscriptタグで、CDNからVueを読みます。
Todoリストを作成して、練習していきます。

sample.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Step1 ToDo Application</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>

</html>

Vueインスタンスの作成

ではbody要素を足していきます。
まずはVueのインスタンスを作ります。更に、elプロパティでappというidの要素を指定すると、それ以下をVueが管理してくれます。

sample.html
<body>
  <div id="app">
    <!-- ここをvueが管理 -->
  </div>
  <script>
    const app = new Vue({
      el: '#app'    // "id=app"を指定
    })
  </script>
</body>

データを持つ

動的に変化するデータを扱うには、data プロパティに宣言します。
そして{{}}内に囲むことで、そのデータを表示できます。

sample.html
<body>
  <div id="app">
    {{ hoge }}
  </div>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        hoge: "sample"
      }
    })
  </script>
</body>

ここでVueの特徴の一つreactive性が出てきます。
これは、変更されたdataが直ちに再描画されるというものです。

フォームデータの束縛(バインディング)

v-model="~"の形で記述された場合、~の変数にデータを格納します。

sample.html
<body>
  <div id="app">
    <label>ToDoの内容<input type="text" v-model="todoText"></label>
    {{ todoText }}
  </div>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        todoText: "sample"
      }
    })
  </script>
</body>

こうすると、inputタグのデータがtodoTextという変数と結びつくため、フォーム内に初めから"sample"という文字列が表示されます。

リストの表示

リストを表示するにはv-forを使います。

sample.html
<body>
  <div id="app">
    <label>ToDoの内容<input type="text" v-model="todoText"></label>
    <ul>
      <li v-for="todo in todoList">{{ todo }}</li>
    </ul>
  </div>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        todoText: "sample",
        todoList: ['掃除', '洗濯', '料理']
      }
    })
  </script>
</body>

算出プロパティ

テンプレートで表示するのに計算が必要な場合は算出プロパティcomputedを使用します。

sample.html
<body>
  <div id="app">
    <label>ToDoの内容<input type="text" v-model="todoText"></label>
    <p>あと{{ todoCount }}個のtodo</p>
    <ul>
      <li v-for="todo in todoList">{{ todo }}</li>
    </ul>
  </div>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        todoText: "sample",
        todoList: ['掃除', '洗濯', '料理']
      },
      computed: {
        todoCount() {
          return this.todoList.length
        }
      }
    })
  </script>
</body>

メソッド

DOMイベントに対して何かのアクションをするには、v-onディレクティブを使用します。
これにより、呼び出したいメソッドを呼ぶことができます。

sample.html
<body>
  <div id="app">
    <label>ToDoの内容<input type="text" v-model="todoText"></label>
    <button v-on:click="register">登録</button>
    <p>あと{{ todoCount }}個のtodo</p>
    <ul>
      <li v-for="todo in todoList">{{ todo }}</li>
    </ul>
  </div>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        todoText: "",
        todoList: []
      },
      computed: {
        todoCount() {
          return this.todoList.length
        }
      },
      methods: {
        register: function() {
          this.todoList.push(this.todoText);
          this.todoText = "";
        }
      }
    })
  </script>
</body>

より本格的な開発のために

コンポーネントシステム

コンポーネントとは、ざっくり言うと「部品」のこと。
ページをコンポーネント化して、各部品として管理することで、再利用性や保守性が高まる。

Vueでは.vueファイルとして保存します。
更にVueの特徴は、.vueファイル一枚にtemplate,style,scriptを書けることです。(単一ファイルコンポーネント)
このおかげで、ファイル管理が楽になります。

Vuex

状態管理のためのライブラリ(+パターン)。
共通の状態やデータを複数のコンポーネントで管理する場合に活用できる。

参考:ざっくり理解、Vuexって何?VuexとAPIの関係を図解してみた

Nuxt.jsについて

Nuxt.jsとは

Nuxtは、モダンな web アプリケーションを作成する Vue.js に基づいたプログレッシブフレームワークです。Vue.js 公式ライブラリ(vue、vue-router や vuex)および強力な開発ツール(webpack、Babel や PostCSS)に基づいています。 Nuxt の目標は、優れた開発者エクスペリエンスを念頭に置き、Web 開発を強力かつ高性能にすることです。

先ほど説明したものや、cssフレームワーク、SSRなどもろもろのベストプラクティスを適用したプロジェクトを一瞬で構築可能。
本格的なWebアプリケーション作成ができる。

Nuxt.jsをセットアップする

npxのインストール

npxを使うと、ローカルにインストールしたnpmパッケージを、npxコマンドだけで実行できるようになります。
これにより、面倒なコマンドを打たなくて済みます。
言わば"便利コマンド集"のようなもの。

普通はnpmが入れば自動的にインストールされる。
しかしnodist経由だと、npxが自動でインストールされないらしい...

そこで個別にインストールします。

$ npm install -g npx

プロジェクト作成

npm create-nuxt-app <プロジェクト名>でNuxt.jsのプロジェクトを作成可能です。
ただし、プロジェクトを作る場所はカレントディレクトリではなく、キャッシュに残っている場所になってしまいます。

しかもその場所のパスにスペースが入っていると、

operation not permitted, mkdir '~'

のエラーが出てプロジェクトを作れない。。。

そこで、エラーが出る場合は以下のコマンドでキャッシュの場所を変えます。

$ npm config set cache <プロジェクトを作りたいディレクトリのパス> --global

その上で改めて

$ npm create-nuxt-app <プロジェクト名>

を実行します。

プロジェクト作成中、UIフレームワークなど、設定を聞かれるので、適宜選択してください。

プロジェクトのディレクトリ構成

  • layouts
    • 全ページで使用されるテンプレートファイルを規定する。ページの外観を変更するために使用される(例えばサイドバーなど)。
    • page の layout プロパティで変更可能
  • pages
    • アプリケーションのビュー及びルーティングファイルを入れる。
    • Nuxt.js フレームワークはこのディレクトリ内のすべての .vue ファイルを読み込み、Vue Routerによって自動的にルーティングされる
      • pages/index.vue/
      • pages/hoge/fuga.vue/hoge/fuga
  • components
    • pages から利用するコンポーネントを入れておく
    • components から components も使ったりする
  • stores
    • Vuex ストアのファイルを入れる
    • デフォルトではVuexストアは無効。このディレクトリに index.js ファイルを作成するとストアが有効になる。

Nuxt.jsでカレンダーアプリを作ってみる

サンプルとしてカレンダーアプリ(と言えるかすら微妙なもの)を作ってみました。

index.vueの作成

まずはindex.vueです。pagesディレクトリ内に作ります。
コンポーネントとして作ったCalender.vueをscript内で読み込みます。
これにより、Calender.vueの内容を、<calender />タグの部分にはめ込むことができます。

index.vue
<template>
  <div class="container">
    <calendar />
  </div>
</template>

<script>
import Calendar from '~/components/Calendar'

export default {
  components: {
    Calendar,
  }
}
</script>

<style></style>

コンポーネントの作成(Calendar.vueとCalendarDay.vue)

Calendar.vueにカレンダーの全体像、CalendarDay.vueにカレンダーの一日の部分を入れます。

Calendar.vue
<template>
  <div class="calendar">
    <calendar-day v-for="day in dayList" :key="day" :day="day" />
  </div>
</template>

<script>
import CalendarDay from '~/components/CalendarDay'
export default {
  components: {
    CalendarDay,
  },
  data() {
    return {
      dayList: []
    }
  },
  created() {
    for (let i = 1; i <= 31; i++) {
      this.dayList.push(i)
    }
  }
}
</script>

<style scoped>
.calendar {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
}
</style>
CalendarDay.vue
<template>
  <div class="calendar-day">
    {{ day }}
    <input type="text" v-model="text">
    <button @click="addSchedule({ day, text })">追加</button>
    <ul>
      <li v-for="schedule in scheduleList" :key="schedule.id">{{ schedule.text }}</li>
    </ul>
  </div>
</template>

<style scoped>
.calendar-day {
  border: solid 1px black;
  width: 150px;
  height: 150px;
}
</style>

<script>
import { mapGetters, mapMutations } from 'vuex'
export default {
  data() {
    return {
      text: '',
    }
  },
  props: {
    day: 0,
  },
  computed: {
    ...mapGetters({
      getScheduleListByDay: 'schedule/getScheduleListByDay',
    }),
    scheduleList() {
      return this.getScheduleListByDay(this.day)
    }
  },
  methods: {
    ...mapMutations({
      addSchedule: 'schedule/addSchedule'
    }),
  }
}
</script>

storeの作成(schedule.js)

storeディレクトリにschedule.jsを作成し、予定データを保存・加工します。

schedule.js
export const state = () => ({
    scheduleList: [], // Array<{ id: Number, day: Number, text: String }>
    lastScheduleId: 0,
})

export const getters = {
    getScheduleListByDay: (state) => (day) => {
        return state.scheduleList.filter(schedule => schedule.day === day)
    },
}

export const mutations = {
    addSchedule (state, { day, text }) {
        state.scheduleList.push({
        id: ++state.lastScheduleId,
        day,
        text,
        })
    },
}

おわりに

Vueを使うと、コンポーネントとしての管理がしやすいなぁと思いました。

ただ、アロー関数などJSの記法をしっかり覚えないと...

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

Vue CLI 4 でStorybookを使う

今更ですが、Storybookに手を出していこうと思い、環境構築から順にメモとして残していきます。

始めに

Storybookとは?

スクリーンショット 2020-02-11 0.22.06.png

  • 公式サイト:https://storybook.js.org/
  • UIコンポーネントカタログが作成できるツール
  • デザイナとプログラマの認識合わせに使える
  • Docなども記載できるため、チーム開発での意思疎通にも使える

Storybookの環境構築

環境

Console
% node -v
v12.13.0
% npm -v
6.13.6

プロジェクトの作成

Vue CLIを使ってお好みの設定でプロジェクトを作成します。

Console
% npx @vue/cli create vue-storybook-demo 

Vue CLI v4.2.2
? Please pick a preset: Manually select features
? Check the features needed for your project: TS, Linter
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? No
? Pick a linter / formatter config: Prettier
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint o
n save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

開発サーバの起動

Console
% cd vue-storybook-demo 
% npm run serve

ブラウザでhttp://localhost:8080/へアクセスし表示確認を行う。

Storybookプラグインの追加

Console
% npx @vue/cli add storybook

? What do you want to generate? Initial framework
? What storybook version do you want? (Please specify semver range) >=5.3.0
? Use Storybook CSF (component story format)? Yes
? Use Storybook Docs? Yes

自動生成されたサンプルの修正

今回のVueCLIの設定だと、自動生成されるサンプルでエラーが発生していたので、修正します。

src/components/MyButton.vue
 <script lang="ts">
- export default {
-   name: "my-button",
- 
-   methods: {
-     onClick() {
-       this.$emit("click");
-     }
-   }
- };
+ import { Component, Vue } from "vue-property-decorator";
+ 
+ @Component
+ export default class MyButton extends Vue {
+   public onClick(){
+     this.$emit("click");
+   }
+ }
 </script>

Storybookサーバの起動

Console
% npm run storybook:serve

ブラウザでhttp://localhost:6006/?path=/story/button--with-textへアクセスし表示確認を行います。
(というか勝手にブラウザが開く。)

localhost_6006__path=_story_button--with-text.png
localhost_6006__path=_story_button--with-text (1).png

Storyの追加

オリジナルのコンポーネントの作成

以下のファイルを新規作成します。

src/components/MyHeading.vue
<template>
  <h1>
    <slot></slot>
  </h1>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

@Component
export default class MyHeading extends Vue {}
</script>

Storyの追加

以下のファイルを新規作成します。

src/stories/MyHeading.stories.js
import MyHeading from "../components/MyHeading.vue";

export default {
  component: MyHeading,
  title: "Heading"
};

export const withText = () => ({
  components: { MyHeading },
  template: '<my-heading>Hello Title</my-heading>'
});
src/stories/MyHeadding.stories.mdx
import { Meta, Props, Story, Preview } from '@storybook/addon-docs/blocks';
import MyHeading from '../components/MyHeading.vue';

<Meta title="MDX|Heading" component={MyHeading} />

# MyHeading

<Props of={MyHeading} />

This is a simple button with some text in it.

<Preview>
  <Story name="With Text">
    {{
        components: { MyHeading },
        template: '<my-heading>Hello Title</my-heading>'
    }}
  </Story>
</Preview>

ブラウザで表示確認

localhost_6006__path=_story_mdx-heading--with-text.png

以上で環境構築は完了です。
ここから自作のコンポーネントを追加したり、お好みのStorybookプラグインを追加してプロジェクト毎のコンポーネントカタログを作成していきます。

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