vue.jsでid取得時にnullになる
Creative Member WEBチーム
どうも、コーダーを担当しているMです。
vue.jsを用いてサイトを作成した際、つまづいた箇所がタイトルの話になります。
事の始まりは「ページ内リンク」の実装時でした。
私はvue-cliという支援ツールを用いてサイト作成を進めていたのですが、
ページ内リンクを設置するときhtmlと同じようにhrefにハッシュ('#')をつけても動作せず困っていました。
クリックした箇所の座標を取得するため、あらかじめクリックする要素にidを付けておいたのですがその時に違和感が。
作業し初めだった時の動作の流れとしましては、クリック時にmethodで定義しておいたgetElementByIdでタグのidを取得し、
クリックした要素の先頭位置へスクロールさせる、というものでした。
これで動作確認すると何故か何も起こらず、ブラウザリロード後クリックしてみたら今度はちゃんと動いたり。
見た目だけでは原因が判らなかったのでconsole.logでidの値が入っているか確認してみたら変数の中身がundefinedになっておりました・・・。
5回に1~2回はundefinedになるときがあります。
methodでidを取得する動作を行うのが駄目ならcreatedの段階でidを取得することを試みたのですが、それでもidが取得できず。。
どうしてなのかフォーラム等での調べによると、
「createdはVueのインスタンスが生成された段階で実行されるハンドラです。」
「documentのルートから辿ったところにidの要素があることになりますが、createdの段階ではまだこれは保証されていません。」
とのことでした。
ということで更にmountedでidを取得を試したところ、mountedだとDOMの作成が完了しているので取得は可能だとわかりました。
が、そもそもクリックしたタグのidをmethod内で取得するよう記述しても安定しないのでidを取得できていても照合ができません・・・。
そこで以下のコードを作成しました。
[template側]
<p id="toggle01" v-bind:class="{'is-open': toggleitems[0]}" @click="accordionToggle(0)">クリック要素</p> <div v-show="toggleitems[0]"> アコーディオン中身 </div> <p id="toggle02" v-bind:class="{'is-open': toggleitems[1]}" @click="accordionToggle(1)">クリック要素</p> <div v-show="toggleitems[1]"> アコーディオン中身 </div> <p id="toggle03" v-bind:class="{'is-open': toggleitems[2]}" @click="accordionToggle(2)">クリック要素</p> <div v-show="toggleitems[2]"> アコーディオン中身 </div>
クリック時にmethod「accordionToggle」を呼びます。
v-bindでclass「is-open」を付与されるようにし、コンテンツ内容が表示されるようにします。
ここでは説明を省いておりますがアコーディオンを実装するためにtransitionタグを挟み、
コンテンツ内容の縦幅を設定する必要があります。
[script側]
export default { data: function () { return { show: false, isNum: '', id: [], toggleitems: [false, false, false, false, false, false], } }, mounted: function () { for (var i = 0; i < 4; i++) { var idNum = 'toggle0' + i this.id[i] = document.getElementById(idNum) } }, methods: { accordionToggle: function (index) { var self = this var targetEl = this.id[index + 1] var result = new Promise(function (resolve, reject) { for (var i = 0; i <= 4; i++) { if (i === index && self.toggleitems[i] === false) { self.toggleitems[i] = true } else { self.toggleitems[i] = false } } resolve() }) result.then(function (value) { // クリックした要素の位置へスクロールする setTimeout(() => { var rect = targetEl.getBoundingClientRect() targetEl = rect.top + window.pageYOffset + -100 scrollTo({ top: targetEl, left: 0, behavior: 'smooth' }) }, 1000) }) } } }
注目する箇所はmounted時です。
まず、dataプロパティで空配列を用意しておきます。
取得するべきid名と取得数はわかっているのでDOMが生成された段階で全て取得し、配列に格納します。
これならクリック時にidを取得する必要がなく、undefinedになりません。
mounted時にidが全て取得完了状態になっているので照合材料が用意できたわけです。
しかしクリック時にgetElementByIdによるid取得が安定しないことがわかっているので、何番目をクリックしたのかだけを取得します。
index番号を引数に設定し、その番号を文字列連結でid名に形成します。
「指定されたindex番号でかつ、その番号に対応している領域が閉じている(false)状態」であれば、
他の箇所は閉じつつ、クリックした箇所を表示します。
アコーディオンはコンテンツ内容が長くなると、スクロールせずその場で開く動作が行われるので、自分のクリックしていた位置からコンテンツ内容の表示位置が大きくズレますが、
「id取得後対応したコンテンツ表示→コンテンツ表示の先頭位置にスクロール」という動作を順番に処理してくれるようPromise関数で実現しています。
これでページ内リンクが上手く機能するようになりました。
htmlのページ内リンクと比べると相当行わなければいけない処理が多いですね。
template内にhtmlを書くことができるからといっても、通常のコーディングと同じ感覚で進めると思わぬところで落とし穴があるので、
vue.jsの時は気をつけなければならないと思いました。