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();
        });
    }
  };
})
4

3 回答 3

32

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

于 2013-08-16T01:14:51.043 回答
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 ;)

于 2013-08-15T09:55:37.317 回答
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);
          });
     }
  }
})
于 2015-06-03T07:49:23.287 回答