Angular.js: ng-model does not set initial value and position of input[type=range]

Created on 18 Mar 2014  ·  35Comments  ·  Source: angular/angular.js

Basically, a range input is set up with an initial value using ng-model. The actual value of the HTML element, and the position of the range slider, are incorrect. I would expect the slider position and html input value to be in sync with the model variable.

Here is a repro: http://jsfiddle.net/fschwiet/HVp2J/

Verified on chrome and firefox running on Windows 8.

I see there are other issues opened for input[type=range], its not clear they are related.

forms moderate duplicate broken expected use bug

Most helpful comment

With GitHub’s new Reactions feature, you no longer need to comment “+1”. Instead, you can mark your interest by clicking the “+:smile:” at the top-right of the first comment in this issue, then choosing :+1: (“+1”). This avoids sending a useless notification to everyone subscribed to this issue.

All 35 comments

This is still an issue with Angular 1.2.14: http://jsfiddle.net/HVp2J/7/

As a work-around, one can set the model value within a $timeout handler.

$timeout doesn' help, the issue remains.

@fschwiet : http://jsfiddle.net/HVp2J/9/

@Rajat-Chowdhary Sorry, I should have included a code sample showing how $timeout can work as work-around: http://jsfiddle.net/fschwiet/YC4av/

The workaround does feel dicey since the model value cannot be initialized until the $timeout callback for the workaround to work.

It seems I found the problem.
Angular sets value before setting min, max and step so it seems that the range is in the wrong position. To solve the problem I used this directive

angular.module('angular-range', []).directive('range', function() {
    return {
        replace: true,
        restrict: 'E',
        scope: {
            value: '=ngModel',
            min: '=rangeMin',
            max: '=rangeMax',
            step: '=rangeStep',
        },
        template: '<input type="range"/>',
        link: function(scope, iElement, iAttrs) {
            scope.$watch('min', function() { setValue(); });
            scope.$watch('max', function() { setValue(); });
            scope.$watch('step', function() { setValue(); });
            scope.$watch('value', function() { setValue(); });

            function setValue() {
                if (
                    angular.isDefined(scope.min) &&
                    angular.isDefined(scope.max) &&
                    angular.isDefined(scope.step) &&
                    angular.isDefined(scope.value)
                ) {
                    iElement.attr("min", scope.min);
                    iElement.attr("max", scope.max);
                    iElement.attr("step", scope.step);
                    iElement.val(scope.value); 
                }
            }
            function read() {
                scope.value = iElement.val();
            }

            iElement.on('change', function() {
                scope.$apply(read);
            });
        }
    };
});

Here there is the new version of ngRange without private scope:

.module('ngRange', [])
.directive('ngRange', function() {
    return {
        replace: true,
        restrict: 'E',
        require: 'ngModel',
        template: '<input type="range"></input>',
        link: function(scope, element, attrs, ngModel) {
            var ngRangeMin;
            var ngRangeMax;
            var ngRangeStep;
            var value;

            if (!angular.isDefined(attrs.ngRangeMin)) {
                ngRangeMin = 0;
            } else {
                scope.$watch(attrs.ngRangeMin, function(newValue, oldValue, scope) {
                    if (angular.isDefined(newValue)) {
                        ngRangeMin = newValue;
                        setValue();
                    }
                });
            }
            if (!angular.isDefined(attrs.ngRangeMax)) {
                ngRangeMax = 100;
            } else {
                scope.$watch(attrs.ngRangeMax, function(newValue, oldValue, scope) {
                    if (angular.isDefined(newValue)) {
                        ngRangeMax = newValue;
                        setValue();
                    }
                });
            }
            if (!angular.isDefined(attrs.ngRangeStep)) {
                ngRangeStep = 1;
            } else {
                scope.$watch(attrs.ngRangeStep, function(newValue, oldValue, scope) {
                    if (angular.isDefined(newValue)) {
                        ngRangeStep = newValue;
                        setValue();
                    }
                });
            }
            if (!angular.isDefined(ngModel)) {
                value = 50;
            } else {
                scope.$watch(
                    function () {
                        return ngModel.$modelValue;
                    }, 
                    function(newValue, oldValue, scope) {
                        if (angular.isDefined(newValue)) {
                            value = newValue;
                            setValue();
                        }
                    }
                );
            }

            function setValue() {
                if (
                    angular.isDefined(ngRangeMin) &&
                    angular.isDefined(ngRangeMax) &&
                    angular.isDefined(ngRangeStep) &&
                    angular.isDefined(value)
                ) {
                    element.attr("min", ngRangeMin);
                    element.attr("max", ngRangeMax);
                    element.attr("step", ngRangeStep);
                    element.val(value); 
                }
            }

            function read() {
                if (angular.isDefined(ngModel)) {
                    ngModel.$setViewValue(value);
                }
            }

            element.on('change', function() {
                if (angular.isDefined(value) && (value != element.val())) {
                    value = element.val();
                    scope.$apply(read);
                }
            });
        }
    };
});

Yep +1

Yes, still an issue.

I asked something similar in stackoverflow How to initialize the value of an input[range] using AngularJS when value is over 100 . Using 1.4.0-beta.build.3791 this bug is still present.

+1

+1, and it became worst when the input range is coupled with an input number.

+1 for this issue

Facing the same problem: +1

yep - same problem +1

this is basic html5 - whats the status on this, is anyone working on this?

taking under consideration this: https://github.com/danielcrisp/angular-rangeslider/issues/51
where Error: ngModel:numfmt range slider is thrown
i adjusted @ardf69 solution to this:

angular.module('common').directive('rangeSlider', [function () {
    return {
        replace: true,
        restrict: 'E',
        require: 'ngModel',
        template: '<input type="range"/>',
        link: function (scope, element, attrs, ngModel) {
            var ngRangeMin;
            var ngRangeMax;
            var ngRangeStep;
            var value;

            function init() {
                if (!angular.isDefined(attrs.ngRangeMin)) {
                    ngRangeMin = 0;
                } else {
                    scope.$watch(attrs.ngRangeMin, function (newValue, oldValue, scope) {
                        if (angular.isDefined(newValue)) {
                            ngRangeMin = newValue;
                            setValue();
                        }
                    });
                }
                if (!angular.isDefined(attrs.ngRangeMax)) {
                    ngRangeMax = 100;
                } else {
                    scope.$watch(attrs.ngRangeMax, function (newValue, oldValue, scope) {
                        if (angular.isDefined(newValue)) {
                            ngRangeMax = newValue;
                            setValue();
                        }
                    });
                }
                if (!angular.isDefined(attrs.ngRangeStep)) {
                    ngRangeStep = 1;
                } else {
                    scope.$watch(attrs.ngRangeStep, function (newValue, oldValue, scope) {
                        if (angular.isDefined(newValue)) {
                            ngRangeStep = newValue;
                            setValue();
                        }
                    });
                }
                if (!angular.isDefined(ngModel)) {
                    value = 50;
                } else {
                    scope.$watch(
                        function () {
                            return ngModel.$modelValue;
                        },
                        function (newValue, oldValue, scope) {
                            if (angular.isDefined(newValue)) {
                                value = newValue;
                                setValue();
                            }
                        }
                    );
                }

                if (!ngModel) {
                    return;
                }
                ngModel.$parsers.push(function (value) {
                    var val = Number(value);
                    if (val !== val) {
                        val = undefined;
                    }
                    return val;
                });
            }

            function setValue() {
                if (
                    angular.isDefined(ngRangeMin) &&
                    angular.isDefined(ngRangeMax) &&
                    angular.isDefined(ngRangeStep) &&
                    angular.isDefined(value)
                    ) {
                    element.attr("min", ngRangeMin);
                    element.attr("max", ngRangeMax);
                    element.attr("step", ngRangeStep);
                    element.val(value);
                }
            }

            function read() {
                if (angular.isDefined(ngModel)) {
                    ngModel.$setViewValue(value);
                }
            }

            element.on('change', function () {
                if (angular.isDefined(value) && (value != element.val())) {
                    value = element.val();
                    scope.$apply(read);
                }
            });

            init();
        }
    };
}
]);

+1, I have the same problem when switching[input type=range] with ng-if

I also ran into this problem, and wrote a Stack Overflow question about it, “In Angular, bind attribute from scope variable _before_ ngModel binds the input’s value. The question includes a runnable code snippet that reproduces this problem.

Some workarounds have been posted as answers on Stack Overflow. The one applicable to my case involves using DOM manipulation with $inputElement.prop() to set the values of min, max, and step (whichever attributes are needed). By doing this DOM manipulation before my custom directive’s underlying <input>’s value is first set, I avoid the problem.

My lazy way of addressing this bug was to divide the min/max and step values by 100. So 300 becomes 3.0, etc. and values fall below 100. Then I multiply things back as needed. Perhaps it's useful to someone.

+1 This is very annoying.

+1

+1

+1

Another lazy workaround hack:

      $timeout(function () {
        angular.element('.slider-input').val(amount);
      });

+1

With GitHub’s new Reactions feature, you no longer need to comment “+1”. Instead, you can mark your interest by clicking the “+:smile:” at the top-right of the first comment in this issue, then choosing :+1: (“+1”). This avoids sending a useless notification to everyone subscribed to this issue.

I have solved my problem using ondrag method by removing ng-model.
check this http://codepen.io/nagarajumusini/pen/BKpGJz

+1

+1

Adding ng-init to input works for me to set initial value

<input type="range" class="slider-years" min="0" max="30" step="1" value="0" data-ng-model="metier.anneeExperience" data-ng-init="metier.anneeExperience ? metier.anneeExperience = metier.anneeExperience : metier.anneeExperience = 0" />

Found solution in:

http://jsfiddle.net/jestho/8dymV/7/

We are working on this (finally). Closing as a duplicate of https://github.com/angular/angular.js/issues/5892

$(document).ready(function () {
angular.element('.yourclasshere').val(valueoftheslideronload);
});

this jQuery workaround works so you can set a value when the page loads but whatever ithe value of the slider controls wont sync until it's touched.

@davedx Maybe this is the best way before the issue is solved.

<input type="range" min="15" data-ng-init="signup.age=30" value="30" max="85" step="1" name="ageInput" class="form-control input-lg" ng-model="signup.age" oninput="ageOutputId.value = ageInput.value" /> <output name="ageOutputName" id="ageOutputId">30</output>

Was this page helpful?
0 / 5 - 0 ratings