12

I'm trying to figure out if knockout js would work nicely for the following problem:

I have multiple sliders that I want to link to textboxes.

When the textbox is changed the corresponding slider must update to the new value and vice versa.

On changing the slider value or textbox a function needs to be called that uses the input from all textboxes to calculate a result.

I have my quick and dirty jQuery solution here.

Would it be easy to achieve the same result in a more elegant way using knockout js?

I guess I would need to create a custom binding handler like its done in jQuery UI datepicker change event not caught by KnockoutJS

Community
  • 1
  • 1
woggles
  • 7,444
  • 12
  • 70
  • 130
  • You could use this library: http://gvas.github.io/knockout-jqueryui/slider.html – j-a Mar 23 '14 at 05:27

4 Answers4

38

Here is an example: http://jsfiddle.net/jearles/Dt7Ka/

I use a custom binding to integrate the jquery-ui slider and use Knockout to capture the inputs and calculate the net amount.

--

UI

<h2>Slider Demo</h2>

Savings: <input data-bind="value: savings, valueUpdate: 'afterkeydown'" />
<div style="margin: 10px" data-bind="slider: savings, sliderOptions: {min: 0, max: 100, range: 'min', step: 1}"></div>

Spent: <input data-bind="value: spent, valueUpdate: 'afterkeydown'" />
<div style="margin: 10px" data-bind="slider: spent, sliderOptions: {min: 0, max: 100, range: 'min', step: 1}"></div>

Net: <span data-bind="text: net"></span>

View Model

ko.bindingHandlers.slider = {
  init: function (element, valueAccessor, allBindingsAccessor) {
    var options = allBindingsAccessor().sliderOptions || {};
    $(element).slider(options);
    $(element).slider({
        "slide": function (event, ui) {
            var observable = valueAccessor();
            observable(ui.value);
        },
        "change": function (event, ui) {
            var observable = valueAccessor();
            observable(ui.value);
        }
    });
    ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
        $(element).slider("destroy");
    });
  },
  update: function (element, valueAccessor) {
    var value = ko.unwrap(valueAccessor());
    if (isNaN(value)) {
        value = 0;
    }
    $(element).slider("value", value);
  }
};

var ViewModel = function() {
    var self = this;

    self.savings = ko.observable(10);
    self.spent = ko.observable(5);
    self.net = ko.computed(function() {
        return self.savings() - self.spent();
    });
}

ko.applyBindings(new ViewModel());
Martin Eden
  • 6,143
  • 3
  • 30
  • 33
John Earles
  • 7,194
  • 2
  • 37
  • 35
  • 1
    Thanks! Way more elegant than my jQuery sln – woggles Oct 12 '12 at 11:07
  • I see that it also limits the input of the textboxes to the range of the sliders...awesome :) Is there a way to prevent users from entering anything but numbers? When a character is put it it outputs NaN – woggles Oct 12 '12 at 11:33
  • I've updated my code above, and the fiddle, to check for numeric before updating. If NaN it resets the value to 0. – John Earles Oct 12 '12 at 12:14
  • Thanks...it looks great-my page has about 6 sliders so that really improves it – woggles Oct 13 '12 at 08:33
  • One other bug I discovered is that if the slider has a step other than 1 set you cant input text: see http://jsfiddle.net/Dt7Ka/11/ – woggles Oct 15 '12 at 14:13
  • You can, you just have to do it in the proper increments. This is a side-effect of the valueUpdate: 'afterKeyDown', which immediately tries to do the update. If you remove that you'll get free entry - but the field has to lose focus to trigger the update. See: http://jsfiddle.net/jearles/Dt7Ka/12/ – John Earles Oct 15 '12 at 23:42
10

I know it's some days ago but I made a few adjustments to John Earles code:

ko.bindingHandlers.slider = {
init: function (element, valueAccessor, allBindingsAccessor) {
    var options = allBindingsAccessor().sliderOptions || {};
    $(element).slider(options);
    ko.utils.registerEventHandler(element, "slidechange", function (event, ui) {
        var observable = valueAccessor();
        observable(ui.value);
    });
    ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
        $(element).slider("destroy");
    });
    ko.utils.registerEventHandler(element, "slide", function (event, ui) {
        var observable = valueAccessor();
        observable(ui.value);
    });
},
update: function (element, valueAccessor, allBindingsAccessor) {
    var value = ko.utils.unwrapObservable(valueAccessor());
    if (isNaN(value)) value = 0;
    $(element).slider("option", allBindingsAccessor().sliderOptions);
    $(element).slider("value", value);
}
};

The reason for this is that if you use options that change (fx another observable) then it won't affect the slider even if you wanted it to do so.

2

@John Earles and @Michael Kire Hansen: thanks for your wonderful solutions!

I used the advanced code from Michael Kire Hansen. I tied the "max:" option of the slider to a ko.observable and it turned out that the slider does not correctly update the value in this case. Example: Lets say the slider is at value 25 of max 25 und you change the max value to 100, the slider stays at the most right position, indicating that it is at the max value (but value is still 25, not 100). As soon as you slide one point to the left, you get the value updated to 99.

Solution: in the "update:" part just switch the last two lines to:

$(element).slider("option", allBindingsAccessor().sliderOptions);
$(element).slider("value", value);

This changes the options first, then the value and it works like a charm.

Its-me
  • 927
  • 1
  • 6
  • 10
0

Thanks so much for the help, I needed to use a range slider in my scenario so here is an extension to @John Earles and @Michael Kire Hansen

ko.bindingHandlers.sliderRange = {
init: function (element, valueAccessor, allBindingsAccessor) {
    var options = allBindingsAccessor().sliderOptions || {};
    $(element).slider(options);
    ko.utils.registerEventHandler(element, "slidechange", function (event, ui) {
        var observable = valueAccessor();
        observable.Min(ui.values[0]);
        observable.Max(ui.values[1]);
    });
    ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
        $(element).slider("destroy");
    });
    ko.utils.registerEventHandler(element, "slide", function (event, ui) {
        var observable = valueAccessor();
        observable.Min(ui.values[0]);
        observable.Max(ui.values[1]);
    });
},
update: function (element, valueAccessor, allBindingsAccessor) {
    var value = ko.utils.unwrapObservable(valueAccessor());
    if (isNaN(value.Min())) value.Min(0);
    if (isNaN(value.Max())) value.Max(0);

    $(element).slider("option", allBindingsAccessor().sliderOptions);
    $(element).slider("values", 0, value.Min());
    $(element).slider("values", 1, value.Max());
}
};

and then the HTML to accompany it

<div id="slider-range" 
             data-bind="sliderRange: { Min: 0, Max: 100 }, 
                                sliderOptions: { 
                                    range: true,
                                    min: 0,
                                    max: 100,
                                    step: 10,
                                    values: [0, 100]
                                }"></div>
Adrian Brink
  • 61
  • 1
  • 8