JSやjQueryで実装していたスムーススクロール機能をVue.js/Nuxt.jsで実現するのに苦戦していませんか?
ライブラリを使うことでスムーススクロールの実装自体は容易です。というよりは一瞬でできます。
しかし、URLにアンカーが付かない(/#element)という大きな問題がありました。いくつかのライブラリを導入してみましたがいずれも対応されておらず、それではユーザビリティを損ねるということで改善策を検討しました。
Nuxt.jsを使うことでモダンに開発できるのはいいことですが、昔から重宝されていた(当たり前とされていた)機能が実現できないということが往々にして発生しがちですので、そこを如何にクリアにしていくかが肝となります。
昔からよく使われている実装例
$(function(){
$('a[href^="#"]').click(function(){
var speed = 500;
var href= $(this).attr("href");
var target = $(href == "#" || href == "" ? 'html' : href);
var position = target.offset().top;
$("html, body").animate({scrollTop:position}, speed, "swing");
return false;
});
});
簡単ですね。これだけで <a href="#element">見出し1</a>
といったページ内リンクでのスムーススクロールが実現できています。
なお、見出し1のアンカーテキストをクリックするとURLは http://example.com/#element
に変化しますので飛び先の状態で第三者にURLを共有することができます。
Vue.js/Nuxt.jsにおけるスムーススクロール
前述のコードはjQueryを使用していますが、Vue.jsやNuxt.jsではそもそもjQueryを使わないようにしているケースが多いと思われます。
Vue.jsでスムーススクロールするためのライブラリがいくつもありますので、基本的にはそのライブラリにお世話になっている方が多いのではないでしょうか。
例
問題点:URLが変化しない
<a href="#" v-scroll-to="'#element'">見出し1</a>
...
<h2 id="element">
</h2>
この状態で見出し1をクリックすると確かにスムーススクロールが実現できることが分かります。
しかし、URLに#element
が付与されないため、遷移先の状態でURLを人に共有することができません(http://example.com/
のまま)。
これは利用者のユーザビリティを大きく損ねることに繋がりますので避けたい事象です。
とは言ってもイチからライブラリの開発をするのもな...ということで検討した結果、以下のような拡張プラグインを作成しました。
※内部的にvue-scrolltoを使用しています。
解決策
vue-scrolltoは活用させていただき、それをwrapperさせたプラグインを作成しました。
やっていることは単純で、クリックイベントを止めて直接vue-scrolltoの内部関数をコールするようにしていること、カスタムディレクティブの場合も同様でクリックイベントをリッスンして直接vue-scrolltoを実行しています。
公開用ではないため冗長な記述もありますのでご容赦ください。
そのままでも使えると思いますので、興味がありましたら使ってみて下さい。
Nuxt.jsをよりユーザーフレンドリーにするための一助となれば幸いです。
コンポーネント
使いやすくするため、カスタムディレクティブにも対応しています。
<a v-smooth-scroll="{duration: 300}" href="#element" class="">some text</a>
↑のような書き方でもOK。
ちなみに、どのような場合であっても<nuxt-link>
では機能しませんので<a>
タグをご使用ください。
<a href="#element" @click.stop="$smoothScroll.scrollTo($event.srcElement.hash, 300)">some text</a>
また、細かな制御をしたいとき用に、Programmaticallyな書き方もできます。
scrollTo (e): void {
this.$smoothScroll.scrollTo(e.srcElement.hash, 300)
}
プラグイン(plugins/smooth-scroll.ts)
import VueScrollTo, { ScrollOptions } from 'vue-scrollto'
import { Plugin } from '@nuxt/types'
import Vue, { VNode } from 'vue'
import { DirectiveBinding } from 'vue/types/options'
type ElementDescriptor = Element | string
interface VueSmoothScrollHTMLElement extends HTMLElement {
vueSmoothScrollDuration?: number,
vueSmoothScrollOptions?: ScrollOptions
}
export interface ScrollToInterface {
(hash: ElementDescriptor, duration: number, options?: ScrollOptions): void
}
export interface SmoothScrollInterface {
scrollTo: ScrollToInterface
}
/**
* クリックイベント発火時にコールされる関数
* @param event From AddEventListener
*/
function scrollToFunction (event: any): void {
VueScrollTo.scrollTo(
event.srcElement.hash,
event.target.vueSmoothScrollDuration,
event.target.vueSmoothScrollOptions
)
}
const smoothScroll: Plugin = (_context, inject) => {
const scrollTo: ScrollToInterface = (hash, duration, options): void => {
VueScrollTo.scrollTo(hash, duration, options)
}
const smoothScroll: SmoothScrollInterface = {
scrollTo
}
inject('smoothScroll', smoothScroll)
Vue.directive('smooth-scroll', {
bind: (el: VueSmoothScrollHTMLElement, binding: DirectiveBinding, _vnode: VNode, _oldVnode: VNode): void => {
el.addEventListener('click', scrollToFunction, false)
el.vueSmoothScrollDuration = binding.value.duration
el.vueSmoothScrollOptions = binding.value.options
},
unbind: (el: VueSmoothScrollHTMLElement, _binding: DirectiveBinding, _vnode: VNode, _oldVnode: VNode): void => {
el.removeEventListener('click', scrollToFunction, false)
}
})
}
export default smoothScroll
nuxt.config.js
plugins: [
...
{ src: '@/plugins/smooth-scroll.ts' }
],
...
modules: [
...
['vue-scrollto/nuxt', { duration: 300 }]
]