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

【エラー】【Vue.js】「npm install --save-dev axios」できない

【やりたいこと】

npm install --save-dev axios

して、axios使いたい。

【エラー文】

MacBook-Pro axios % npm install --save-dev axios
npm ERR! code ENOSELF
npm ERR! Refusing to install package with name "axios" under a package
npm ERR! also called "axios". Did you name your project the same
npm ERR! as the dependency youre installing?
npm ERR!
npm ERR! For more information, see:
npm ERR!     <https://docs.npmjs.com/cli/install#limitations-of-npms-install-algorithm>

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/hoge/.npm/_logs/2020-02-24T14_04_39_904Z-debug.log

【解決方法】

インストールしようとしている「axios」

このプロジェクトの名前「axios」が一緒だからダメっぽい。

package.json

{                    
  "name": "testaxios",   <=  変更 "name": "axios"
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.4",
    "vue": "^2.6.11",
    "vue-router": "^3.1.5"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.2.0",
    "@vue/cli-plugin-eslint": "~4.2.0",
    "@vue/cli-plugin-router": "~4.2.0",
    "@vue/cli-service": "~4.2.0",
    "axios": "^0.19.2",
    "babel-eslint": "^10.0.3",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.1.2",
    "vue-template-compiler": "^2.6.11"
  }
}

【参考】

【エラー】Refusing to install as a dependency of itself

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

Vue.jsで日本語入力中に未確定状態でinput boxにmaxlengthを超えた部分が表示されないようにするには

インプットボックスの文字数制限

最近、インプットボックスの文字数制限を実装する必要があり、日本語入力中の挙動で結構はまりました。とある方のアドバイスで解決したので、備忘録として残しておこうと思います。アドバイスしてくださった方、ありがとうございました。なお、今回はVue.jsでの実装で直面した問題でしたが、Vue.js以外でも(JavaScriptなら)同様のことが起こりうると思います。(より良い実装の情報があれば、ぜひ教えてください。)

シンプルにmaxlengthを指定した実装

以下ソースコードで問題ないと思いきや...

ソースコード

html
<div id=app>
  <input type="text" v-model="test" maxlength="5">
  {{test}}
</div>
Vue.js
const app = new Vue ({
  el: '#app',
  data: {
    test:''
  }
})

もし、挙動を試してみたい方は、以下codepenからお試しください。

See the Pen This form can't restrict max length on Japanese language input by Yasunori MATSUOKA (@YasunoriMATSUOKA) on CodePen.

微妙な状況に

しかし、この実装では日本語入力中に未確定状態では最大文字数のmaxlengthを超えた部分についてもインプットボックスに表示されてしまうという状況に悩まされていました。(Enter押す等して確定すれば、maxlength以降の文字は消えるのですが、その前の未変換の状態や変換中はmaxlength以上の文字が入力できているような印象をユーザーに与えてしまうという点で微妙な状況でした。)

解決策

inputイベント時にevent.target.valueの長さをチェックし、maxlengthを超えている場合はinput boxからフォーカスを外す(event.target.blur()する)

解決策のソースコード

html
<div id=app>
  <input type="text" v-model="test" maxlength="5" @input="onInput">
  {{test}}
</div>
Vue.js
const app = new Vue ({
  el: '#app',
  data: {
    test:''
  },
  methods: {
    onInput (event) {
      console.log('onInput', event)
      if (event.target.value.length > event.target.getAttribute("maxlength")) {
        event.target.blur()
      }
    }
  }
})

無理矢理感は否めませんが、これで、maxlengthを超えた際は、自動的に対象のinput boxからフォーカスが外れ、日本語入力が強制的に確定され、maxlength以降の仮入力中の文字がinput boxに表示されることが無くなりました。

試してみたい方は、以下のcodepenでお試しください。

See the Pen Restrict max length on Japanese language input by Yasunori MATSUOKA (@YasunoriMATSUOKA) on CodePen.

試したがうまくいかなかったもの

keydownイベント時にevent.target.valueの長さをチェックし、maxlengthを超えている場合はイベントを無効化する

うまくいかなかった実装のソースコード

html
<div id=app>
  <input type="text" v-model="test" maxlength="5" @keydown="onKeyDown">
  {{test}}
</div>
VUe.js
const app = new Vue ({
  el: '#app',
  data: {
    test:''
  },
  methods: {
    onKeyDown (event) {
      console.log('onKeyDownEvent', event)
      if (event.target.value.length >= 5) {
        if (event.code !== "Backspace" && event.code !== "Delete" && event.code !== "ArrowRight" && event.code !== "ArrowLeft" && event.code !== "ArrowUp" && event.code !== "ArrowDown" && event.code !== "PageUp" && event.code !== "PageDown" && event.code !== "Enter" && event.code !== "Space" && event.code !== "Tab") {
          event.preventDefault()
          console.log('preventDefault', event)
        }
      }
    }
  }
})

IME ONの状態での入力中に、入力をJavaScriptで制御するのは難しいようで、半角英数ならば意図した通に動くものの、日本語入力中には意図した通り動作させることができませんでした。
試してみたい方は、以下codepen上でお試しください。

See the Pen Try to apply max length restriction on input Japanese language (don't work well) by Yasunori MATSUOKA (@YasunoriMATSUOKA) on CodePen.

まとめ

半角英数で完結する言語うらやましい...日本語入力等のIME ONにする必要がある言語はこういうとき大変...

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

Single Page Application の Form 画面における Best Practice を考える(後編)

2回に分けて Form Validation について考察しました。最後に Form Validation を子となる Component に任せるやり方を紹介します。

正直この手法は少しやりすぎな気もしているので好みの問題が出てくると思います。

Page Component はどこまで関心を持つべきか?

さて、前回 form validation の判定を Nuxt.js でいうところの Page Component で行っていました。この validation に不備があればエラーメッセージをコントロールし、不備がなければ Submit button を押す事ができます。

これらの処理について本当に Page Component が知っているべきなのか。というのは議論の余地があると思いますが、試しに form validation のロジックを form component に任せてみましょう。

やってみた

姓、名、セイ、メイの入力フォームは基本的に1セットだと思うのでこれらを component にしてみます。onChange で変更が発生するたびに fields の情報と isValid を返します。Page Componet が関心を持つのは入力された値とそれらが valid であるかどうかだけ、にしました。

components/forms/user-name-fields.vue
<template>
  <div>
    <div class="field is-horizontal">
      <div class="field-label is-normal">
        <label class="label">お名前(漢字)</label>
      </div>
      <div class="field-body">
        <InputTextField
          placeholder="姓"
          :errorMessages="errors.lastNameKanji"
          :onChange="(value) => handleChangeFields({lastNameKanji: value})"
        />
        <InputTextField
          placeholder="名"
          :errorMessages="errors.firstNameKanji"
          :onChange="(value) => handleChangeFields({firstNameKanji: value})"
        />
      </div>
    </div>
    <!-- お名前(カタカナ) -->
    <div class="field is-horizontal">
      <div class="field-label is-normal">
        <label class="label">お名前(カタカナ)</label>
      </div>
      <div class="field-body">
        <InputTextField
          placeholder="セイ"
          :errorMessages="errors.lastNameKana"
          :onChange="(value) => handleChangeFields({lastNameKana: value})"
        />
        <InputTextField
          placeholder="メイ"
          :errorMessages="errors.firstNameKana"
          :onChange="(value) => handleChangeFields({firstNameKana: value})"
        />
      </div>
    </div>
  </div>
</template>

<script>
import { validate } from '~/utils/validator'
import InputTextField from '~/components/forms/input-text-field'

export default {
  components: {
    InputTextField
  },
  props: ['onChange'],
  data () {
    return {
      fields: {},
      errors: {},
    }
  },
  methods: {
    handleChangeFields (fields) {
      this.fields = { ...this.fields, ...fields }
      this.errors = Object.assign({}, validate(this.fields))
      this.onChange({
        isValid: Object.keys(this.errors).length === 0,
        fields: this.fields
      })  
    }   
  }
}
</script>

呼び出し元はこのように書けます。

pages/vue-form-validation/example2.vue
<template>
  ...
  <UserNameFields :onChange="handleChangeUserNameFileds" />
  ...
</template>

<script>
import UserNameFields from '~/components/forms/user-name-fields'

export default {
  components: {
    UserNameFields,
  }
  data () {
    return {
      isUserNameFieldsValid: false,
      fields: {}
    }
  },
  methods: {
    handleChangeUserNameFileds ({isValid, fields}) {
      this.fields = { ...this.fields, ...fields }
      this.isUserNameFieldsValid = isValid
    },
    ...
  }
}
</script>

このようにする事によって Page Component の記述量がかなり軽減されます。今回の案件では問合せ画面がかなり多いらしいので、Page Component を大量に作成する事になりそうです。そうなると同じ処理を繰り返さないように Form Component に Validation を任せるのも一案だと考えています。

懸念点

すべての Validation の処理が同期で行われるのであればこれでも良いかと思いましたが、郵便番号を入力した際に、住所を補完するために非同期で API を問合せする処理も必要になると思います。

場面によって一長一短があるのでどこで関心を分離するのかはまだ私の中で決着が着いていません。

まとめ

最後に Validation の処理を Page Component ではなく、子となる component に任せる方法について言及しました。たぶん似たような Form 画面を大量に作成する案件と出会う事はほとんどないと思うので基本的に前回、前々回に書いた記事の内容で事足りると思いますのでどちらの Component がどこまで関心を持つべきなのかはまだ私の中で決着はついていません。

長々と書き綴ってきましたが、これはいろいろあるやり方の一つでしかないので各自で適宜判断してもらえればと思います。そしてきっと後世のエンジニアたちがもっと素晴らしい手法を発見して記事にしてくれると思いますので私からは以上とさせて頂きます。

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

Nuxt質問所、aLizサークルを開設しました!!

この度noteサークルを開設しました✨?
皆様の質問の、ぜ〜んぶに答えます!!!?

?こんな方にオススメです♪
・質問できるコミュニティがない…
・ググってみたけど分からない…
・やってみたもののエラーで動かない…
・初学者同士のコミュニケーションを取りたい!
・付け焼き刃の知識ではなく論理的に理解したい!

?参加リンク
aLiz CLUB
https://note.com/aliz/circle

?以下サークルの詳細です!
・Nuxtをメインとしたサービス
・Nuxt初心者のサポートが中心
・これからプログラミング始めようとしている方でも参加OK
・質問したり、ディスカッションをするのがメイン

?aLizのポートフォリオ はこちら
https://note.com/aliz/n/nf1d2b5e16bc1#SsL5X

?入りたい!
参加していただいた方を
aLizのSlackにご招待させていただきます。
noteサークル内の掲示板では
対応しておりませんのでご注意ください。
今後は、「Nuxt案件を受けたけど不安?」
そんな方のガッツリサポート版も作る予定です!
お楽しみに♪

?サークルに入れないという方はこちら!
まずnote IDを作る必要があるので
そちらの手順を細かく解説してくれています。
https://note.com/kanoyuri/n/n97c0f4191bb5

皆様のご参加
お待ちしております??

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

Vue+Firebase+Hosting+FunctionsでOGP対策

SPA における OGP 対策

皆さん SPA 作ってますか!

私は 2020 年の年始から SPA の勉強を始めています。

勉強をすすめに連れて、ページ遷移するたびに更新が走る web サイトにストレスを感じるようになってきました。開発者でなくても現代のリッチな web サイトにおいてページ遷移に 1 秒程度であろうともそれぐらいの時間がかかろうものなら「おそっ」と思ってしまう人も多くなってきたのではないでしょうか。

本記事では

  • SPA の勉強をしている人
  • SPA でブログを運営しようと考えてる開発者

以上の人を対象に、SPA の弱点である OGP 対策のうちの解決策の1つを提供します。

また、本記事では Vue + Firebase + Hosting + Functions を扱っていますが、OGP 対策のための根幹となる部分は変わらないのでご心配しなくても大丈夫です。

SPA における OGP 対策手法

SPA において OGP 対策は切っても切り離すことができない問題です。

ちなみに OGP(Open Graph protocol) を知らない人は以下のような画像をお見せすれば理解できるでしょうか。

OGPの例

Twitter なら Twitter のクローラー用の bot が、 Facebook なら Facebook のクローラー用の bot が html の meta タグから必要な情報を取得してくるのです。

<meta property="og:title" content="Sample" />
<meta name="description" content="Sample Description Body" />
<meta property="og:type" content="website" />
<meta property="og:url" content="http://sample.hoge.sample.com/" />
<meta property="og:image" content="http://sample.hoge.sample.test.jpg" />
<meta name="twitter:card" content="summary" />
<meta property="fb:app_id" content="数字の列挙" />

上記は OGP の一例です。SPA において OGP 対策のこと知らずに開発している人や、とりあえず作ってみた系の時には public/index.html のような、ルートとなる index ファイルの中身に上記のような meta タグを仕込んで Hosting しておけばリンクを貼った時にいい感じにグラフィカルな URL を作ることができていたのではないでしょうか。

但し、SNS などがそのサイトへの流入の手段として大きい場合、各ページごとに OGP の中身を設定してリンクごとに生成されるグラフィックや文言が変わって欲しいのが普通だと思います。

それらを解決する手段として、現状以下の方法に大別されることになると思います。

  1. Server Side Rendering
  2. Prerendering
  3. その他(本記事では Functions を扱います)

私が OGP 対策をする際に考慮したのは以下のようなポイントです

  • 難易度
  • ブログ部分のページごとに個別 OGP を設定できること
  • 継続的に運用のために時間が取られないこと

本記事では SSR と Prerendering に関しては実行コードないので、結論だけ知りたい人は、Functionsへレッツゴーです。

SSR

SSR(Server Side Rendering)のメリット

  • 柔軟に設定できる
  • ファーストビューまでが最速

一見メリットしか感じないような SSR ですが、小規模なサイトにおいてはオーバーテックだと言わざるを得ないです。

Vue で SPA を作り Firebase に Hosting した人は、フロントで完結することに歓喜していたはずです。私は歓喜しました!

なのに、OGP のためだけにサーバーを立ち上げるのは辛すぎます。

先ほど、OGP 対策時に考慮したポイントとして、 難易度個別OGP設定 継続的な運用の時間が取られない を上げましたが、このうち難易度と継続的な運用の時間が取られないことに関しては、SSR を選択した場合は怪しいです。習熟している人なら全く問題がないかもしれないですが、私にとっては習得までの間にブログ運営が辛くなってやめたくなることが想像できたのでこの選択肢は却下です。

もし、詳しく調べてみたい人がいたら、Vue.js サーバサイドレンダリングガイド を公式がドキュメントを作ってくれていますので、確認してみるといいかもしれません。

Prerendering

次に Prerendering のメリット

  • 難易度低い(かも)
  • 個別に設定可能

prerender-spa-pluginというプラグインを使うことで比較的簡単に OGP の設定が可能なように感じました。

参考:Vue.js のプリレンダリングで手軽な OGP 対応

が、個別のページを設定するために、ページ単位で自分でタイトルや詳細メッセージを変えていくのは、継続的に運用のために時間が取られないことに反してしまったので却下しました。(もしかしたら自動で設定できたのかもしれないですが、判断した時点では知識が足らずこの方法を選択することができませんでした。もし便利な方法や記事があれば教えて欲しいくらいです。)

Functions

そして、最後に その他 の方法を説明しようと思います。

本記事では Firebase の Functions を利用することにしています

functions/index.jsにて、以下のように API などによってブログの内容を取得し、そのタイトルやボディ部分を使って meta タグ設定つきの HTML を作成するようにしています。

ちなみに、Vue は公式が ButterCMS とのブログ連携をドキュメントとして残しているので、ブログの内容取得部分を参考にしてみると良いかもしれません。

const functions = require("firebase-functions");
const fetch = require("node-fetch");

function buildHtmlWithPost(id, obj) {
  return `<!DOCTYPE html><head>
<meta name="description" content="${obj.title}" />
<meta property="og:site_name" content="Sample Blog" />
<meta property="og:title" content="${obj.title}">
<meta property="og:description" content='${truncate(
    removeHTMLTag(obj.body),
    150
  )}' />
<meta property="og:url" content="http://sample.hoge.sample.com/" />
<meta property="og:image" content="${obj.thumbnail.url}">
<meta property="og:type" content="website" />
<meta property="og:locale" content="ja_JP" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="${obj.title}" />
<meta name="twitter:image" content="${obj.thumbnail.url}" />
<link rel="canonical" href="/note/${id}">
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title>${obj.title}</title>
</head>
<body>
  <script>
    window.location = "/_note/${id}";
  </script>
</body>
</html>`;
}

function truncate(str, len) {
  return str.length <= len ? str : str.substr(0, len) + "...";
}

function removeHTMLTag(str) {
  return str.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, "");
}

exports.note = functions.https.onRequest((req, res) => {
  const path = req.path.split("/");
  const noteId = path[path.length - 1];
  fetch(`何かしらのAPIのURL`, {
    headers: {
      "SOME-API-KEY": functions.config().vue.app.same.api.key
    }
  })
    .then(response => response.json())
    .then(result => {
      // APIのレスポンス結果からHTMLを作成して、Clientに返却する
      const htmlString = buildHtmlWithPost(noteId, result);
      res.set("Cache-Control", "public, max-age=3600, s-maxage=3600");
      res.status(200).end(htmlString);
    })
    .catch(err => {
      res.status(500).end(err);
    });
});

Functions へのリクエストは、Hosting にて設定することが可能です。
rewritesを用いて、/note/~に対するリクエストがきたら、Functions の note 関数へとリクエストを送ります

/note/~以外の URL へのリクエストの場合は index.html を返すようにしています

"hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [{
        "source": "/note/**",
        "function": "note"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  },

src/router/index.jsに以下のようなパスを設定することで、functions で設定した window.location = "/_note/${id}"を通常の /note/\${id} へのコンポーネントへのリンクとしてリダイレクトしてくれます

また、Router の mode にhistoryを設定することも忘れないでください

const routes = [
  {
    path: "/",
    component: App
  },
  {
    path: "/note/:id",
    component: Note
  },
  {
    path: "/_note/:id",
    redirect: "/note/:id"
  }
];

const router = new VueRouter({
  mode: "history",
  routes
});

この対応によって、直接リンクが踏まれた際に Functions にて生成された ogp 用の meta タグ付きの HTML を生成して bot に渡すことができるようになりました。

まとめ

本記事では Vue + Firebase + Hosting + Functions によってブログページ個別に OGP を設定することができました。

もし、今後、SSR か Prerender に置き換えるような時が来れば、それも記事にして参考になるようにしたいと思います。

正直ベストプラクティスには思えていないので、難易度個別OGP設定可能運用が楽の 3 つが揃うものがあればぜひコメントいただけると嬉しいです。

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

Single Page Application の Form 画面における Best Practice を考える(中編)

というわけで前回 Form Validation について考えたので、それを踏まえて実際に Component を作成します。

input text field

以下、私が作成した components/forms/input-text-field.vue です。@change ではなく、@input を使っているのは @change だと focus が外れるまで変更を検知できないからです。focus が外れてから form validation のトリガーが実行されると不便なので @input を使使います。なお、 CSS framework に Bulma を使用しています。

また 子となる component に pristine という state を持たせています。これはフォームに未入力の場合、xx は入力必須です といったメッセージはまだ表示させたくないので、あえて子となる component に state をもたせました。

私は原則 component は stateless にしたいと考えています。なぜなら密結合をさせない為にも、子となる component を外側の世界に関与させないためです。なのですが、逆に親 component に関与させたくない細かい事象については子となる component で完結させたいと考えました。

components/forms/input-text-field.vue
<template>
  <div class="field">
    <p class="control has-icons-right">
      <input @input="handleChange" :class="{'is-danger': hasError}" class="input" type="text" :placeholder="placeholder">
      <span v-if="isValid" class="icon is-small is-right">
        <fa icon="check" aria-hidden="true" />
      </span>
    </p>
    <p v-if="hasError" class="help is-danger">
       {{ errorMessages }}
    </p>
  </div>

</template>

<script>
export default {
  props: ['placeholder', 'errorMessages', 'onChange'],
  data () {
    return {
      pristine: true
    }
  },
  computed: {
    isValid () {
      return !this.pristine && !this.errorMessages
    },
    hasError () {
      return !this.pristine && this.errorMessages
    }
  },
  methods: {
    handleChange (e) {
      this.onChange(e.target.value)
      this.pristine = false
    }
  }
}
</script>

Nuxt.js でいうところの Page Component からは以下のように呼び出します。

pages/vue-examples/example.vue
<template>
  <div class="field-body">
    <InputTextField
      placeholder="姓"
      :errorMessages="errors.lastNameKanji"
      :onChange="(value) => handleChangeForms({lastNameKanji: value})"
    />
    <InputTextField
      placeholder="名"
      :errorMessages="errors.firstNameKanji"
      :onChange="(value) => handleChangeForms({firstNameKanji: value})"
    />
  </div>
</template>

<script>
import { validate } from '~/utils/validator'
import InputTextField from '~/components/forms/input-text-field'

export default {
  components: {
    InputTextField,
  },
  data () {
    return {
      fields: {},
      errors: {}
    }
  },
  methods: {
    async handleChangeForms (fields) {
      this.fields = { ...this.fields, ...fields }
      this.errors = Object.assign({}, validate(this.fields))
    }
  }
}
</script>

<template> の記述量はすこし多いですが、前回作成した validator 関数のおかげで<script> 内の記述量は抑える事ができました。

生年月日の Select Field を考える

よくある生年月日の入力フォームですが、今度は単一ではなく複数の select 要素から構成される場合を考えます。今回は yyyy-mm-dd 形式の文字列を value として受け取る component を考えます。

components/forms/birthday-field.vue
<template>
  <div class="field is-narrow">
    <div class="control">
      <div class="select">
        <select @change="handleChageYear" :value="year">
          <option :value="null" :key="`year:0`"></option>
          <option v-for="n in 50" :value="n + 1950" :key="`year:${n}`">{{ n + 1950 }}</option>
        </select>
      </div>
      <div class="select">
        <select @change="handleChageMonth" :value="month">
          <option :value="null" :key="`month:0`"></option>
          <option v-for="n in 12" :value="zeroPadding(n)" :key="`month:${n}`">{{ n }}</option>
        </select>
      </div>
      <div class="select">
        <select @change="handleChageDay" :value="day">
          <option :value="null" :key="`day:0`"></option>
          <option v-for="n in 31" :value="zeroPadding(n)" :key="`day:${n}`">{{ n }}</option>
        </select>
      </div>
    </div>
    <p v-if="hasError" class="help is-danger">
       {{ errorMessages }}
    </p>
  </div>
</template>

<script>
export default {
  props: ['value', 'errorMessages', 'onChange'],
  data () {
    return {
      pristine: true
    }
  },
  computed: {
    year () {
      return this.value && this.value.substr(0, 4)
    },
    month () {
      return this.value && this.value.substr(5, 2)
    },
    day () {
      return this.value && this.value.substr(8, 2)
    },
    isValid () {
      return !this.pristine && !this.errorMessages
    },
    hasError () {
      return !this.pristine && this.errorMessages
    }
  },
  methods: {
    zeroPadding (value) {
      return String(value).padStart(2, '0')
    },
    handleChageYear (e) {
      this.onChange([e.target.value, this.month, this.day].join('-'))
      this.pristine = false
    },
    handleChageMonth (e) {
      this.onChange([this.year, e.target.value, this.day].join('-'))
      this.pristine = false
    },
    handleChageDay (e) {
      this.onChange([this.year, this.month, e.target.value].join('-'))
      this.pristine = false
    }
  }
}
</script>

Nuxt.js でいう Page Component では以下のように呼び出す事ができます。

<BirthdayField
  :value="fields.birthday"
  :errorMessages="errors.birthday"
  :onChange="(birthday) => handleChangeForms({birthday})"
/>

まとめ

今回の例では Validation 部分は Page Component に記述して、子となる Component の処理は極力簡略化しています。Component の作成は CSS が得意な人に専念してもらいたいので責任をここで分離しています。

次回、Form Validation を Page Component から追い出せないかを考えます。

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

【Nuxt.js】MicroCMSで取得したAPI(非同期データー)をコンポーネント内で扱う方法

Nuxt.jsで作るWebサイトをでInfomationを手軽に作るため、heddresscmsであるmicrocmsを利用しようと思いました。

こちらの記事を参考にMicrocmsからのAPIを取得しNuxt内に表示される方法を参考にしました。
https://microcms.io/blog/create-nuxt-microcms-recruit/

まずはTopページに下記のように書きました。

index.vue
<template lang="pug">
省略
    div
      .items
        .item-box(v-for='item in items' :key='item.id')
          .name
            | {{ item.title }}
省略
</template>

<script>
export default {
  data () {
    return {
      items: ''
    }
  },
  async asyncData ({ $axios }) {
    const data = await $axios.$get('https://*******.microcms.io/api/v1/news', {
      headers: { 'X-API-KEY': ''****************************' }
    })
    return {
      items: data.contents
    }
    }
}
</script>

うまく表示されましたので、次にコンポーネント化し使用と思い。
info.veuとしてソースをわけました。

index.vue
<template lang="pug">
  main
    mainvisual
    section
    information

</template>

<script>

import Mainvisual from '~/components/home/Mainvisual.vue'
import Section from '~/components/home/Section01.vue'
import Imformation from '~/components/home/information.vue'

export default {
  components: {
    Mainvisual,
    Section,
    Information
  },
}
</script>
Information.vue
<template lang="pug">
    div
        .items
            .item-box(v-for='item in items' :key='item.id')
                .name
                    | {{ item.title }}

</template>

<script>
export default {
  data () {
    return {
      items: ''
    }
  },
  async asyncData ({ $axios }) {
    const data = await $axios.$get('https://*******.microcms.io/api/v1/news', {
      headers: { 'X-API-KEY': '****************************' }
    })
    return {
      items: data.contents
    }
    }
}

</script>

しかしいざ開いてみると表示されず、エラーも表示されない状況でした。

原因を調べてみますと公式にかいてありました。
コンポーネント内で非同期データを扱うには?

どうやらasyncDataメソッドはページコンポーネントで使えるもので、通常のコンポーネントでは使用できないようです。

しかたないのでmountedに書き換えてみることにします。

index.vue
<template lang="pug">
    div
        .items
            .item-box(v-for='item in items' :key='item.id')
                .name
                    | {{ item.title }}

</template>

<script>
export default {
  data () {
    return {
      items: ''
    }
  },
  mounted() {
    this.asyncData()
  },
  asyncData () {
    await axios.get('https://*******.microcms.io/api/v1/news',{
      headers: { 'X-API-KEY': '****************************' }
    })
    .then(res =>{
      console.log(res)
      this.items= res.data.contents
    })
  }
}
</script>

このように書き直したらうまく表示されました。

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

【Nuxt.js】非同期データーをコンポーネント内で扱う方法

Nuxt.jsで作るWebサイトをでInfomationを手軽に作るため、heddresscmsであるmicrocmsを利用しようと思いました。

こちらの記事を参考にMicrocmsからのAPIを取得しNuxt内に表示される方法を参考にしました。
https://microcms.io/blog/create-nuxt-microcms-recruit/

まずはTopページに下記のように書きました。

index.vue
<template lang="pug">
省略
    div
      .items
        .item-box(v-for='item in items' :key='item.id')
          .name
            | {{ item.title }}
省略
</template>

<script>
export default {
  data () {
    return {
      items: ''
    }
  },
  async asyncData ({ $axios }) {
    const data = await $axios.$get('https://*******.microcms.io/api/v1/news', {
      headers: { 'X-API-KEY': ''****************************' }
    })
    return {
      items: data.contents
    }
    }
}
</script>

うまく表示されましたので、次にコンポーネント化し使用と思い。
info.veuとしてソースをわけました。

index.vue
<template lang="pug">
  main
    mainvisual
    section
    information

</template>

<script>

import Mainvisual from '~/components/home/Mainvisual.vue'
import Section from '~/components/home/Section01.vue'
import Imformation from '~/components/home/information.vue'

export default {
  components: {
    Mainvisual,
    Section,
    Information
  },
}
</script>
Information.vue
<template lang="pug">
    div
        .items
            .item-box(v-for='item in items' :key='item.id')
                .name
                    | {{ item.title }}

</template>

<script>
export default {
  data () {
    return {
      items: ''
    }
  },
  async asyncData ({ $axios }) {
    const data = await $axios.$get('https://*******.microcms.io/api/v1/news', {
      headers: { 'X-API-KEY': '****************************' }
    })
    return {
      items: data.contents
    }
    }
}

</script>

しかしいざ開いてみると表示されず、エラーも表示されない状況でした。

原因を調べてみますと公式にかいてありました。
コンポーネント内で非同期データを扱うには?

どうやらasyncDataメソッドはページコンポーネントで使えるもので、通常のコンポーネントでは使用できないようです。

しかたないのでmountedに書き換えてみることにします。

index.vue
<template lang="pug">
    div
        .items
            .item-box(v-for='item in items' :key='item.id')
                .name
                    | {{ item.title }}

</template>

<script>
export default {
  data () {
    return {
      items: ''
    }
  },
  mounted() {
    this.asyncData()
  },
  asyncData () {
    await axios.get('https://*******.microcms.io/api/v1/news',{
      headers: { 'X-API-KEY': '****************************' }
    })
    .then(res =>{
      console.log(res)
      this.items= res.data.contents
    })
  }
}
</script>

このように書き直したらうまく表示されました。

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

Single Page Application の Form 画面における Best Practice を考える(前編)

あらすじ

Nuxt.js で Form 画面をゴリゴリ作成する案件に少しだけ関わる事になったのですが、少し画面数が多いらしく、複数人のエンジニアがそれぞれ手分けして画面を作成する事になりそうです。さて、そうなると予め関数の設計方法などある程度認識を合わせておかないと、各画面でいろいろな関数が現れてしまい、収拾がつかなくなります。

ところで SPA における Form 画面の作成方法に関する Best Pracitce はどんなものなのでしょうか?残念ながら私は良い感じの文献を発掘できませんでした。

というわけで私が思う Best Practice を考えてみました。例で出てくるコードは Vue.js で書かれていますが、Angular でも React でも応用ができる話になっていると思います。

なお、ここに登場するコードは以下のレポジトリで確認できます。

https://github.com/okamuuu/vue-examples

Form Validation を考える

Form Validation を実装するのは単調で退屈で作業コストがかかる作業です。今回はこの作業を軽減させるために validatorjs を使用します。以下、Form Validation の簡単な使用例です。

const Validator = require('validatorjs')
Validator.useLang('ja')
const rules = { name: 'required' }
const v = new Validator({name: ''}, rules)
const attributeNames = { name: '氏名' }
v.setAttributeNames(attributeNames)
v.passes() // 判定が行われる
console.log(v.errors.errors) // 判定結果: { name: [ '氏名は必須です。' ] }

このように良く使われる Validation rule が最初から登録されていて、かつ、エラーメッセージも最初から用意されています。attributeNames は登録が必要ですがこれは関数化して使いやすくすれば良いでしょう。

カスタムルールを追加する

姓は全角で入力してください, や セイは全角カタカナで入力してください といった日本特有の処理があるのでその場合は以下のようにして rule を追加できます。ちなみに正規表現の書き方ネットで調べたものをコピペしただけなので間違えてたらごめんなさい。

/* eslint-disable no-control-regex */
Validator.register(
  'zenkaku',
  value => /[^\x01-\x7E\xA1-\xDF]+$/.test(value),
  ':attributeは全角で入力してください'
)
Validator.register(
  'zenkakuKana',
  /* eslint-disable-next-line no-irregular-whitespace */
  value => /^[ァ-ヶー ]+$/.test(value),
  ':attributeは全角カナで入力してください'
)

validatorjs を wrap して Vue.js で使いやすくする

というわけで以下のようにしました。birthday に関しては date というルールがあるのですが、transpile するときに解釈が異なるらしくエラーとなるので自前で書いています。

https://github.com/skaterdav85/validatorjs/issues/357

utils/validator.js
import Validator from 'validatorjs'
import { isValid as isValidDate, parseISO } from 'date-fns'

Validator.useLang('ja')

const rules = {
  lastNameKanji: 'required|zenkaku',
  firstNameKanji: 'required|zenkaku',
  lastNameKana: 'required|zenkakuKana',
  firstNameKana: 'required|zenkakuKana',
  birthday: 'required|birthday',
  zipcode: 'required|zipcode'
}

const attributeNames = {
  lastNameKanji: '',
  firstNameKanji: '',
  lastNameKana: 'セイ',
  firstNameKana: 'メイ',
  birthday: '生年月日',
  zipcode: '郵便番号'
}

/* eslint-disable no-control-regex */
Validator.register(
  'zenkaku',
  value => /[^\x01-\x7E\xA1-\xDF]+$/.test(value),
  ':attributeは全角で入力してください'
)
Validator.register(
  'zenkakuKana',
  /* eslint-disable-next-line no-irregular-whitespace */
  value => /^[ァ-ヶー ]+$/.test(value),
  ':attributeは全角カナで入力してください'
)
Validator.register(
  'birthday',
  value => value.length === 10 && isValidDate(parseISO(value)),
  ':attributeを正しく入力してください'
)
Validator.register(
  'zipcode',
  value => /^\d{3}-?\d{4}$/.test(value),
  ':attributeは7桁で入力してください'
)

/* eslint-eable no-control-regex */
export function validate (obj) {
  const v = new Validator(obj, rules)
  v.setAttributeNames(attributeNames)

  if (v.passes()) {
    return null
  }
  return v.errors.errors
}

これを utils/validator.js に記述しておくと、Vue.js で Form 画面を作成する時、以下のように form validation の判定を行う事ができます。

pages/vue-form-validation/example.vue
import { validate } from '~/utils/validator'

export default {
  data () {
    return {
      fields: {},
      errors: {}
    }
  },
  methods: {
    async handleChangeFields (fields) {
      this.fiedls = { ...this.fields, ...fields }
      this.errors = Object.assign({}, validate(this.fields))
    }
  }
}

まとめ

というわけで単調で面倒な作業であるエラーメッセージの定義を簡略化する事ができました。validatorjs は Node.js でも動作するので、この utils をサーバーサイドエンジニアに託す事によってフロントエンドエンジニアが責務を軽減する事もできます。

次回は Component の作り方を考えます。

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

Vue.jsのスタイルガイドを読んだ

先日、神戸三宮を中心に関西を盛り上げているVue.jsコミュニティ、三宮.vueの勉強会に初めて参加して来ました。

そこで、「Vue.jsのスタイルガイドをちゃんと読もう!」という発表に触発され、改めて自分でもスタイルガイドを読んでみました。

読んでみて、「当たり前ではないか!へのつっぱりはいらんですよ!」と思うことから、「へーー!そら知らなんだ!」ということもありましたが、どれも「エラーを防止する」「可読性を上げる」「一貫性を持たせる」「保守性を上げる」という面で大事です。

実際にスタイルガイドを読むのが一番ですが、折角なので優先度Aの必須項目をメモとして書きました。

複数単語コンポーネント名を使う

ルートの App コンポーネントや、Vue が提供する <transition><component> のようなビルトインコンポーネントを除き、コンポーネント名は常に複数単語とするべきです。

example1.vue
Vue.component('todo-item', {
  // ...
})
example2.vue
export default {
  name: 'TodoItem',
  // ...
}

これは、HTML要素の名前とコンポート名が衝突するのを防ぐのが目的です。

全ての HTML 要素は 1 単語となっているので、コンポート名として複数単語を使っているとHTMLの要素名と衝突することはないということです。

例えば todoTodo というコンポート名をつけると将来 todo というHTML要素が出て来たときに名前が被ってしまうので、コンポート名としては example1example2 のように todo-itemTodoItem というような名前にすると良いです。

コンポートのデータは関数でなければならない

コンポーネントで data プロパティを使用する際 (つまり new Vue 以外のどこでも)、その値はオブジェクトを返す関数でなければなりません。

例えば、data の値がオブジェクトの場合、それはコンポーネントの全てのインスタンスで共有されます。

bad-example.vue
export default {
  data: {
    listTitle: '',
    todos: []
  }
}

このように書き、このコンポーネントを再利用しようとした場合、コンポーネントの全てのインスタンスが同じデータオブジェクトを参照するので、1つのリストのタイトルを変えると、他のリストのタイトルも変わってしまいます。

そこで、各コンポーネントのインスタンスには自身のデータだけを管理してもらおうとすると、各インスタンスは一意のデータオブジェクトを生成する必要があります。

good-example1.vue
export default {
  data: function () {
    return {
      listTitle: '',
      todos: []
    }
  }
}

もしくは

good-example.vue
export default {
  data () {
    return {
      listTitle: '',
      todos: []
    }
  }
}

このように、関数内でオブジェクトを返すと、各インスタンスが一意のデータオブジェクトを生成することができます。(※ ルートで直接オブジェクトを使うのは OK です)

プロパティの定義はできる限り詳細とする

コミットされたコード内で、プロパティの定義は常に少なくとも1つのタイプを指定し、できる限り詳細とするべきです。

example.vue
export default {
  props: {
    status: {
      type: String,
      required: true,
    }
  }
}

typerequired を明記することで、ミスを防いだり、エラーの原因の特定をし易くするという話ですね。

v-for に対して常に key を使用する

good-example.vue
<ul>
  <li
    v-for="todo in todos"
    :key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul>

このように一意のキーを振りましょう。
もし、

bad-example.vue
<li
    v-for="(item, index) in items"
    :key="index"
 >

とすると、キーの値が書き換わった場合に意図しない挙動に繋がる可能性があります。そのため、キーにはgood-exampleのように一意のidを設定するのが基本です。(参考: Vue.js: v-forで項目インデックスをkey属性にしていいのか)

v-for と一緒に v-if を使うのを避ける

good-example.vue
<ul v-if="shouldShowUsers">
  <li
    v-for="user in users"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

good-exampleの場合、v-if をコンテナ要素に置いているので、shouldShowUsersfalse の場合は v-for を評価しません。

もしこれを、

bad-example.vue
<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

このように書くと、

v-forは v-if よりも優先度が高いので、このテンプレートは、

this.users.map(function (user) {
  if (user.isActive) {
    return user.name
  }
})

と同様に評価されます。
アクティブユーザーが変更されたかどうかに関わらず、再レンダリングするたびにリスト全体を繰り返し処理する必要があります。

コンポーネントをスコープにする

アプリケーションにとって、トップレベルの App コンポーネントとレイアウトコンポーネント内のスタイルはグローバルかもしれませんが、他のすべてのコンポーネントは常にスコープされているべきです。

good-example.vue
<template>
  <button class="button button-close">X</button>
</template>

<style scoped>
.button {
  border: none;
  border-radius: 2px;
}

.button-close {
  background-color: red;
}
</style>

上の例は stylescoped 属性を使用しています。

これは、スタイルの汚染を防ごうということです。

scoped 属性を使わなくても、BEMなどを用いることで汚染を防ぐ方が良い場合もあります。

プライベートなプロパティ名を使う

プライベートな関数に外部からアクセスできないようにするために、モジュールスコープを使用してください。それが難しい場合は、プラグインやミックスインなどのプライベートなカスタムプロパティには常に$_ プレフィックスを使用してください。さらに、他の著者によるコードとの衝突を避けるため、名前付きのスコープを含めてください (例 $_yourPluginName_ )。

good-example.vue
var myGreatMixin = {
  // ...
  methods: {
    publicMethod() {
      // ...
      myPrivateFunction()
    }
  }
}

function myPrivateFunction() {
  // ...
}

export default myGreatMixin

以上がVue.jsスタイルガイドの中で、優先順位が最も高い必須7項目です。

これまでちゃんと読んだことがなかったのですが、読んだ方が良いですね。

最初に書いたように、当たり前だと思うことも知らなかったことも書いてありますが、使う上での共通認識として理解しておくべきだと感じました。

本記事で紹介している優先度Aの必須7項目以外にも優先度B(強く推奨)、C(推奨)、D(使用注意)の項目があるので、是非スタイルガイドに目を通してみてください。

そう言えば、Nuxtを使ったときにESLintに Require self-closing on Vue.js custom components”:value” should go before “〜”のように言われることがありました。これらも、優先度Bの項目の自己終了形式のコンポーネントコンポーネント名における単語の順番 に則ったルールに基づいているんですね。

知らなんだ。


三宮.vue 楽しかったので、関西にいる方は是非参加してみてください!

参考

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

Vue.jsのテストでwrap越しに子コンポーネントのevent up($emit)をエミュレートする方法

概要

Vue.js(Vue CLI v4系)で、wrap = shallowMount() 越しに次のことをテストする方法について記載する。

  • 子コンポーネント(Component)からevent upされた状態を検証する方法
  • 対象のVueコンポーネントからevent upしたことを検証する方法

内容としては、公式サイトのガイドやAPI仕様書に記載されている通りなのだが、私はそれだけでは「実際にどのようにするのか?」を読み取れなかったので、その観点でのメモが目的。

対象とする被テストのコンポーネント

例として、画像ファイルを登録する前の説明ページを使う。
「ファイルの取り扱い」ダイアログを表示して、
その後に「同意する」チェックボックスを押した場合にのみ、
「画像を登録」ボタンを押せる仕様、とする。

実際の画像登録は、親コンポーネントへemit()して処理するものとし、
被テストのコンポーネントでは取り扱わない。

Explanation.vue
<template>
<div>
    <div id="id_button_confirm_privacy" v-on:click="openModalOfPrivacy">
        <span>【登録された画像ファイルの取り扱いについて】</span>
    </div>
    <!-- コンポーネント ModalConfirmPrivacy -->
    <ModalConfirmPrivacy id="id_modaldialog_pivacy" v-on:close="closeModalOfPrivacy" v-if="isModalDialog">
        <h3 slot="header">登録ファイルの取り扱いについてダイアログ</h3>
        <span slot="body">
            ここに任意のテキストを入れる。<br>
        </span>
    </ModalConfirmPrivacy>
    <br>
    <div id="id_checkbox_agree_privacy" v-on:click="toggleAgree">
        <span id="id_agree2policy" v-show="isAgree">[レ]</span>
        <span id="id_not_agree2policy" v-show="!isAgree">[ _ ]</span>
        <span>「ファイルの取り扱い」に同意する</span>
    </div>
    <br>
    <div id="id_button_select_image" v-on:click="go2Next">
        【画像を登録】
    </div>
</div>
</template>

<script>
// javascriptファイルをココへ配置
import ModalConfirmPrivacy from './ModalConfirmPrivacy.vue';

export default {
    name : "Explanation",
    components : { ModalConfirmPrivacy },
    data : function () {
        return {
            isModalDialog: false,
            isHavingSeen : false,
            isAgree : false
        }
    },
    methods : {
        openModalOfPrivacy : function () {
            this.isModalDialog = true;
        },
        closeModalOfPrivacy : function () {
            this.isModalDialog = false;
            this.isHavingSeen = true;
        },
        toggleAgree : function () {
            this.isAgree = (this.isHavingSeen) ? !this.isAgree : false;
        },
        go2Next : function () {
            if(this.isAgree){
                this.$emit('go2SelectImage')
            }
        },
    }
}
</script>

<style scoped>
/* Cssファイルはここへ配置する。 */
#id_button_confirm_privacy {
    cursor: pointer;
    text-decoration: underline;
}
#id_button_select_image {
    cursor: pointer;
    text-decoration: underline;
}
</style>

以下の環境で動作検証

    "vue": "^2.6.11"

    "@vue/cli-plugin-babel": "~4.2.0",
    "@vue/cli-plugin-eslint": "~4.2.0",
    "@vue/cli-plugin-unit-mocha": "~4.2.0",
    "@vue/cli-service": "~4.2.0",
    "@vue/test-utils": "^1.0.0-beta.29",
    "babel-eslint": "^10.0.3",
    "chai": "^4.1.2",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.1.2",
    "vue-template-compiler": "^2.6.11"

※なお、vue create でインストールされる
@vue/test-utils@1.0.0-beta.31 だとtrigger() でDOMが更新されないように見える。
@vue/test-utils@1.0.0-beta.29 だと問題ない。
なので、vue create した後で、npm install @vue/test-utils@1.0.0-beta.29 --save-dev にて上書きしてダウングレードインストールして動作検証した。

子コンポーネントからのevent upを、スタブを用いてエミュレートする

Explanation.vueをshallowMount()でwrapper生成して、
コンポーネントModalConfirmPrivacyの close イベントに紐づけられた
closeModalOfPrivacy() が呼ばれた状態をエミュレートするには、
次にようにする。

  1. shallowMount() に stabs オプションを指定して、「closeイベントを$emitするノードを持ったスタブ」を設定する
  2. スタブ内のノードを trigger()して $emitを経由してcloseイベントを発火する

これにより、closeModalOfPrivacy() を直に呼び出すのではなく、
あくまでも close イベントに紐づけられたソレを呼び出した状態、を検証できる。

具体的なテストコードの例は以下となる。

Explanation.spec.js
import { shallowMount } from '@vue/test-utils';
import { expect } from 'chai';
import targetVue from '@/components/Explanation';

describe('Explanation.vue', () => {
  const factory = (propsData)=>{
    return shallowMount(targetVue,{
      stubs: {
        ...propsData
      }
    });
  };

  describe('画像ファイルの取り扱いへ同意のチェックボックス', ()=>{
    it('ダイアログModalConfirmPrivacyでcloseModalOfPrivacy()が呼ばれた後は、id_checkbox_agree_privacyをクリックしてid_agree2policyが表示(チェック)へ変化する', () => {
      const wrapper = factory({
        ModalConfirmPrivacy : '<div><span id="_stub_id_modal_close" @click="$emit(\'close\')">closeボタンだけを持ったモーダルダイアログへスタブ化</span></div>'
      });

      // チェックボックス(相当)の初期状態を検証
      expect(wrapper.find('#id_agree2policy').isVisible()).to.be.false;
      expect(wrapper.find('#id_not_agree2policy').isVisible()).to.be.true;

      // ダイアログを開いて閉じる。
      wrapper.find('#id_button_confirm_privacy').trigger('click');
      wrapper.find('#_stub_id_modal_close').trigger('click'); // ↑をclickしてからでないと、これは取れない。

      // チェックボックス(相当)をクリック
      wrapper.find('#id_checkbox_agree_privacy').trigger('click');

      // チェックボックス(相当)が変更されたことを検証
      expect(wrapper.find('#id_agree2policy').isVisible()).to.be.true;
      expect(wrapper.find('#id_not_agree2policy').isVisible()).to.be.false;
    });
  });
});

被テスト対象のVueコンポーネントで $emit() されたことを検証する方法

コンポーネント内で「this.$emit('発火するイベント名称') が呼ばれたか?」
を検証するには emitted() を用いる。

具体的なテストコードの例は以下となる。

Explanation.spec.js
import { shallowMount } from '@vue/test-utils';
import { expect } from 'chai';
import targetVue from '@/components/Explanation';

describe('Explanation.vue', () => {
  const factory = (propsData)=>{
    return shallowMount(targetVue,{
      stubs: {
        ...propsData
      }
    });
  };


  describe('画像ファイル登録ボタン', ()=>{
    it('id_agree2policyが表示(チェック済み)ならば、親コンポーネントのgo2SelectImage()が呼ばれること', () => {
      const wrapper = factory({
        ModalConfirmPrivacy : '<div><span id="_stub_id_modal_close" @click="$emit(\'close\')">closeボタンだけを持ったモーダルダイアログへスタブ化</span></div>'
      });

      // ダイアログを開いて閉じる。
      wrapper.find('#id_button_confirm_privacy').trigger('click');
      wrapper.find('#_stub_id_modal_close').trigger('click'); // ↑をclickしてからでないと、これは取れない。
      // チェックボックス(相当)をクリック
      wrapper.find('#id_checkbox_agree_privacy').trigger('click');

      // 画像登録ボタンをクリック
      wrapper.find('#id_button_select_image').trigger('click');

      // 親コンポーネントのgo2SelectImage()を呼び出したことを検証
      // ※引数がある場合は、キー(例:'go2SelectImage')のバリューとして配列で入ってくる。
      //   今回の事例では引数無しなので省略。
      expect(wrapper.emitted()).has.property('go2SelectImage');
    });
  });
});

以上ー。

参考サイト

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

Vue.jsでコンポーネント更新後に要素をスクロール(TypeScript)

結論

・nextTickで更新を待ってから位置を取得する

失敗するコード

この書き方だと、コンポーネントの切り替えが間に合わず、取得位置が切り替え前の位置になる場合があり、思った位置にスクロールしてくれない

// 表示中のコンポーネントの切り替えなど
this.switchComponent(); 

// 上端を基準とする位置を取得
const selectedElement: Element = document.getElementsByClassName('className')[0];
const elementPosition: number = selectedElement.getBoundingClientRect().top;
const currentPosition: number = window.pageYOffset;
const targetPosition: number = elementPosition + currentPosition;

// スクロール
window.scrollTo({
    top: targetPosition,
    behavior: 'smooth',
});

成功するコード

this.$nextTickでコンポーネントの更新を待つことで、切り替え後の位置を正確に取得出来、思った通りの位置にスクロールされる

// 表示中のコンポーネントの切り替えなど
this.switchComponent(); 

this.$nextTick(() => {
    // 上端を基準とする位置を取得
    const selectedElement: Element = document.getElementsByClassName('className')[0];
    const elementPosition: number = selectedElement.getBoundingClientRect().top;
    const currentPosition: number = window.pageYOffset;
    onst targetPosition: number = elementPosition + currentPosition;

    // スクロール
    window.scrollTo({
        top: targetPosition,
        behavior: 'smooth',
    });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む