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):
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.
window.requestAnimationFrame() and CSS
translate() determines the position of the “scrolling” element at a given time, and I gave it “physics,” using
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
I used a solution proposed by GitHub user Rouven Bühlmann (nirazul): at the
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
<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
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
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