DEV Community

tq-bit
tq-bit

Posted on • Updated on

Let's build Vue directives: Scrolling elements into view

TL: DR - take me to the code

1. Vue 2 Code Sandbox

2. Vue 3 Code Sandbox

3. Difference between Vue 2 and Vue 3 implementation

It is one of those features that slowly but steadily found its way into most of the content-heavy pages, such as blogs or documentations. Scrolling elements into view gives your user an intuitive shortcut to the content they'd like to read about.

Why a Vue directive?

You might wonder - why would I need Javascript, even more, a Vue directive for this? I can use built-in HTML and CSS properties, such as href="#headline" in combination with scroll: smooth.

In fact, you can. If you don't mind a few limitations:

  • A fixed navbar might be in the way of your heading
  • You have to give every heading a fixed id or create it with Javascript
  • Globally applied smooth scrolling sometimes leads to some funny side effects when using SPA routers

You could arguably also use vanilla Javascript. Then again, the nature of Vue directives allows for a quick and reusable implementation.

Please note that this article focuses on Vue 3, in which custom directives are handled slightly different than in Vue 2. Check out the linked code sandboxes in TL:DR for differences in main.js and the vScroll.js directive file.

Setup a Vue 3 Project with Vite

We'll use Vite to spin up a basic application. You can alternatively use Vue CLI.

Change into a directory of your choice and type:

# 1: Init your project
npm init vite@latest # using Vite with npm
# yarn create vite   # using Vite with yarn
# vue create .       # using Vue CLI

# 2: Change into the created folder and start the dev server
cd vite-project
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Make the app object available for directive registration

Before registering our custom directives, let's make a small adjustment in Vue's main file. It uses createApp on the fly, but we need the created app object to register components on.

This step is optional, you could also chain .directive() as part of the createApp bootstrapping process.

// Inside main.js: Change this 
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

// ------

// to 
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// ... register directives here

app.mount('#app')
Enter fullscreen mode Exit fullscreen mode

With app.directive(directiveName, directiveFunction), we're now able to register our own directives everywhere in the app.

Create the scroll directive

For this purpose, we require two browser functions, both of which are available in all major browsers.

  • domElement.getBoundingClientRect() to receive the element's coordinates
  • window.scroll() to scroll to the calculated part of the window

What's left for us to do is to:

  • calculate the difference between the current window position and the element (1)
  • make the window scroll to that calculated position (2)

So let's move ahead and create a folder in the /src folder named /directives. Inside it, create a file named vScroll.js and paste the following code into it:

const vScroll = {
  mounted: (el) => {
    el.style.cursor = 'pointer';
    el.addEventListener('click', () => {
      const coord = el.getBoundingClientRect().top + window.scrollY; //(1)
      window.scroll({ top: coord, behavior: 'smooth' });  //(2)
    });
  },
};

export default vScroll;
Enter fullscreen mode Exit fullscreen mode

Now let's go back into the main.js file and register vScroll:

import { createApp } from 'vue'
import App from './App.vue'
import vScroll from './directives/vScroll'

const app = createApp(App)

app.directive('scroll', vScroll)

app.mount('#app')
Enter fullscreen mode Exit fullscreen mode

Use the directive on the template

Now what's left to do is to check whether the directive works as intended. Let's go ahead and replace the content of the App.vue file with a Lorem Ipsum template.

Try adding v-scroll to any of the h2 - tags. Now, when clicking on them, they will be smoothly scrolled to the upper part of the window.

<h2 v-scroll>Cursus mattis</h2>
Enter fullscreen mode Exit fullscreen mode

What about the fixed navbar problem?

The solution in a nutshell: You can bind values to directives! In this case to add a vertical offset when scrolling to an element. These bindings can be accessed the same way as the element itself, given they are passed as an argument in the directive's lifecycle function. In our case, binding.value will resolve to a number we want to subtract from the scroll-top position.

In the vScroll.js file:

const vScroll = {
  mounted: (el, binding) => {
    el.style.cursor = 'pointer';
    el.addEventListener('click', () => {
      let coord = 0;
      coord = binding.value
        ? el.getBoundingClientRect().top + window.scrollY - binding.value
        : el.getBoundingClientRect().top + window.scrollY;
        window.scroll({ top: coord, behavior: 'smooth' });
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

In the HTML template:

<h2 v-scroll="200">
  Pharetra magna
</h2>
<p>
  Congue quisque egestas diam in arcu cursus euismod quis viverra. 
  Pharetra magna ac placerat vestibulum lectus mauris ultrices.....
</p>
Enter fullscreen mode Exit fullscreen mode

If everything went well, you now have a functional directive you can include in any new or existing project.

There's more to it

As you can see, we achieved this functionality fairly easily. You can move even further and extend the directive. For example, dynamically add an icon before the text or mutate the element's style whenever a user interacts with it. If you'd like to get an even deeper understanding of Vue directives, make sure to take a look into the official docs for Vue 3.

https://v3.vuejs.org/guide/custom-directive.html

This post was originally published at https://blog.q-bit.me/lets-build-vue-directives-scrolling-elements-into-view/
Thank you for reading. If you enjoyed this article, let's stay in touch on Twitter 🐤 @qbitme

Discussion (0)