Web

A horizontal chooser component for Vue.js

As an exercise in writing a simple single-file reusable component in Vue, I made a scrolling item chooser. The idea is that you pass it an array of values as a prop, and it displays as many of them as it can fit. If the desired item is outside the visible range, you grab the bar and drag it. It uses mouse events (beyond clicks), so in its current state it’s useless on mobile.

Here is a small embedded Vue.js app to demonstrate the component, which is awkwardly named ChooserHorizScroll (because it’s first a chooser, but it’s also horizontal, and if necessary, it scrolls):

Warning: file /vue/chooser-horiz-scroll could not be included.

Information flow: props down; events up

This small component uses Vue’s “props down; events up” mechanism of information flow, so that it makes no assumptions about the project, including whether or not it uses Vuex (let alone what actions or mutations might be available in the Vuex store).

In this case the component can emit two possible events; one to indicate a click event on one of the values in the chooser, and one to indicate a mousedown on a value (without a corresponding mouseup within that value’s element). The latter allows for a temporary value to be “dragged” to another component in the app, where a mouseup could be used to trigger some action.

Following that idea, for this post, the ChooserHorizScroll component is wrapped in a parent app that displays the most recently-selected value, the previously-selected value, and the current temporary value, if any.

Animation: window.requestAnimationFrame() and CSS translate()

CSS translate() determines the position of the “scrolling” element at a given time, and I gave it “physics,” using window.requestAnimationFrame().

Step size works fine as an analog for speed. I first wrote the frame update code using timestamps to calculate velocities. This also worked, but was unnecessarily complicated to write and significantly less performant. I still need to remind myself that not everything needs to be a literal physical simulation (see also my attempt to “simulate” a still fluid in Blender).

Identifying elements uniquely in different component instances

Implementing limits for the moving element ("chooser-horiz-scroll") animation requires knowing its width, as well as that of the containing element ("horiz-chooser-frame"). I use HTMLElement.offsetWidth to get these values at the mounted Vue lifecycle hook. It may make sense to also add a listener for window resize events, but instead the component simply checks the widths when the user grabs the scrolling element. Multiple instances of this component in an app (which could be reasonable for such a generic component) would result in multiple "chooser-horiz-scroll" elements, so if I want to use document.getElementById(), I need to make some unique (within the app) element ids.

I used a solution proposed by GitHub user Rouven Bühlmann (nirazul): at the beforeCreate hook, a new uuid number is created for the component instance. This number is used to construct unique element ids so that document.getElementById() can find the intended elements.

Responding to events on a parent component

When the app is embedded in another page using <iframe> or <object>, as it is in this post, the mouse may wander outside the document object that has all the app’s event listeners on it. This is a particular problem in Chrome and Safari when the "chooser-horiz-scroll" element has been grabbed but has not yet been released by a mouseup by the time the pointer leaves the document (Firefox dealt with this more gracefully).

In this case, to ensure the component behaves as expected, all that’s needed is to listen for the mouse to leave the demo app’s root element and have the component call its own method to release the "chooser-horiz-scroll" element.

This event info has to be passed from the parent app to the component, which can be done using a prop as follows:

The app uses mouseleave and mouseenter event listeners to invoke methods that set the value of one of its data properties. This data property is passed as a prop to the component, which in turn watches the prop’s value. When the prop changes to flag a mouseleave event on the parent, the component invokes the appropriate one of its own methods. The mouseenter listener (on the parent) resets the data property, which resets the prop.

The ChooserHorizScroll component has a repository at GitHub: https://github.com/clutterstack/chooser-horiz-scroll