Vue.js始めるおれおれアドベントカレンダー2016 – 16日目分(24日公開)

しゅんしゅん動くよ。

アニメーション付きのナビバーを作ってみました、簡単でした。やったね。

基本的な作り方

  • location.hash を監視して情報更新
  • hashに該当する内容を表示
  • バー位置をhashの候補の順序から算出

簡単に作れたは良いんだけど、最後のやつどうしようかなと。

バー位置をhashの候補の順序から算出

最初に書いたコードはこう。

<nav>
  <a href="#">Home</a>
  <a href="#about">About</a>
  <a href="#contact">Contact</a>
  <span :style="underlineStyle"></span>
</nav>
const store = require('./store.js')

const hashes = ['', '#about', '#contact']

module.exports = {
  data () {
    return store.state
  },
  computed: {
    underlineStyle () {
      const itemWidth = 100
      const left = itemWidth * hashes.indexOf(this.hash)
      return {
        transform: 'translateX(' + left + 'px)'
      }
    }
  }
}

これで全然動くんだけど、疑問点が二つ。

  • 候補値 '', '#about', '#contact' をHTML側と共有しているの、どうにかならんかな
  • 項目の幅 100 をCSS側と共有しているの、どうにかならんかな

jQueryであれば実際の要素を見てあれこれするんだけど、Vueはそうはしないじゃないすか。普通。

$refs を使う?

とか言いつつ要素を見てあれこれするやつ、使ったらできるにはできた。

<nav ref="list">
  <a href="#">Home</a>
  <a href="#about">About</a>
  <a href="#contact">Contact</a>
  <span :style="underlineStyle"></span>
</nav>
computed: {
  /**
   * 項目の幅の実測値を返す。
   */
  itemWidth () {
    const elList = this.$refs.list
    const elItem = elList.firstElementChild
    return elItem.clientWidth
  },

  /**
   * 項目の `href` からhash候補値を得る。
   */
  hashes () {
    const elList = this.$refs.list
    const elItems = elList.children
    const hashes = Array.from(elItems).map(elItem => {
      let hash = elItem.getAttribute('href')
      if (hash === '#') {
        hash = ''
      }
      return hash
    })
    return hashes
  },

  underlineStyle () {
    let left

    // 最初のDOM構築の際には当然underlineStyleは呼ばれるが、
    // 最初だからDOMがまだないので、 `$refs` が使えない。
    if (this.$refs.list) {
      left = this.itemWidth * this.hashes.indexOf(this.hash)
    } else {
      // `hash` 変更時にキャッシュ値を更新するよう、
      // ここで呼んで記憶してもらう
      left = this.hash.length * 0
    }

    return {
      transform: 'translateX(' + left + 'px)'
    }
  }
},

うわあ、すごく危険な香りがする。

実際公式ガイドにもやめてねって書いてあるし。

$refs はコンポーネントが描画された後にのみ追加されます。そしてそれはリアクティブではありません。直接子コンポーネントを操作するための最終手段としての意味しかありません。テンプレートまたは算出プロパティの中での $refs の使用は避けるべきです。

うん。

テンプレートまたは算出プロパティの中での $refs の使用は避けるべきです。

そう思います。

マウントのタイミングで確認したい……

……マウント……ライフサイクル……ん?

module.exports = {
  data () {
    return {
      itemWidth: 999,
      hashes: [],
      state: store.state
    }
  },
  mounted () {
    const elList = this.$refs.list
    const elItems = elList.children

    this.itemWidth = elItems[0].clientWidth

    this.hashes = Array.from(elItems).map(elItem => {
      let hash = elItem.getAttribute('href')
      if (hash === '#') {
        hash = ''
      }
      return hash
    })
  },
  computed: {
    underlineStyle () {
      const left = this.itemWidth * this.hashes.indexOf(this.state.hash)
      return {
        transform: 'translateX(' + left + 'px)'
      }
    }
  }
}

なるほど、こういう感じか。これなら良さそう。