コンポーネント単体テストについてつらつらと
書きます。
何をテストするのか
基本的に、考え方は関数のテストと同じだと思います。何かが入力されると結果が出力される。テストでは、その結果が正しいかを確認します。「入力」と「出力」にフォーカスすることが大切です。
一般的なヒント | Vue Test Utils では以下のように説明しています。
コンポーネントのパブリックインターフェイスを検証するテストを作成し、内部をブラックボックスとして扱うことをお勧めします。単一のテストケースでは、コンポーネントに提供された入力(ユーザーのやり取りやプロパティの変更)によって、期待される出力(結果の描画またはカスタムイベントの出力)が行われることが示されます。
このように考えれば、テストの方針も立てやすくなります。結果、何をテストしているのか理解しやすくなりますし、コードもシンプルになります。内部実装をリファクタしたとしても、テストが壊れることはないです。
なぜ書くのか
なぜテストをするのですか | Vue Test Utils から引用。
単体テストの利点:
(ここに「フレームワークへの理解が深まる」も追加できると思います。)
単体テストのすべきこと:
- 実行が早いこと
- 理解しやすいこと
- 一つの作業だけをテストすること
テストが書きにくいな思ったら、もしかしたら、コンポーネントの設計に問題があるかもしれません。例えば、あるコンポーネントに複数の役割を持たせていたり、必要以上に大きく依存関係が複雑になってたり。
テストを書くことで既存コードに改良の余地があることに気づく、そういったケースもあるかと思います。
入力と出力とは何か
具体的に「入力」と「出力」にはどのようなものがあるのでしょうか。一般的な SPA の Web アプリケーションを例にして考えてみます。
入力:
- プロパティ(親コンポーネントから渡される)
- Vue なら
props
, Angular なら@Input
に相当するもの
- Vue なら
- 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
に相当するもの
- Vue なら
- DOM
- 表示されたテキスト, 要素の On/Off, スタイル, 属性 など
- Route
- Router へのアクション
- Store
- Vue (Vuex) なら
this.$store.{dispatch|commit}
, Angular (ngrx) ならthis.store.dispatch
など
- Vue (Vuex) なら
これ以外にもあるとは思いますが、大体このようなものではないでしょうか。
テストを書く前に調べること
初めてさわるフレームワークでコンポーネントの単体テストを書く場合、上にあげたような「入力」をシュミレートする方法と「出力」を検証する方法を知っておくと、順調にテストが書けると思います。
あとは非同期の扱いや、テストダブル(スタブ・モック・スパイなど)の扱いについて知っておくといいかと。(これらに関しては、利用するテストフレームワーク側の機能を利用することも多いです。)
一般的なコンポーネントのテストシナリオ
Angular ドキュメントには Component Test Scenarios というセクションがあり、一般的なテストシナリオの実装方法について解説しています。「一般的なテストシナリオにはどんなものがあるのか」について知るという点で、Angular ユーザー以外にも参考になると思います。
以下は抜粋です。
- Component binding
- DOM の操作 / 評価
- Component with a dependency
- 依存関係のあるコンポーネント
- Component with async service
- 非同期処理を扱うサービスを利用しているコンポーネント
- Component with inputs and outputs
- Routing component
- Router を扱うコンポーネント
- Routed components
- ルート設定されているコンポーネント
- Nested component tests
- Components with RouterLink
- Vue の場合
<nuxt-link :to="...">
,<router-link :to="...">
を使うコンポーネント
- Vue の場合
- Use a page object
- Page オブジェクトを利用して要素操作に関するロジックを分離する
Vue 単体テストのサンプル
テストシナリオを踏まえて、Vue の単体テストで必要そうなケースをいくつかピックアップしたのがこちらです。(若干中途半端ではありますが)
props
と emit
のテスト
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(); }); });
nuxt-link
(router-link
) を利用しているコンポーネントのテスト
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
- Jest
- Angular
- その他
- テストダブル - Wikipedia
- テストスタブ, テストスパイ, モックオブジェクト, フェイクオブジェクト, ダミーオブジェクト
- テストダブル - Wikipedia
Vue SSR 環境構築メモ
Vue SSR ガイド, vuejs/vue-hackernews-2.0 を参考に環境構築を試してみた時のメモです。
- サーバサイドで Vue をコンパイルする
- クライアントとサーバのコードをわける (Webpack)
- ルーティング
- プリフェッチを実現
- サーバとクライアント、それぞれをビルドするために個別の Webpack 設定ファイルを用意
- バンドルレンダラ
- クライアントマニフェストを生成
- ビルド設定をもうちょい
サーバサイドで Vue をコンパイルする
https://github.com/ryotah/vue-ssr/compare/step-0...step-1-render-to-string
やっていること:
コード抜粋:
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-*.js
がapp.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" } // ... };
参考:
ルーティング
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
- ts-loader バージョンアップ(Webpack4 に対応)
lang="postcss"
を削除- https://github.com/nuxt/nuxt.js/issues/3231#issuecomment-381885334
because nuxt has enabled postcss-loader in css by default
参考:
- Release Notes - Nuxt.js
- migrate to nuxt 2.0 by Ataww · Pull Request #55 · nuxt-community/typescript-template · GitHub
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 arguments · Issue #1687 · nuxt/nuxt.js · GitHub
- vue-router の
meta
を利用すると綺麗に実装できる
- vue-router の
// 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
- GitHub - tc39/proposal-regexp-unicode-property-escapes: Proposal to add Unicode property escapes
\p{…}
and\P{…}
to regular expressions in ECMAScript. - JavaScriptのUnicode Property Escapesについての補説
- ECMAScript 2018よりRegExpで使えるようになったUnicodeプロパティーエスケープ (\p{...}, \P{...}) についての補説
なおHiraganaとKatakanaはあるのにKanjiがないと思われるかもしれませんが、Han (Hani) というのが漢字のことです(「漢字」の中国語読みをローマ字化したHanziに由来)。
補足:
文字コードの変換
- Unicode Lookup: convert special characters
- 文字を入力すると Hex (16進数) , HTML コードに変換してくれる
latin
,kana
なども
- マルチバイト文字, fromCharCode, charCodeAt, Unicode - ryotah’s blog
'>'.charCodeAt(0).toString(16) => '3e'
,String.fromCharCode(0x3e) => '>'
- JavaScriptでのサロゲートペア文字列のメモ - Qiita
['𩸽'.charCodeAt(0), '𩸽'.charCodeAt(1)].map(num => num.toString(16)); // => ["d867", "de3d"]
パスワード向け正規表現
例: 数字・アルファベット・記号を許容する
- 記号とは
- アスキー文字ならば可能、と定義した場合
サンプル:
// 数字・小文字・大文字・記号を最低1文字含む, 8文字以上64文字以下 /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!-\/:-@[-`{-~])[!-~]{8,64}$/.test(value);
その他参考:
肯定的先読み
- 言語別:パスワード向けの正規表現 - Qiita
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 基礎知識:
- SVG 要素リファレンス - SVG: Scalable Vector Graphics | MDN
- 構造的要素
<defs>, <g>, <svg>, <symbol>, <use>
- 構造的要素
- svg 要素の基本的な使い方まとめ
defs要素は各種定義情報(テンプレートとなる図形やグラデーション等)を格納する.プログラムにおけるサブルーチンを記述する場所に相当する.
use要素はxlink:href属性に設定した要素の内容を元に新しい図形を描画することを示す.
g要素が図形の論理的なひとまとまりを表すのに対し,symbol要素はviewBoxを定義し新たな図形を定義する点が異なる.
- SVGのviewBoxをわかりやすく紐解く | Tech Blog | 株式会社INDETAIL - インディテール
ツール・書き出し:
- GitHub - svg/svgo: Node.js tool for optimizing SVG files
- GitHub - svgstore/svgstore: Combines multiple svg files into one.
- GitHub - jkphl/svg-sprite: SVG sprites & stacks galore — A low-level Node.js module that takes a bunch of SVG files, optimizes them and bakes them into SVG sprites of several types along with suitable stylesheet resources (e.g. CSS, Sass, LESS, Stylus, etc.)
- Workflow for creating SVG sprites with NPM scripts – Oleg Varaksin – Medium
- Preparing and Exporting SVG Icons in Sketch – Design + Sketch – Medium
- Sketchでアイコンを作成 / 書き出しするときの Tips
その他:
JIRA 芸
JIRA 使い始めました。
Form Events
- input - Event reference | MDN
- change - Event reference | MDN
For input elements with type=checkbox or type=radio, the input event should fire when a user toggles the control (via touch, mouse or keyboard) per the HTML5 specification, but historically, this has not been the case.
- html-form-event - StackBlitz
- サンプル
書いた記事
その他
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>
v-validate
にバリデーションルールを渡す- Validation Rules | VeeValidate と自分で追加可能な Custom Rules | VeeValidate がある
- バリデーションルールを設定しない場合でも VeeValidate が属性から追加のルールを推定する
errors.first
メソッドで入力フィールドのエラーを取得
バリデーションルールのシンタックス
<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
プロパティについて、など
- 引数を必要とするルール・他の入力フィールドと比較をするルール・Non-immediate なルール(
エラーメッセージ
エラーメッセージを変更/追加する
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' } }, };
- 以下のようにフィールド名が変更される
- The email field is required. => The Email Address field is required.
data-vv-as
を利用してフィールド名を変更することもできるが、多言語対応するなら辞書を利用した方がいい
さらに詳しく
- Error Messages | VeeValidate
custom
プロパティについて、など
カスタムコンポーネントで使う
- Validating Custom Components | VeeValidate
DEMO:
スコープ
- VeeValidate のバリデータスコープはコンポーネント毎につくられる
- コンポーネントに
inject: ['$validator']
をすることで親のスコープを取得できる - メモリ消費量を抑えるため、バリデータスコープの自動生成をさせないことも可能方法
- 設定変更に関しては Configuration | VeeValidate に詳細がある (
inject: false
) - (自動生成されなくなるので)自身のコンポーネントにバリデータスコープを生成したい場合は以下のように
$_veeValidate
を利用する
- 設定変更に関しては Configuration | 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
- Flags | VeeValidate
Field
クラスのプロパティflags
, mapFields helper など
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
参考:
- Validation Events | VeeValidate
- data-vv-validate-on Custom Events Not Working · Issue #1381 · baianat/vee-validate · GitHub
入力フィールドが動的に表示/生成される場合
Vue が要素を再利用しないように、ユニークな key
を設定する必要がある。
参考:
Misc.
v-validate:xxx
.immediate
,.continues
,.bails
型定義ファイル
9月メモ・リンク集
9月に調べたことのメモです。
Vue 関連
- What is the best way to access this.$router.push() / router.push() inside the Vuex? · Issue #1384 · nuxt/nuxt.js · GitHub
- v-on to bind dom event in a custom component · Issue #2942 · vuejs/vue · GitHub
<foo @click.native="hello">
- カスタムコンポーネントがネイティブイベントを利用するために
- .sync vs v-model - Vue Forum
- Vue の
.sync
とv-model
について - 基本は同じもの。複数の prop に v-model ロジックを利用したいとう要望のため
.sync
が追加された
- Vue の
- nuxt.js/examples/with-keep-alive at dev · nuxt/nuxt.js · GitHub
ルーティング関連
- Vue Router の公式ガイドを読んだときに試したサンプル
- Vue.js vue-router Nuxt.js の各フック(など)がいつ実行されるかメモ - ryotah’s blog
- ナビゲーション実行時のフックの実行順を確認するためのサンプル
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 からも呼び出しが可能。
エイリアスが利用できるならcomponents
やmodules
などから利用することも可。- 考慮したいこと
- 翻訳依頼するときに翻訳者がコンテキストを理解しやすいか
- 開発時に不要なメッセージが増えにくいか
- 必要な言語を探しやすいか
- 新しいメッセージをどこに追加するか迷わないか
- 利用していないメッセージを発見しやすいか
その他
Vue.js vue-router Nuxt.js の各フック (など) がいつ実行されるかメモ
続編のようなものを書きました。 ryotah.hatenablog.com
GitHub - ryotah/vue-vue-router-nuxt-hooks
ログに表示される内容:
[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
- Nuxt.js の
確認できること:
- 以下の遷移
- /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 関連
基礎
- Vue.js 学習メモ - ryotah’s blog
- Vuex 学習メモ - ryotah’s blog
- Nuxt.js 学習メモ - ryotah’s blog
- How to redirect with vue-router? · Issue #1843 · nuxt/nuxt.js · GitHub
- tips
- vue.js - VueJs 2.0 - how to listen for
props
changes - Stack OverflowbeforeUpdate
,updated
でいいかとおもったけどちがった- 上記 hook は
props
に限らない
- 上記 hook は
- Props change will not trigger child component update if props not referred in template · Issue #5325 · vuejs/vue · GitHub
updated
hook means the component did a re-render. If the prop is never used during render, why would you expect the component to re-render when it changes?
- vue.js - What is the difference between updated hook and watchers in VueJS? - Stack Overflow
The lifecycle hooks around update respond to changes in the DOM. Watchers respond to changes in the data.
ちょっと応用
- Vue Mastery
- Evan You on Proxies - Advanced Components | Vue Mastery
- Proxy を利用した場合の話
- Vue のリアクティブシステム - ryotah’s blog
- 動的に Vue インスタンスを append する方法のヒント
- Modal 実装に関して
- Modal - BootstrapVue
- Component | Element
- モーダルコンポーネント — Vue.js
- ミニマムなモーダル
- vue-modal - StackBlitz
- 以下機能を追加したモーダルサンプル
- エスケープキーで閉じる
- 背景(backdrop)クリックで閉じる
show
,shown
,hide
,hidden
イベント発火v-model
を利用した open/hide 操作show()
,hide()
メソッドによる open/hide 操作body
のスクロール防止- (スクロールバーがなくなることによるレイアウトずれに関しては未実装)
- モーダル内にフォーカス固定
- 以下機能を追加したモーダルサンプル
- TypeScript
Angular 関連
- (WIP) Learn Angular - ryotah’s blog
- Angular 学習のためのロードマップ
JavaScript その他
- Proxy
- GitHub - GoogleChrome/proxy-polyfill: Proxy object polyfill
This is a polyfill for the Proxy object, part of ES6. See the MDN docs or Introducing ES2015 Proxies for more information on Proxy itself. Unlike other polyfills, this does not require Object.observe, which is deprecated.
- ECMAScript 2015 の Proxy(Proxies) / Reflect をなんとなく理解する
- GitHub - GoogleChrome/proxy-polyfill: Proxy object polyfill
- Object.observe の死 (ECMAScript の提案取り下げ、V8 からも削除予定) - てっく煮ブログ
- redux Presentational / Container componentの分離 - react-redux.connect()のつかいかた
- ISO 形式 (ISO 8601) の文字列判定 - ryotah’s blog