山田 典明 | @noliaki

昭和57年9月28日生 AB型 | 37歳

ActionScript3生まれ jQuery育ち

$なヤツはだいたい友達

$なヤツとだいたい同じ

Webサイトで使える

(Vueの)小テク

2020.1.25.Sat

目次

  1. アコーディオンというような呼ばれ方をするUI

  2. モーダル

  3. まとめ

アコーディオンというような呼ばれ方をするUI

どんなUI?

jQueryの

$('.hoge').slideUp()
// or
$('.hoge').slideDown()

で、OK

ですが

  • 最近、NuxtでWebサイト作ってる

  • データは外部で管理してて、データ取得後、DOM構築までしなきゃならないので、Vueを使ってる

  • たくさんあるので、Vueを使ってる

Vueでアニメーション

といえば、

<transition>

  • transitionするプロパティはheight

  • 「auto → 任意の高さ」へのtransitionは出来ないので、一度、数値の高さへ変更後 任意の高さへtransitionする
    [enter] 0 → scrollHeight
    [leave] scrollHeight → 0

<li class="posts-item" :class="{ '-active': isShownBody }">
  <button
    type="button"
    class="posts-item-btn"
    @click.prevent="isShownBody = !isShownBody"
  >{{ post.title }}</button>
  <transition
    name="post-body"
    @enter="enter"
    @afterEnter="afterTransition"
    @leave="leave"
    @afterLeave="afterTransition"
  >
    <div v-show="isShownBody">
      <div class="posts-item-body">{{ post.body }}</div>
    </div>
  </transition>
</li>
Vue.component('template-posts-item', {
  //  ~~~~~~
  methods: {
    enter(el: HTMLElement): void {
      // 中身がはみ出して表示されてないようにする
      el.style.overflow = 'hidden'
      
      // 最初の高さは0
      el.style.height = '0px'
      
      // 最終的な高さを代入
      el.style.height = `${el.scrollHeight}px`
    },
    leave(el: HTMLElement): void {
      // 中身がはみ出して表示されてないようにする
      el.style.overflow = 'hidden'
      
      // 最初の高さはコンテンツの高さ分
      el.style.height = `${el.scrollHeight}px`
      
      // 最終的に高さの0を代入
      el.style.height = '0px'
    },
    afterTransition(el: HTMLElement): void {
      el.style.overflow = ''
      el.style.height = ''
    }
  }
})
.post-body-enter-active,
.post-body-leave-active {
  -webkit-transition: height 400ms ease;
  transition: height 400ms ease;
}

CSSはこんな感じ

enterは上手く

いってるけど、

leaveが上手くいってない

なぜ🤔?

JSの実行 ≠ 視覚効果反映

Vue.component('template-posts-item', {
  //  ~~~~~~
  methods: {
    enter(el: HTMLElement): void {
      // 中身がはみ出して表示されてないようにする
      el.style.overflow = 'hidden'
      
      // 最初の高さは0
      el.style.height = '0px'
      
      // 最終的な高さを代入
      el.style.height = `${el.scrollHeight}px`
    },
    leave(el: HTMLElement): void {
      // 中身がはみ出して表示されてないようにする
      el.style.overflow = 'hidden'
      
      // 最初の高さはコンテンツの高さ分
      el.style.height = `${el.scrollHeight}px`
      
      // 最終的に高さの0を代入
      el.style.height = '0px'
    },
    afterTransition(el: HTMLElement): void {
      el.style.overflow = ''
      el.style.height = ''
    }
  }
})

強制レイアウト

強制レイアウト

初期状態の次のフレームで

最終的な高さを代入したい

Vue.component('template-posts-item', {
  //  ~~~~~~
  methods: {
    enter(el: HTMLElement): void {
      // 中身がはみ出して表示されてないようにする
      el.style.overflow = 'hidden'
      
      // 最初の高さは0
      el.style.height = '0px'
      
      // 最終的な高さを代入
      el.style.height = `${el.scrollHeight}px`
    },
    leave(el: HTMLElement): void {
      // 中身がはみ出して表示されてないようにする
      el.style.overflow = 'hidden'
      
      // 最初の高さはコンテンツの高さ分
      el.style.height = `${el.scrollHeight}px`
      
      const scrollHeight: number = el.scrollHeight
      
      // 最終的に高さの0を代入
      el.style.height = `${scrollHeight - scrollHeight}px`
    },
    afterTransition(el: HTMLElement): void {
      el.style.overflow = ''
      el.style.height = ''
    }
  }
})

ちょっと変更

🎉出来た🎉

でも、強制レイアウトってブラウザに

優しくないじゃん??

他の方法を模索する

requestAnimationFrameの中のrequestAnimationFrameは

次のフレームで実行されるんだよ🤗

function nextFrame(cb: () => void): void {
  window.requestAnimationFrame(() =>
    window.requestAnimationFrame(cb)
  )
}
Vue.component('template-posts-item', {
  //  ~~~~~~
  methods: {
    enter(el: HTMLElement): void {
      el.style.overflow = 'hidden'
      const height: number = el.scrollHeight
      el.style.height = '0'

      nextFrame((): void => {
        el.style.height = `${height}px`
      })
    },
    leave(el: HTMLElement): void {
      el.style.overflow = 'hidden'
      el.style.height = `${el.scrollHeight}px`

      nextFrame((): void => {
        el.style.height = '0px'
      })
    },
    afterTransition(el: HTMLElement): void {
      el.style.overflow = ''
      el.style.height = ''
    }
  }
})

問題なし!

簡単なまとめ

  • Vueに限らず、JSでスタイルを変更した後になにか処理をしたい時はnextFrameを使うのが吉

  • フレームとかを意識した実装をすると、アニメーションとか処理が軽快になりそう

  • beforeEnterとかbeforeLeaveとか$nextTickとか 色々試しましたが、うまくいきませんでした。

vue-nextでは??

(vue3)

モーダル

モーダルでやりたいこと

  • ブラウザの表示領域の上下左右中央に配置したい

  • モーダルが表示されてるときはページ自体のスクロールはしないようにしたい

<transition name="modal" @after-leave="afterLeave">
  <div class="modal" v-show="isShownModal">
    <div class="modal-bg" @click.prevent="hideModal"></div>
    <div class="modal-content" v-if="target">
      <transition :name="detailTransitionName" mode="out-in">
        <div class="detail" :key="target.id">
          <div class="detail-thumbnail">
            <img :src="target.thumbnailUrl" :alt="target.title">
          </div>
          <p>{{ target.title }}</p>
        </div>
      </transition>
      <button
        type="button"
        class="detail-prev"
        @click.prevent="onClickPrev"></button>
      <button
        type="button"
        class="detail-next"
        @click.prevent="onClickNext"></button>
    </div>
  </div>
</transition>
.modal {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  overflow: auto;
  padding: 1.5rem;
}

.modal-bg {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: rgba(0,0,0,0.8);
}

.modal-content {
  position: relative;
  margin: auto;
  width: 40%;
}

modalに

justify-content,

align-itemsを

centerにすると…

ページ自体のスクロールを

やめる

html.-clipped {
  overflow: hidden;
}
const htmlEl: HTMLHtmlElement =
  document.querySelector('html')
const clippedClassName: string = '-clipped'
// ~~~~~~~~~~~~~~~~~~
htmlEl.classList.add(clippedClassName)

これでスクロールができなくなるが、スクロールバーが無くなるので、ガクッとなってしまう。

もう一つ処理を追加

if (document.documentElement.clientHeight < bodyEl.scrollHeight) {
  bodyEl.style.paddingRight = `${scrollBarWidth}px`
}

Bootstrapを参考

全体まとめ

  • ありふれたUIで決まりきった実装をしていた部分を改めて実装したりすると、考える必要のあることや知らなかったことを発見したりできる

  • ライブラリなどに頼るのも解決策だが、かゆいところに手が届かないことが少なくないので、どのような実装になってるのか調べてみるのでも新しい発見がある

🙇‍♂️ありがとうございました🙇‍♂️

deck

By noliaki

deck

  • 1,286