Three ways to build a range slider from scratch in EmberJS

Last week, I built a range slider that triggered up to 20 route changes per second… it’s a long story. But I needed that range slider to work, it had to be fast, and the click and drag UI had to be smooth.

Good thing I had options for how I could manage statefulness and user interactions. By the end of this article, you’ll have a better handle on the many ways to catch actions and events in your components, AND you’ll be able to use the mysterious mut helper.

Here’s a working Ember 3 demo and Ember 2 demo of all three strategies, plus a “regular” use of the Ember input helper.

This article’s code blocks and template examples work for Ember 1.13 through 3.x. See my article How to use Ember 2 code in your Ember 3 app for more help using older code examples.

What’s the problem?

There’s something weird about range sliders that don’t use the input helper built into Ember. When you slide them, the change doesn’t have any effect on the bound template variables you used to set the value. The binding is only one way (unlike what happens when you use Ember’s built-in input helpers for text fields and check boxes).

Here’s an example. Let’s say that we passed in score equal to 0. The slider would be in the correct place, at 0, when the component renders. But when you move the slider, score is still 0.

<input type="range" min=”0" max=”10" value={{score}}>
This doesn’t change when you move the slide: {{score}}
Rendered input where value isn’t bound :(

We need to handle the event ourselves to change the value of score. But how?

Do you need to write this stuff by hand?

Maybe. There are a few range sliders available as Ember addons, plus the built-in input helper (guides and docs). I’ve used ui-ember-slider a bunch, and their interactive docs are awesome. You should check them out if just to know what to shoot for when you write an addon ;)

Unfortunately, none of my past solutions worked because I was doing something truly strange by tying query parameters to a slider, and then changing all the slider attributes asynchronously in response to data fetching etc. I build weird things.

In many cases, Ember’s built in input helper works just fine. Here’s an example. This will trigger an action when someone stops dragging the slider and releases the mouse click. (demo, Option 0)

<!-- my-component.hbs template -->
{{input type='range' min=0 max=10 value=sliderValue mouseUp=(action 'sliderUpdate' sliderValue)}}

// in my-component.js
actions: {
sliderUpdate(sliderValue) {
// do something with the size variable
}
}

In my use case however, everything I tried resulted in the slider button skipping around and flickering as I scrolled through routes. We were not at 60fps. It couldn’t keep up with the rest of my app. The addons might work for you. Try them first. But if you need finer grained control…

Option 1: Use the mut helper (and understand it)

I used to skip right past code snippets that used the mut helper. I didn’t get it. So, if you feel similarly, bear with me and I’ll see if I can bring you up to speed. This is what I ended up using in my app.

I only need to do work in one file, range-slider-component.hbs:

<input type="range" 
oninput={{action (mut score) value="target.value"}}
min={{min}}
max={{max}}
value={{score}}
steps="1">
GIF of my super fast range slider

Here’s a play-by-play. First, see how I’m passing value={{score} ? I still need to initialize the slider with the value we passed in.

Next, see that oninput? There are only two events that can be fired from a range slider, input and change. Input fires every time the slider moves. Change fires only after the user releases a click. In my case, I wanted to use input. To tap into the input, I set an action equal to on plus the event name. If you are working on other types of elements besides a range slider, you can use this same strategy for any events that are in this list in the Ember Guides. Just don’t forget to add on to the beginning.

Take a look at that target.value . An event is normal HTML stuff, similar to the arguments you’d get in a JQuery click handler. Events with targets and values are nothing Ember-specific. Our event here is input , the target is the HTML element, and the value is the number the slider is on.

Now, what’s up with mut? There are many ways to use mut, which is part of the reason I found it confusing. In this use case, I think of mut as a setter. The first “argument,” score , is what will get changed. The second “argument”, target.value is the value of the slider. So we’re setting score to be the value of the slider, every time the slider’s value changes and the input event fires. Mut can also be used to mark variables as able-to-be-edited when passed from a parent to a child component, but that’s not what we’re doing here.

Here’s a simpler example of mut:

<p>Kitten Count: {{kittenCount}}</p>
<button onclick={{action (mut kittenCount) 1}}>1</button>
<button onclick={{action (mut kittenCount) 2}}>2</button>
<button onclick={{action (mut kittenCount) 3}}>3</button>

Clicking on any of those buttons will set the KittenCounter to the value that comes after (mut kittenCount) . My component js file is empty. There are no actions in it and I didn’t pass any actions in from a parent either.

Option 2: Use Ember’s built-in event listeners

Ok, so remember that list of events that can be used in templates, like oninput, onchange, onclick, etc? All of those events can be listened for from the component too. It’s kinda spooky.

The range-slider-component.hbs:

<input type="range" 
min={{min}}
max={{max}}
value={{score}}
steps="1">

The range-slider-component.js:

import Ember from ‘ember’;
export default Ember.Component.extend({
input(event) {
console.log(event.target.value)
}
});

This is a little odd because there’s no action in the template, and there are no actions in our JS file either. There’s just this input function. It’s listening for an input event to be fired from within the template. The event itself is automatically available as an argument. event.target.value is whatever the range slider was set to. Try console.logging the entire event object, then event.target to get a better idea of what’s going on here.

Now, this solution doesn’t actually change score if you were displaying it elsewhere in the template. If you wanted to, you could set score to be the value, like this.set(‘score’, e.target.value). It works, but I feel kinda weird about that. In my case, I didn’t need to actually change the values of the variables I passed in, but just trigger some other UI changes, so I just called for those inside of the input function.

Option 3: Use closure actions

I happen to like this one a lot, since it’s closest to the rest of my app’s patterns. Whether it’s a good choice depends on what you want to do with the new value. Does it need to be displayed, or do other things simply need to change in response? If you’re working with the latter, this might be the way to go.

In my range-slider-component hbs template:

<input type="range" 
oninput={{action 'sliderMoved'}}
min={{min}}
max={{max}}
value={{someValue}}
steps="1">

In range-slider-component.js:

import Ember from ‘ember’;
export default Ember.Component.extend({
actions: {
sliderMoved(event) {
console.log(event.target.value) // the slider's value
}
},
});

We’re taking advantage of a lesser-known behavior of closure actions. The term “closure actions” can mean a lot of things, but here it means setting an action equal to an event name in the template, such as oninput={{action 'sliderMoved'}}. Whenever you use an action in this way, by default, it receives the event as an argument. Like in the other examples, event.target.value is the number that the slider is resting on.

If you’re not a fan of the implicit event argument passing, you can do something like this instead:

<input type="range" 
oninput={{action 'sliderMoved' value="target.value"}}
min={{min}}
max={{max}}
value={{score}}
steps="1">

Then, the slider’s value is received as an argument when the action fires. It’s a little more elegant, but I wanted to uncover some of the other powers of closure actions first.

Further Reading

I really like Deep Dive on Ember Events by Marie Chatfield. I think it helped unlock how I think about DOM events.

I’ve read Miguel Camba’s Ember Closure Actions in Depth about 8 times and while I still don’t understand all of it, it was a critical part of my refactor of an entire app to use closure actions.

Lastly, I wrote an article a long time ago called Easy select element in EmberJS, based on Balint Erdi’s How to do a select in Ember 2.0. These articles outline some very similar strategies, but for a select dropdown instead of a range slider. It’s been nearly a year since I first read Balint’s article, and you can see all the same patterns we’re using here. It just took a while to sink in.

In conclusion

Which option is right? Well, you know what they say…

There’s no wrong way to eat an oreo