Как использовать последнее допустимое значение модели, если модель становится недействительной?

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

шаблон:

   <input ng-model="fooCtrl.name" autosave>
   <input ng-model="fooCtrl.email" autosave>

директива:

  .directive('autosave', ['$parse', function  ($parse) {

    return {
      restrict: 'A',
      require: 'ngModel',
      link: function (scope, element, attrs, ngModel) {

        function saveIfModelChanged () {
          // save object containing name and email to server ...
        }

        ngModel.$viewChangeListeners.push(function () {
          saveIfModelChanged();
        });
      }
    };
  }]);

Пока у меня все работает нормально. Однако, когда я добавляю проверку в смесь, например, проверяя поле ввода как действительный адрес электронной почты, modelValue устанавливается на undefined, как только viewValue изменяется на недопустимый адрес электронной почты.

Что я хотел бы сделать, так это: запомнить последнее действительное значение модели и использовать его при автосохранении. Если пользователь изменит адрес электронной почты на недействительный, объект, содержащий name и email, все равно должен быть сохранен на сервере. Используя текущий действительный name и последний действительный email.

Я начал с сохранения последнего допустимого значения модели следующим образом:

шаблон с добавленной проверкой:

   <input type="email" ng-model="fooCtrl.name" autosave required>
   <input ng-model="fooCtrl.email" autosave required>

директива с сохранением lastModelValue:

  .directive('autosave', ['$parse', function  ($parse) {

    return {
      restrict: 'A',
      require: 'ngModel',
      link: function (scope, element, attrs, ngModel) {

        var lastModelValue;

        function saveIfModelChanged () {

          // remeber last valid modelValue
          if (ngModel.$valid) {
             lastModelValue = ngModel.$modelValue;
          }

          // save object containing current or last valid
          // name and email to server ...
        }

        ngModel.$viewChangeListeners.push(function () {
          saveIfModelChanged();
        });
      }
    };
  }]);

Мой вопрос: как использовать lastModelValue при сохранении, но сохраняя недопустимое значение в представлении?

ИЗМЕНИТЬ:

Другая возможность, как предложено Jugnu ниже, заключается в обертывании и управлении сборкой в ​​валидаторах.

Я попытался сделать следующее: обернуть все существующие валидаторы и запомнить последнее действительное значение, чтобы восстановить его в случае сбоя проверки:

Object.keys(ngModel.$validators).forEach(function(validatorName, index) {
    var validator = ngModel.$validators[validatorName];
    ngModel.$validators[validatorName] = createWrapper(validatorName, validator, ngModel);
});

function createWrapper(validatorName, validator, ngModel){

  var lastValid;

  return function (modelValue){

    var result = validator(modelValue);

    if(result) {
      lastValid = modelValue;
    }else{
        // what to do here? maybe asign the value like this:
      // $parse(attrs.ngModel).assign(scope, lastValid);
    }

    return result;
  };
}

Но я не уверен, как продолжить этот подход. Могу ли я установить значение модели без включения AngularJS и попытаться проверить это новое установленное значение?


person Tim Büthe    schedule 17.09.2015    source источник
comment
Учитывая, что вы также разрабатываете серверную часть своего приложения, я бы рассмотрел возможность передачи всей проблемы на серверную часть. Если бы бэкэнд был node.js, я бы просто обновил эти значения v в моей базе данных, для которых хранится v!==undefined, и сэкономил бы себе несколько рабочих часов. Я понимаю, что вы, возможно, не сможете сделать это или сделать этот проект в учебных целях, просто мои мысли об эффективности разработки.   -  person Kevin Dreßler    schedule 21.09.2015


Ответы (3)


Я создал простую директиву, которая служит оболочкой для директивы ng-model и всегда будет поддерживать последнее допустимое значение модели. Он называется valid-ng-model и должен заменить использование ng-model в тех местах, где вы хотите иметь последнее допустимое значение.

Я создал пример использования здесь, надеюсь, он вам понравится. Любые идеи по улучшению приветствуются.

Это код реализации директивы valid-ng-model.

app.directive('validNgModel', function ($compile) {
  return {
      terminal: true,
      priority: 1000,
      scope: {
        validNgModel: '=validNgModel'
      },
      link: function link(scope, element, attrs) {

        // NOTE: add ngModel directive with custom model defined on the isolate scope
        scope.customNgModel = angular.copy(scope.validNgModel);
        element.attr('ng-model', 'customNgModel'); 
        element.removeAttr('valid-ng-model');

        // NOTE: recompile the element without this directive
        var compiledElement = $compile(element)(scope);
        var ngModelCtrl = compiledElement.controller('ngModel');

        // NOTE: Synchronizing (inner ngModel -> outside valid model)
        scope.$watch('customNgModel', function (newModelValue) {
          if (ngModelCtrl.$valid) {
            scope.validNgModel = newModelValue;
          }
        });

        // NOTE: Synchronizing (outside model -> inner ngModel)
        scope.$watch('validNgModel', function (newOutsideModelValue) {
          scope.customNgModel = newOutsideModelValue;
        });
      }
    };
});

Изменить: реализация директивы без изолированной области: Plunker.

person S.Klechkovski    schedule 21.09.2015
comment
Я поиграл с плунжером, и он выглядит очень многообещающе! Я попытаюсь интегрировать его в наше приложение и вернусь с результатами. Как вы думаете, было бы проблематично/трудно объединить директивы autosave и valid-ng-model? - person Tim Büthe; 21.09.2015
comment
Спасибо, я рад, что вам понравилось. Для слияния двух директив это не проблема, но ИМХО они касаются двух разных проблем, которые я не люблю смешивать. Но если вы считаете, что так было бы лучше, то смело объединяйте их, я могу помочь вам и в этом. - person S.Klechkovski; 22.09.2015
comment
Спасибо, но я думаю, что смогу объединить директивы. Однако меня интересует другой аспект: директива You создает изолированную область, чего не делают ни ngModel, ни моя директива автосохранения. Возможно ли это без изолированного прицела? Должны ли мы для начала присвоить customNgModel области действия? - person Tim Büthe; 22.09.2015
comment
На самом деле я сталкиваюсь с разными проблемами из-за этой изолированной области: 1. внутри моей директивы автосохранения я должен использовать scope.$parent.$eval(...); сейчас вместо scope.$eval(...);, поэтому мне придется искать как текущую область, так и родительскую область в зависимости от использования valid-ng-model или ng-model 2. Всплывающее окно выбора даты в bootstrap-ui перестает работать при изменении ng-model на valid-ng-model. 3. ng-disabled="foo.enabled" перестает работать при смене ng-model="foo.title" на valid-ng-model="foo.title" - person Tim Büthe; 22.09.2015
comment
Основная идея моего подхода состоит в том, чтобы связать директиву ng-model с некоторой внутренней моделью, чтобы я мог контролировать значения, которые передаются моей внешней (реальной) модели, сохраняя при этом ее функциональность. Вот почему директиве нужна новая область применения, которую не нужно изолировать. Вот директива, реализованная с новой областью действия, которая прототипически наследуется от своего родителя. Это должно решить проблемы № 1 и № 3, которые у вас есть, для № 2 я не уверен, и вы должны попробовать. Скажите мне, что вы думаете, если это сработает для вас, я обновлю свой ответ. Ссылка: plnkr.co/edit/3sCddqzHYy99XcIh8U5f?p=preview - person S.Klechkovski; 22.09.2015
comment
@TimBüthe, я обновил ответ последней реализацией директивы valid-ng-model. Если вы довольны предложенным решением, я бы посоветовал вам использовать бонусные баллы в качестве вознаграждения до истечения времени. Спасибо. - person S.Klechkovski; 26.09.2015

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

  1. У вас есть допустимый объект { name: 'Valid', email: 'Valid' }.
  2. Вы меняете имя на недействительное; директива autosave, размещенная при вводе имени, знает свое собственное последнее действительное значение, поэтому отправляется правильный объект.
  3. Вы также меняете адрес электронной почты на недействительный. Директива autosave, размещенная при вводе электронной почты, знает свое собственное последнее действительное значение, но НЕ имя. Если последние известные хорошие значения не централизованы, будет отправлен объект типа { name: 'inalid', email: 'Valid' }.

Итак, предложение:

  1. Сохраняйте очищенную копию объекта, который вы редактируете. Под санацией я подразумеваю, что любые недопустимые начальные значения должны быть заменены допустимыми нетронутыми (например, нулями, нулями и т. д.). Выставьте эту копию в качестве члена контроллера, например. fooCtrl.lastKnowngood.
  2. Сообщите autosave последнее известное хорошее состояние, например. в виде:

    <input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required />
    
  3. Сохраните последнее известное хорошее локальное значение в этом объекте; используйте выражение ng-model, например. в виде:

    var lastKnownGoodExpr = $parse(attrs.autosave);
    var modelExpr = $parse(attrs.ngModel);
    
    function saveIfModelChanged () {
        var lastKnownGood = lastKnownGoodExpr(scope);
    
        if (ngModel.$valid) {
            // trick here; explanation later
            modelExpr.assign({fooCtrl: lastKnownGood}, ngModel.$modelValue);
        }
    
        // send the lastKnownGood object to the server!!!
    }
    
  4. Отправьте объект lastKnownGood.

Хитрость, ее недостатки и способы ее улучшения: при установке значения локальной модели для объекта lastKnownGood вы используете объект контекста, отличный от текущей области; этот объект предполагает, что контроллер называется fooCtrl (см. строку modelExpr.assign({fooCtrl: lastKnownGood}, ...)). Если вам нужна более общая директива, вы можете передать корень как другой атрибут, например:

<input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required
    autosave-fake-root="fooCtrl" />

Вы также можете самостоятельно проанализировать выражение ng-model, чтобы определить первый компонент, например. подстрока 0 1-е появление точки (опять же упрощенно).

Другой недостаток заключается в том, как вы обрабатываете более сложные пути (в общем случае), например. fooCtrl.persons[13].address['home'].street - но, похоже, это не ваш вариант использования.


Кстати, это:

ngModel.$viewChangeListeners.push(function () {
    saveIfModelChanged();
});

можно упростить так:

ngModel.$viewChangeListeners.push(saveIfModelChanged);
person Nikos Paraskevopoulos    schedule 17.09.2015
comment
Спасибо, мне нравятся предложения! Это наводит меня на мысль: если я начну возиться с выражением модели, я смогу создать прокси для исходного объекта. Затем я бы установил значения для прокси, если исходная модель станет недействительной, и отправлю прокси-объект на сервер. - person Tim Büthe; 17.09.2015
comment
Это кажется близким к идее и может сработать, если я правильно понял - какой-нибудь пример поможет. - person Nikos Paraskevopoulos; 17.09.2015

Валидаторы Angular по умолчанию будут присваивать значение модели только в том случае, если ее действительный адрес электронной почты. Чтобы преодолеть это, вам нужно будет переопределить валидаторы по умолчанию.

Для получения дополнительной информации см.: https://docs.angularjs.org/guide/forms#modifying-built-in-validators

Вы можете создать directive, который присвоит недопустимое значение модели некоторой переменной области видимости, а затем вы сможете ее использовать.

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

Вот скрипка: http://plnkr.co/edit/EwuyRI5uGlrGfyGxOibl?p=preview

person Juned Lanja    schedule 17.09.2015
comment
обертывание и манипулирование сборкой в ​​валидаторах кажется хорошей альтернативой. Я пытался пойти по этому пути, но застрял. Не могли бы вы взглянуть на мое редактирование вопроса, пожалуйста? Есть ли у вас дополнительные советы? - person Tim Büthe; 21.09.2015
comment
Я не рассматриваю это как вариант, переопределение валидаторов будет означать, что им нужно будет вернуть успешную проверку, даже если проверка не удалась, чтобы сохранить значение модели. Это нарушило бы их функциональность. Эта строка взята из исходного кода ngModelCtrl и выполняется после запуска всех валидаторов: ctrl.$modelValue = allValid ? модельное значение: не определено; - person S.Klechkovski; 21.09.2015