14

What I am trying to achieve is to add extra interface for input fields to be able to increace and decrease numeric value in them by clicking + and - buttons.

(In essence it is what input[type=number] fields have on chrome, but I want this to be cross-broswer compatible and also have full control of presentation accross all browsers).

Code in view:

<input data-ng-model="session.amountChosen" type="text" min="1" class="form-control input-small" data-number-input>

Directive code:

app.directive('numberInput', function() {
return {
    require: 'ngModel',
    scope: true,
    link: function(scope, elm, attrs, ctrl) {

        var currValue = parseInt(scope.$eval(attrs.ngModel)),
            minValue = attrs.min || 0,
            maxValue = attrs.max || Infinity,
            newValue;

        //puts a wrap around the input and adds + and - buttons
        elm.wrap('<div class="number-input-wrap"></div>').parent().append('<div class="number-input-controls"><a href="#" class="btn btn-xs btn-pluimen">+</a><a href="#" class="btn btn-xs btn-pluimen">-</a></div>');

        //finds the buttons ands binds a click event to them where the model increase/decrease should happen
        elm.parent().find('a').bind('click',function(e){

            if(this.text=='+' && currValue<maxValue) {
                newValue = currValue+1;    
            } else if (this.text=='-' && currValue>minValue) {
                newValue = currValue-1;    
            }

            scope.$apply(function(){
                scope.ngModel = newValue;
            });

            e.preventDefault();
        });


    }
  };

})

This is able to retrieve the current model value via scope.$eval(attrs.ngModel), but fails to set the new value.

Aftermath edit: this is the code that now works (in case you wan't to see the solution for this problem)

app.directive('numberInput', function() {
  return {
    require: 'ngModel',
    scope: true,
    link: function(scope, elm, attrs, ctrl) {

        var minValue = attrs.min || 0,
            maxValue = attrs.max || Infinity;

        elm.wrap('<div class="number-input-wrap"></div>').parent().append('<div class="number-input-controls"><a href="#" class="btn btn-xs btn-pluimen">+</a><a href="#" class="btn btn-xs btn-pluimen">-</a></div>');
        elm.parent().find('a').bind('click',function(e){

            var currValue = parseInt(scope.$eval(attrs.ngModel)),
                newValue = currValue;

            if(this.text=='+' && currValue<maxValue) {
                newValue = currValue+1;    
            } else if (this.text=='-' && currValue>minValue) {
                newValue = currValue-1;    
            }

            scope.$eval(attrs.ngModel + "=" + newValue);
            scope.$apply();            

            e.preventDefault();
        });
    }
  };
})
3
  • may be ctrl.$setViewValue(newValue) ? Commented Aug 15, 2013 at 9:20
  • use ng-click and a template. Commented Aug 15, 2013 at 9:26
  • Cherniv, no it did not do the trick, but your suggestion helped me to understand Angular better. Thanks. Commented Aug 15, 2013 at 10:59

3 Answers 3

33

ngModelController methods should be used instead of $eval() to get and set the ng-model property's value.

parseInt() is not required when evaluating an attribute with a numeric value, because $eval will convert the value to a number. $eval should be used to set variables minValue and maxValue.

There is no need for the directive to create a child scope.

$apply() is not needed because the ngModelController methods ($render() in particular) will automatically update the view. However, as @Harijs notes in a comment below, $apply() is needed if other parts of the app also need to be updated.

app.directive('numberInput', function ($parse) {
    return {
        require: 'ngModel',
        link: function (scope, elm, attrs, ctrl) {
            var minValue = scope.$eval(attrs.min) || 0,
                maxValue = scope.$eval(attrs.max) || Infinity;
            elm.wrap('<div class="number-input-wrap"></div>').parent()
                .append('<div class="number-input-controls"><a href="#" class="btn btn-xs btn-pluimen">+</a><a href="#" class="btn btn-xs btn-pluimen">-</a></div>');
            elm.parent().find('a').bind('click', function (e) {
                var currValue = ctrl.$modelValue,
                    newValue;
                if (this.text === '+' && currValue < maxValue) {
                    newValue = currValue + 1;
                } else if (this.text === '-' && currValue > minValue) {
                    newValue = currValue - 1;
                }
                ctrl.$setViewValue(newValue);
                ctrl.$render();
                e.preventDefault();
                scope.$apply(); // needed if other parts of the app need to be updated
            });
        }
    };
});

fiddle

Sign up to request clarification or add additional context in comments.

5 Comments

The problem (or insufficiency) with this in my use case is that it only updates this one input field, not the whole model - all other app fields that depend on this input. Therefore scope.$apply() is necessary for me. And you are corrrect about the scope, however I don´t fully see why $eval should be called upon attributes. It works just as well with parseInt.
@HarijsDeksnis, thanks, I updated my answer to reflect your comments. Yes, you can use parseInt() or $eval. In your original code, you used both, so I was trying to point out that you only need to use one. In "Aftermath edit" you are not using either with minValue and maxValue, and you should use one of them, otherwise minValue and maxValue will be of type string. $eval() is a little more general, as it will correctly parse booleans, ints or strings, and assign the correct type to the variable, whereas parseInt() obviously only works for numeric values. So I tend to favor $eval().
Now I learned to appreciate the benefit of $eval for attrs - it allows the value to be an expression, e.g. passed from some other scope variable. I was trying to find where you used $parse, but seems you injected it only out of convention, right?
@HarijsDeksnis, I forgot to remove $parse -- it is not needed here. Use $parse if you want to pass/specify a scope object property as a directive attribute, and then modify the value inside the directive: stackoverflow.com/a/15725402/215945
@MarkRajcok: The solution does not work under one condition. Manually change the value in input text box, and then press + or -. You will notice that the model is not updated. And also it behaves as a string instead of number after this event.
4

You would not want to replace the scope.ngModel variable, but the value that's behind that variable. You did it already when you read the value in the first line of the link function:

 currValue = parseInt(scope.$eval(attrs.ngModel))
 //                         ^^^^^^^^^^^^^^^^^^^^

If it's a plain value, like myProperty, you could use that on the scope:

 scope[attr.ngModel] = newValue

But this will not work, if you have an expression-value, like container.myProperty. In that case (and this is the more generic type, you should be aiming for) you'd have to eval the value being set to the scope, like this:

scope.$eval(attrs.ngModel + "=" + newValue)

I must admit, that the $eval part is a bit ugly, as it is in JavaScript with the eval pendant, but it does the trick. Just keep in mind that it might not work this way, when you want String values to be set. Then you'd have to escape those values.

Hope that helps ;)

4 Comments

Thanks, it does update the model value! Albeit it does not change the view on itself. I have to add this to get the input field to be updated on the view. ctrl.$viewValue = newValue; ctrl.$render(); However, I still have to figure out how to update other fields in my app that use this model's value.
It was easier than expected. Just had to add scope.$apply() at the end.
Ahh, yeah, forgot to mention, that any jQuery event listener like the bind("click") does not trigger angular's digest cycle, so you'd have to do that manually. But apparently you figured that already ;)
ngModelController methods should be used instead of $eval (see my answer).
3
.directive('numberInput', function ($parse) {
   return {
      require: 'ngModel',
      link: function (scope, elm, attrs){ 
        elm.bind("click", function() {
          var model = $parse(attrs.ngModel);
          var modelSetter = model.assign;
          scope.$apply(function() {
               modelSetter(scope, model);
          });
     }
  }
})

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.