Как обновить модель AngularJS из функции связывания директив?

Чего я пытаюсь добиться, так это добавить дополнительный интерфейс для полей ввода, чтобы иметь возможность увеличивать и уменьшать числовое значение в них, нажимая кнопки + и -.

(По сути, это то, что поля input[type=number] имеют в chrome, но я хочу, чтобы это было совместимо с несколькими браузерами, а также имело полный контроль над представлением во всех браузерах).

Код в поле зрения:

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

Код директивы:

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


    }
  };

})

Это позволяет получить текущее значение модели через scope.$eval(attrs.ngModel), но не может установить новое значение.

Редактирование Aftermath: это код, который теперь работает (на случай, если вы не хотите видеть решение этой проблемы)

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

person Harijs Deksnis    schedule 15.08.2013    source источник
comment
может быть ctrl.$setViewValue(newValue) ?   -  person Ivan Chernykh    schedule 15.08.2013
comment
используйте ng-click и шаблон.   -  person Davin Tryon    schedule 15.08.2013
comment
Чернив, нет, это не помогло, но ваше предложение помогло мне лучше понять Angular. Спасибо.   -  person Harijs Deksnis    schedule 15.08.2013


Ответы (3)


Методы ngModelController следует использовать вместо $eval() для получения и установки свойств ng-model ценность.

parseInt() не требуется при оценке атрибута с числовым значением, потому что $eval преобразует значение в число. $eval следует использовать для установки переменных minValue и maxValue.

Нет необходимости в директиве для создания дочерней области.

$apply() не требуется, потому что методы ngModelController (в частности, $render()) будут автоматически обновлять представление. Однако, как отмечает @Harijs в комментарии ниже, $apply() необходима, если другие части приложения также нуждаются в обновлении.

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

скрипка

person Mark Rajcok    schedule 16.08.2013
comment
Проблема (или недостаток) с этим в моем случае использования заключается в том, что он обновляет только это одно поле ввода, а не всю модель — все остальные поля приложения, которые зависят от этого ввода. Поэтому мне необходим scope.$apply(). И вы правы насчет области действия, однако я не совсем понимаю, почему $eval следует вызывать для атрибутов. Он работает так же хорошо с parseInt. - person Harijs Deksnis; 16.08.2013
comment
@HarijsDeksnis, спасибо, я обновил свой ответ, чтобы отразить ваши комментарии. Да, вы можете использовать parseInt() или $eval. В вашем исходном коде вы использовали оба, поэтому я пытался указать, что вам нужно использовать только один. В редактировании Aftermath вы не используете ни minValue, ни maxValue, и вы должны использовать один из них, иначе minValue и maxValue будут строкового типа. $eval() немного более общий, так как он правильно анализирует логические значения, целые числа или строки и присваивает правильный тип переменной, тогда как parseInt(), очевидно, работает только для числовых значений. Поэтому я предпочитаю $eval(). - person Mark Rajcok; 16.08.2013
comment
Теперь я научился ценить преимущества $eval для attrs - он позволяет значению быть выражением, например. передается из какой-либо другой переменной области видимости. Я пытался найти, где вы использовали $parse, но, похоже, вы внедрили его не по соглашению, верно? - person Harijs Deksnis; 20.08.2013
comment
@HarijsDeksnis, я забыл удалить $parse - здесь он не нужен. Используйте $parse, если вы хотите передать/указать свойство объекта области в качестве атрибута директивы, а затем изменить значение внутри директивы: stackoverflow. com/a/15725402/215945 - person Mark Rajcok; 20.08.2013
comment
@MarkRajcok: решение не работает при одном условии. Вручную измените значение в текстовом поле ввода, а затем нажмите + или -. Вы заметите, что модель не обновляется. А также он ведет себя как строка вместо числа после этого события. - person Ritesh Kumar Gupta; 27.05.2015

Вы бы не хотели заменять переменную scope.ngModel, но значение, стоящее за этой переменной. Вы уже сделали это, когда прочитали значение в первой строке функции ссылки:

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

Если это простое значение, например myProperty, вы можете использовать его в области видимости:

 scope[attr.ngModel] = newValue

Но это не сработает, если у вас есть выражение-значение, например container.myProperty. В этом случае (и это более общий тип, к которому вы должны стремиться) вам нужно будет оценить значение, установленное для области, например так:

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

Я должен признать, что часть $eval немного уродлива, как в JavaScript с подвеской eval, но она делает свое дело. Просто имейте в виду, что это может не работать таким образом, когда вы хотите, чтобы значения String были установлены. Тогда вам придется избегать этих значений.

Надеюсь, это поможет ;)

person Tharabas    schedule 15.08.2013
comment
Спасибо, это обновляет значение модели! Хотя это не меняет взгляда на себя. Я должен добавить это, чтобы обновить поле ввода в представлении. ctrl.$viewValue = новое значение; ctrl.$рендеринг(); Однако мне все еще нужно выяснить, как обновить другие поля в моем приложении, которые используют значение этой модели. - person Harijs Deksnis; 15.08.2013
comment
Это было легче, чем ожидалось. Просто нужно было добавить scope.$apply() в конце. - person Harijs Deksnis; 15.08.2013
comment
Ах, да, забыл упомянуть, что любой прослушиватель событий jQuery, такой как bind("click"), не запускает цикл angular digest, поэтому вам придется делать это вручную. Но, видимо, вы это уже поняли ;) - person Tharabas; 15.08.2013
comment
Вместо $eval следует использовать методы ngModelController (см. мой ответ). - person Mark Rajcok; 16.08.2013

person    schedule
comment
Спасибо, это действительно помогло! - person Slava Fomin II; 10.02.2016