コンポーネント単体テストについてつらつらと

書きます。

何をテストするのか

f:id:ryotah:20181203134621p:plain

コンポーネント単体テストで何をテストするのか。

基本的に、考え方は関数のテストと同じだと思います。何かが入力されると結果が出力される。テストでは、その結果が正しいかを確認します。「入力」と「出力」にフォーカスすることが大切です。

一般的なヒント | Vue Test Utils では以下のように説明しています。

コンポーネントのパブリックインターフェイスを検証するテストを作成し、内部をブラックボックスとして扱うことをお勧めします。単一のテストケースでは、コンポーネントに提供された入力(ユーザーのやり取りやプロパティの変更)によって、期待される出力(結果の描画またはカスタムイベントの出力)が行われることが示されます。

このように考えれば、テストの方針も立てやすくなります。結果、何をテストしているのか理解しやすくなりますし、コードもシンプルになります。内部実装をリファクタしたとしても、テストが壊れることはないです。

なぜ書くのか

なぜテストをするのですか | Vue Test Utils から引用。

単体テストの利点:

  • コンポーネントがどう動作すべきかのドキュメントを提供します
  • 過度な手動テストの時間を節約します
  • 新しい機能におけるバグを減らします
  • 設計を改良します
  • リファクタリングを容易にします

(ここに「フレームワークへの理解が深まる」も追加できると思います。)

単体テストのすべきこと:

  • 実行が早いこと
  • 理解しやすいこと
  • 一つの作業だけをテストすること

テストが書きにくいな思ったら、もしかしたら、コンポーネントの設計に問題があるかもしれません。例えば、あるコンポーネントに複数の役割を持たせていたり、必要以上に大きく依存関係が複雑になってたり。

テストを書くことで既存コードに改良の余地があることに気づく、そういったケースもあるかと思います。

入力と出力とは何か

f:id:ryotah:20181203134642p:plain

具体的に「入力」と「出力」にはどのようなものがあるのでしょうか。一般的な SPA の Web アプリケーションを例にして考えてみます。

入力:

  • プロパティ(親コンポーネントから渡される)
    • Vue なら props, Angular なら @Input に相当するもの
  • DOM
    • ユーザーによる UI 操作により発生
    • クリックアクションやフォームの入力など
  • Route
    • URL が変更することにより発生
    • Vue なら this.$route, Angular なら ActivatedRoute
  • Store
    • Flux, Redux のような状態管理を導入している場合、Store の変更もコンポーネントへの入力となる
    • Vue (Vuex) なら this.$store.state (あるいは this.$store.getters), Angular (ngrx) なら this.store.pipe(select(...) など

出力:

  • イベント(親コンポーネントへ渡す)
    • Vue なら emit, Angular なら @Output に相当するもの
  • DOM
    • 表示されたテキスト, 要素の On/Off, スタイル, 属性 など
  • Route
    • Router へのアクション
  • Store
    • Vue (Vuex) なら this.$store.{dispatch|commit}, Angular (ngrx) なら this.store.dispatch など

これ以外にもあるとは思いますが、大体このようなものではないでしょうか。

テストを書く前に調べること

初めてさわるフレームワークコンポーネント単体テストを書く場合、上にあげたような「入力」をシュミレートする方法と「出力」を検証する方法を知っておくと、順調にテストが書けると思います。

あとは非同期の扱いや、テストダブル(スタブ・モック・スパイなど)の扱いについて知っておくといいかと。(これらに関しては、利用するテストフレームワーク側の機能を利用することも多いです。)

一般的なコンポーネントのテストシナリオ

Angular ドキュメントには Component Test Scenarios というセクションがあり、一般的なテストシナリオの実装方法について解説しています。「一般的なテストシナリオにはどんなものがあるのか」について知るという点で、Angular ユーザー以外にも参考になると思います。

以下は抜粋です。

Vue 単体テストのサンプル

テストシナリオを踏まえて、Vue の単体テストで必要そうなケースをいくつかピックアップしたのがこちらです。(若干中途半端ではありますが)

propsemit のテスト

describe("Button", () => {
  it(`should emit a 'click' event`, () => {
    const wrapper = shallowMount(Button, {
      propsData: {
        text: "text"
      }
    });
    wrapper.find("button").trigger("click");
    expect(wrapper.emitted().click).toBeTruthy();
  });
});

NuxtLink (RouterLink) をスタブします。

describe("Breadcrumbs", () => {
  it("should have nuxt-link correspond to props", () => {
    const breadcrumbs = [{ label: "Foo", path: "foo" }];
    const wrapper = shallowMount(Breadcrumbs, {
      propsData: {
        breadcrumbs
      },
      stubs: {
        NuxtLink: RouterLinkStub
      }
    });
    expect(wrapper.find(RouterLinkStub).props().to).toBe(breadcrumbs[0].path);
  });
});

Router を利用しているコンポーネントのテスト

createLocalVue を利用してグローバルの Vue クラスを汚染しないローカルの Vue クラスで VueRouter を利用します。

import { createLocalVue, shallowMount } from "@vue/test-utils";
import VueRouter from "vue-router";

describe("Menu", () => {
  it(`should have '-current' class if the path is same as the current route`, () => {
    const localVue = createLocalVue();
    localVue.use(VueRouter);
    const router = new VueRouter();
    router.push("/foo");
    const wrapper = shallowMount(Menu, {
      localVue,
      router,
      propsData: {
        menus: [
          { label: "foo", value: "/foo" },
          { label: "bar", value: "/bar" }
        ]
      },
      stubs: {
        NuxtLink: RouterLinkStub
      }
    });
    const fooLink = wrapper.findAll(RouterLinkStub).at(0);
    const barLink = wrapper.findAll(RouterLinkStub).at(1);
    expect(fooLink.classes()).toContain("-current");
    expect(barLink.classes()).not.toContain("-current");
  });
});

内部で他のコンポーネントを利用しているケース

shallowMount() を利用すると、子コンポーネントはスタブされたコンポーネントになります。

メモ:

Nuxt のページコンポーネントのテスト

Nuxt の機能と、テスト対象のコンポーネントの機能をわけて考えることが大切です。

以下のテストでは「fetch 時に適切なアクションがディスパッチされているか」と「適切な middleware が登録されているか」についてだけテストをしています。初回アクセス時に fetch が呼ばれているか、パラメータ変更時にも fetch が呼ばれるか、許可されたユーザーのみアクセスできるか、などのテストはしていません。(それは Nuxt のテストや登録されているミドルウェア自体のテストになるから)

describe("FooPage", () => {
  it("dispatches FetchFoo before rendering the page", () => {
    const wrapper = shallowMount(FooPage);
    const store = {
      dispatch: jest.fn()
    };
    const fooId = "asdf";
    const route = fooRoute(fooId);
    const context: any = {
      store,
      route
    };
    // simulate the nuxt fetch hook
    wrapper.vm.$options.fetch(context);
    expect(context.store.dispatch).toHaveBeenCalledWith(FetchFoo({ fooId }));
  });
  it(`needs 'requireAuth' middleware`, () => {
    const wrapper = shallowMount(FooPage);
    expect(wrapper.vm.$options.middleware).toContain("requireAuth");
  });
});
const fooRoute = fooId => ({ params: { fooId } });

ミューテーション単体のテスト

// from https://vuex.vuejs.org/ja/guide/testing.html
describe("mutations", () => {
  it("INCREMENT", () => {
    // ステートのモック
    const state = { count: 0 };
    // ミューテーションを適用する
    increment(state);
    // 結果を検証する
    expect(state.count).to.equal(1);
  });
});

アクション単体のテスト

アクションは外部の API を呼び出す場合が多いです。そのため、モック関数を作成したり、非同期処理をうまく扱う必要があります。

import { FetchFoo, ReceiveFoo, ResetShopStaffs } from "./types";
import { actions } from "../actions";

describe("actions", () => {
  let store;
  beforeEach(() => {
    // inject plugins
    store = {
      $api: {
        request: jest.fn()
      }
    };
  });
  it("FetchFoo request Foo and then commit ReceiveFoo", async () => {
    // fake data
    const fakeRequest = "request";
    const fakeResponse = "response";

    // mock functions
    const commit = jest.fn();
    store.$api.request.mockReturnValue(fakeResponse);

    // dispach
    const action = FetchFoo(fakeRequest);
    actions[action.type].bind(store)({ commit }, action);

    // request
    expect(store.$api.request).toHaveBeenCalledWith(fakeRequest);

    await resolvePromises();

    expect(commit).toHaveBeenCalledWith(ReceiveFoo(fakeResponse));
  });
});

export const resolvePromises = () =>
  new Promise(resolve => setImmediate(resolve));

ストアのテスト(アクション、ミューテーション など全てを含む)

// from https://vue-test-utils.vuejs.org/ja/guides/using-with-vuex.html
import { createLocalVue } from "@vue/test-utils";
import Vuex from "vuex";
import storeConfig from "./store-config";
import { cloneDeep } from "lodash";

test("increments count value when increment is commited", () => {
  const localVue = createLocalVue();
  localVue.use(Vuex);
  const store = new Vuex.Store(cloneDeep(storeConfig));
  expect(store.state.count).toBe(0);
  store.commit("increment");
  expect(store.state.count).toBe(1);
});

test("updates evenOrOdd getter when increment is commited", () => {
  const localVue = createLocalVue();
  localVue.use(Vuex);
  const store = new Vuex.Store(cloneDeep(storeConfig));
  expect(store.getters.evenOrOdd).toBe("even");
  store.commit("increment");
  expect(store.getters.evenOrOdd).toBe("odd");
});

補足

グローバルプロパティをスタブする:

const $route = { path: "http://www.example-path.com" };
const wrapper = shallowMount(Foo, {
  mocks: {
    $route
  }
});

https://vue-test-utils.vuejs.org/ja/api/options.html#mocks

まとまらない何か

テストに関係する Tips とかメモとか。

flush-promises (setImmediate) を利用して非同期を解決させる

Vue でグローバル登録されてるフィルタが存在する場合

import Vue from "vue";
import * as filters from "./util/filters";

// We would extract this to a function that would be reused by both app.js and jest-setup but,
// we didn't want to change original production code
Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key]);
});

How do we test these globally registered filters? https://github.com/agualis/vue-hackernews-2.0/blob/master/src/jest-setup.js

同年、同月、同日にテストすることを願わん

momentjs - How do I set a mock date in Jest? - Stack Overflow

Date.now = jest.fn(() => 1487076708000); //14.02.2017`

Jest のカスタムマッチャ

const myMockFn = jest.fn()...;

// The mock function was called at least once
expect(mockFunc).toBeCalled();

上記のようなカスタムマッチャは .mock プロパティを検査する方法の糖衣構文

https://jestjs.io/docs/en/mock-functions

jest.spyOn(object, methodName)

  • jest.fn と同様の関数を作成する
  • 引数に与えられた object[methodName] へのコールも実装

https://jestjs.io/docs/ja/jest-object#jestspyonobject-methodname

参考 URLs

Vue SSR 環境構築メモ

Vue SSR ガイド, vuejs/vue-hackernews-2.0 を参考に環境構築を試してみた時のメモです。

サーバサイドで Vue をコンパイルする

https://github.com/ryotah/vue-ssr/compare/step-0...step-1-render-to-string

やっていること:

  • vue-server-renderer からレンダラーを作成
  • リクエスト毎に vue インスタンスを生成
  • HTML に描画して返信

コード抜粋:

server.js

const Vue = require("vue");
const server = require("express")();
// ステップ 1: レンダラを作成
const renderer = require("vue-server-renderer").createRenderer({
  template: require("fs").readFileSync("./index.template.html", "utf-8")
});

server.get("*", (req, res) => {
  // ステップ 2: Vue インスタンスを作成
  const app = new Vue({
    data: {
      url: req.url
    },
    template: `<div>The visited URL is: {{ url }}</div>`
  });

  // ステップ 3: Vue インスタンスを HTML に描画
  renderer
    .renderToString(app)
    .then(html => res.end(html))
    .catch(err => {
      res.status(500).end("Internal Server Error");
    });
});

server.listen(8080);

参考:

クライアントとサーバのコードをわける (Webpack)

https://github.com/ryotah/vue-ssr/compare/step-1-render-to-string...step-2-webpack

やっていること:

  • クライアントとサーバー両方をバンドルするために webpack を導入
  • entry-client.js, entry-server.js, app.js を用意
    • entry-*.jsapp.js (アプリケーションのユニバーサルエントリー) を読み込む

コード抜粋:

entry-client.js

import { createApp } from "./app";

export default context => {
  const { app } = createApp(context);
  return app;
};

server.js

// ...
const createApp = require("./dist/entry").default;
server.get("*", (req, res) => {
  // ステップ 2: Vue インスタンスを作成
  const app = createApp({ url: req.url });

  // ステップ 3: Vue インスタンスを HTML に描画
  // ...
});

server.listen(8080);

webpack.config.js

module.exports = {
  entry: "./entry-server.js",
  output: {
    filename: "entry.js",
    path: path.resolve(__dirname, "dist"),
    libraryTarget: "commonjs2"
  }
  // ...
};

参考:

f:id:ryotah:20181127023444p:plain

ルーティング

https://github.com/ryotah/vue-ssr/compare/step-2-webpack...step-3-router

やっていること:

  • vue-router を導入
  • entry-server.js にルーティングロジックを実装

コード抜粋:

entry-server.js

import { createApp } from "./app";

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router } = createApp();

    // set server-side router's location
    router.push(context.url);

    // wait until router has resolved possible async components and hooks
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();
      if (!matchedComponents.length) {
        reject({ code: 404 });
      }
      resolve(app);
    }, reject);
  });
};

参考:

プリフェッチを実現

https://github.com/ryotah/vue-ssr/compare/step-3-router...step-4-store

やっていること:

  • サーバサイドとクライアントサイドで同じデータを利用可能にするために Vuex を導入
  • ルートコンポーネントにプリフェッチ用メソッド asyncData を実装
  • entry-client.js にサーバからのデータを受け取る store.replaceState(window.__INITIAL_STATE__) を追加

コード抜粋:

entry-server.js

// ...
router.onReady(() => {
  const matchedComponents = router.getMatchedComponents();
  // ...

  // call `asyncData()` on all matched route components
  Promise.all(
    matchedComponents.map(
      ({ asyncData }) =>
        asyncData &&
        asyncData({
          store,
          route: router.currentRoute
        })
    )
  )
    .then(() => {
      // すべてのプリフェッチのフックが解決されると、ストアには、
      // アプリケーションを描画するために必要とされる状態が入っています。
      // 状態を context に付随させ、`template` オプションがレンダラに利用されると、
      // 状態は自動的にシリアライズされ、HTML 内に `window.__INITIAL_STATE__` として埋め込まれます
      context.state = store.state;
      resolve(app);
    })
    .catch(reject);
}, reject);
// ...

参考:

サーバとクライアント、それぞれをビルドするために個別の Webpack 設定ファイルを用意

https://github.com/ryotah/vue-ssr/compare/step-4-store...step-5-hydration

バンドルレンダラ

https://github.com/ryotah/vue-ssr/compare/step-5-hydration...step-6-bundle-renderer

やっていること:

  • バンドルレンダラを利用
  • JSON 形式でサーバサイドのコードをバンドル

参考:

クライアントマニフェストを生成

https://github.com/ryotah/vue-ssr/compare/step-6-bundle-renderer...step-7-client-manifest

参考:

ビルド設定をもうちょい

https://github.com/ryotah/vue-ssr/compare/step-7-client-manifest...step-8-build

やっていること:

  • webpack-dev-middleware を導入
  • コードに変更があったら、サーバとクライアント両方をビルドするように設定

コード抜粋:

server.js

const path = require('path');
const express = require('express');
const MemoryFS = require('memory-fs');
const app = express();
const webpack = require('webpack');

let clientManifest;
let serverBundle;
let renderer;

function update() {
  if (!clientManifest || !serverBundle) {
    return;
  }
  // create a renderer
  const { createBundleRenderer } = require('vue-server-renderer');
  const template = require('fs').readFileSync('./index.template.html', 'utf-8');
  renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false, // recommended
    template,
    clientManifest,
  });
}

// dev middleware
const clientConfig = require('./build/webpack.client.config.js');
const clientCompiler = webpack({
  ...clientConfig,
  // TODO:
  mode: 'development',
});
const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
  publicPath: clientConfig.output.publicPath,
});
app.use(devMiddleware);
clientCompiler.plugin('done', stats => {
  try {
    clientManifest = JSON.parse(
      devMiddleware.fileSystem.readFileSync(
        path.join(clientConfig.output.path, 'vue-ssr-client-manifest.json'),
        'utf-8'
      )
    );
  } catch (e) {
    console.log(e);
  }
  update();
});

// watch and update server renderer
const serverConfig = require('./build/webpack.server.config.js');
const serverCompiler = webpack({
  ...serverConfig,
  // TODO:
  mode: 'development',
});
const mfs = new MemoryFS();
serverCompiler.outputFileSystem = mfs;
serverCompiler.watch({}, (err, stats) => {
  try {
    serverBundle = JSON.parse(
      mfs.readFileSync(
        path.join(clientConfig.output.path, 'vue-ssr-server-bundle.json'),
        'utf-8'
      )
    );
  } catch (e) {
    console.log(e);
  }
  update();
});

// ...

10月メモ・リンク集

10月に調べたことのメモです。

Vue, Nuxt 関連

Migrate to Nuxt 2.0

参考:

Nuxt の options.ignore を利用して pages 内にコンテナやストアを配置

// nuxt.config.js
ignore: [
  // ...
  '**/pages/**/{containers,components}/*'
],

メリット:

  • 関係のあるコードを近くに配置する
  • pages, containers, store の関係をはっきりさせる
    • a page has a store and container(s)

変更前のディレクトリ構成:

.
├── containers
│    └── foo
│        ├── index.vue (<= コンテナコンポーネント)
│        ├── components
│        ├── store
│        └── utils
└── pages
    └── foo
        └── index.vue (<= ページコンポーネント)

変更後のディレクトリ構成:

.
└── pages
    └── foo
        ├── index.vue (<= ページコンポーネント)
        ├── components
        ├── containers
        │    └── index.vue (<= コンテナコンポーネント)
        ├── store
        └── utils

https://nuxtjs.org/api/configuration-ignore#the-ignore-property

ページ遷移時に権限確認

// middleware
// ...
// Get authorizations for matched routes (with children routes too)
const authorizationLevels = route.meta.map((meta) => {
  if (meta.auth && typeof meta.auth.authority !== 'undefined')
    return meta.auth.authority
  return 0
})
// Get highest authorization level
const highestAuthority = Math.max.apply(null, authorizationLevels)
// ...

不要なビルド処理を削減

  • API: build プロパティ - Nuxt.js
    • extend メソッドは一度はサーバーのバンドルのため、一度はクライアントのバンドルのため、つまり二度呼び出されます。

webpack.config.js (実装例):

module.exports = (config, options) => {
  // Add plugins
  config.plugins.push(
    // ...
  );
  // Do not run type checking twice. (This config is called twice,
  // one time for the server bundle, and one time for the client bundle.)
  if (options.isServer) {
    config.plugins.push(new ForkTSCheckerPlugin({
      vue: true
    }));
  }
  // ...
  return config;
};

文字コード正規表現

Unicode Property Escapes

補足:

文字コードの変換

['𩸽'.charCodeAt(0), '𩸽'.charCodeAt(1)].map(num => num.toString(16));
// => ["d867", "de3d"]

パスワード向け正規表現

例: 数字・アルファベット・記号を許容する

サンプル:

// 数字・小文字・大文字・記号を最低1文字含む, 8文字以上64文字以下
/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!-\/:-@[-`{-~])[!-~]{8,64}$/.test(value);

その他参考:

SVG 関連

SVG をアイコンフォントの代替にする

参考記事:

実装例 (Font Awesome):

<svg>
  <use xlink:href="fa-brands.svg#facebook"></use>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Font Awesome Free 5.5.0 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
-->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  // ...
  <symbol id="facebook" viewBox="0 0 448 512">
    <title id="facebook-title">Facebook</title>
    <path d="..."></path>
  </symbol>
  // ...
</svg>

https://fontawesome.com/how-to-use/on-the-web/advanced/svg-sprites

書き出し (WIP):

  • svgo で不要タグなどを整理. 色情報消去(fill を空にする)
    • svgo *.svg --disable=removeTitle --config='{ "plugins": [ { "removeAttrs": { "attrs": "fill" } } ], "floatPrecision": xx }'
  • <symbol id="xxx" viewBox="0 0 xx xx"> を追加 (単体ファイルにする? or SVG Sprite にする?)
    • svgo で対応する or 別ツールを利用する

アウトプット例:

<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><title>title</title><symbol id="root" viewBox="0 0 24 24"><path d="..." fill-rule="evenodd"/></symbol></svg>

関連情報

SVG 基礎知識:

ツール・書き出し:

その他:

JIRA 芸

JIRA 使い始めました。

Form Events

書いた記事

その他

VeeValidate メモ

Vue のバリデーションライブラリ VeeValidate の使い方をまとめたもの。(VeeValidate 2.1.0-beta.11, Vue 2.5.17)

基本的な使い方

<input v-validate="'required|email'" type="email" name="email">
<span>{{ errors.first('email') }}</span>

バリデーションルールのシンタックス

<input v-validate="'required|min:6'" type="password" name="password">
<input v-validate="{ required: true, email: true }" type="email" name="email">

カスタムルール

// Function Form
Validator.extend('custom', (value, args) => {
  // Return a Boolean or a Promise that resolves to a boolean.
});

// Object Form
Validator.extend('custom', {
  getMessage(field, args) {
    // will be added to default locale messages.
    // Returns a message.
  },
  validate(value, args) {
    // Returns a Boolean or a Promise that resolves to a boolean.
  }
});
  • getMessage の引数 field はフィールド名 (String)
  • getMessage を利用すると現在のロケールの辞書に追加される。多言語対応をする場合は Localization API を利用する。

さらに詳しく

  • Custom Rules | VeeValidate
    • 引数を必要とするルール・他の入力フィールドと比較をするルール・Non-immediate なルール(immediate 修飾子が設定されていない場合、初期バリデーションをスキップする)・data プロパティについて、など

エラーメッセージ

エラーメッセージを変更/追加する

Validator.localize を利用して辞書 (rootDictionary) を登録する。

import { Validator } from 'vee-validate';

const dictionary = {
  en: {
    messages:{
      custom: () => 'Some English Message',
    }
  }
};
// Override and merge the dictionaries
Validator.localize(dictionary);

// メモ: メッセージを生成する関数の形式
// function rule (fieldName: string, params: any[], data?: any): string {
//   return `Some error message for the ${fieldName} field.`;
// }

Configuration | VeeValidate を利用して登録することも可。

入力フィールドの名前

エラーメッセージと同様に辞書を更新する。

const dictionary = {
  en: {
    attributes: {
      email: 'Email Address'
    }
  },
};

さらに詳しく

カスタムコンポーネントで使う

DEMO:

スコープ

f:id:ryotah:20181025225325p:plain

  • VeeValidate のバリデータスコープはコンポーネント毎につくられる
  • コンポーネントinject: ['$validator'] をすることで親のスコープを取得できる
  • メモリ消費量を抑えるため、バリデータスコープの自動生成をさせないことも可能方法
    • 設定変更に関しては Configuration | VeeValidate に詳細がある (inject: false)
    • (自動生成されなくなるので)自身のコンポーネントにバリデータスコープを生成したい場合は以下のように $_veeValidate を利用する
$_veeValidate: {
  validator: 'new'
}
  • 同じコンポーネント内(バリデータスコープ内)でフィールド名がコンフリクトする場合 data-vv-scope を利用してスコープを作成することができる(コンポーネント毎に生成されるバリデータスコープとは別物)
    • form タグに data-vv-scope を設定すると自動で入力フィールドにも同じスコープが設定される

DEMO:

参考:

  • Component Injections | VeeValidate
    • With SSR Frameworks like Nuxt, it is recommended to disable automatic injection since it may cause memory leaks due to all the validator instances being created for every component, which is not needed and may slow down your site.
  • Scopes | VeeValidate

VueI18n と一緒に使う

設定:

// ...
import en from '~/locales/en/validation';

Vue.use(VueI18n);

const i18n = new VueI18n();

Vue.use(VeeValidate, {
  // ...
  // i18nRootKey: 'validation', 'validation' is default
  i18n,
  dictionary: {
  en
  }
});

利用:

// 言語を変更する場合は `$i18n` から
this.$i18n.locale = 'ar';

言語ファイル:

import en from 'vee-validate/dist/locale/en';

// 最終的に VueI18n の言語データとして書き出される
export default {

  // VeeValidate の辞書形式
  attributes: {
    custom: 'Custom',
    // ...
  },
  messages: { 
    ...en.messages,
    custom: () => 'Some English Message',
  },
};

参考:

その他

Validator#validate と Validator#validateAll

validate に関しては Validator API | VeeValidate にまとまっているが validateAll に関しては情報が少ない。実際には、引数の形式は違うが機能としては同じものがいくつかある。validator.validateAll() より validator.validate() の方が実質の All だったりする。

// validate all fields.
validator.validate();

// validate a field that has a matching name with the provided selector.
validator.validate('field');

// validate a field within a scope.
validator.validate('scope.field');

// validate all fields within this scope.
// => 内部で validator.validateAll('scope') を呼ぶ
validator.validate('scope.*');

// validate all fields without a scope.
// => 内部で validator.validateAll() を呼ぶ
validator.validate('*');

DEMO:

Flags

DEMO:

export class Field {
  id: string;
  name: string;
  scope: string;
  flags: FieldFlags;
  isRequired: boolean;
  initial: boolean;
  el: any;
  value: any;
  rules: any;
  update(options:object): void;
}

export interface FieldFlags {
  untouched: boolean;
  touched: boolean;
  dirty: boolean;
  pristine: boolean;
  valid?: boolean;
  invalid?: boolean;
  validated: boolean;
  required: boolean;
  pending: boolean;
}

Validation Events

  • vee-validate はデフォルトで input イベントをリスニングしている
  • リスニング対象を変更したい場合、デフォルトの挙動を変更するか、フィールド毎に変更するか、2通りの方法がある
    • data-vv-validate-on

参考:

入力フィールドが動的に表示/生成される場合

Vue が要素を再利用しないように、ユニークな key を設定する必要がある。

参考:

Misc.

型定義ファイル

9月メモ・リンク集

9月に調べたことのメモです。

Vue 関連

ルーティング関連

i18n

言語データの整理方法メモ

{ 
  "common": { // 一般的な用語
     "action": {
       "close": "閉じる"
     },
     "label": {
       "calendar": "カレンダー"
     },
     // ...
  },
  "glossary": { // アプリケーション用語, Domain
    "task": {
      "label": {
        // ...
      },
    },
  },
  "components": { // Shared Components に対応
    "calendar": {
      // ...
    },
  },
  "modules": { // 各ルートに対応
    "todo": {
      // ...
    },
    "todos": {
      // ...
    }  
  },
}
  • Angular アプリでの例
  • 意味のグループと場所のグループ
    • common, glossary => 意味
    • components, modules => 場所 (View と強く結びつく)
  • common, glossary はどこの View からも呼び出しが可能。
    エイリアスが利用できるなら componentsmodules などから利用することも可。
  • 考慮したいこと
    • 翻訳依頼するときに翻訳者がコンテキストを理解しやすいか
    • 開発時に不要なメッセージが増えにくいか
      • 必要な言語を探しやすいか
      • 新しいメッセージをどこに追加するか迷わないか
    • 利用していないメッセージを発見しやすいか

その他

Vue.js vue-router Nuxt.js の各フック (など) がいつ実行されるかメモ

続編のようなものを書きました。 ryotah.hatenablog.com


GitHub - ryotah/vue-vue-router-nuxt-hooks

f:id:ryotah:20180924173906p:plain

ログに表示される内容:

  • [INFO] vue: [page] / [hook]
    • Vue.js のライフサイクルフック
    • created, beforeUpdate, destroyed, etc.
  • [INFO] vue-router: [page] / [guard]
    • vue-router のナビゲーションガード
    • beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave
  • [INFO] nuxt: [page] / [method]
    • Nuxt.js の asyncData, fetch

確認できること:

  • 以下の遷移
    • /hooks/parents/1
    • /hooks/parents/2
    • /hooks/parents/1/child
    • /hooks/parents/2/child
    • /hooks/parents/2/child?q=query
  • リダイレクトや遷移失敗 (abort) 時の処理
  • asyncData / fetch でページ (state) を初期化、beforeRouteLeave でページ (state) をリセットした場合の問題点の把握
    • 今回のモチベーションはこれ

8月メモ・リンク集

8月に調べたことのメモです。

Vue 関連

基礎

ちょっと応用

Angular 関連

JavaScript その他

その他