Могу ли я использовать ng-модель с изолированной областью?

Я создаю простую директиву ui-datetime. Он разбивает объект Date javascript на части _date, _hours и _minutes. _date использует jquery ui datepicker, _hours и _minutes - числовые входы.

angular.module("ExperimentsModule", [])
    .directive("uiDatetime", function () {
    return {
        restrict: 'EA',
        replace: true,
        template: '<div class="ui-datetime">' +
            '<input type="text" ng-model="_date" class="date">' +
            '<input type="number" ng-model="_hours" min="0" max="23" class="hours">' +
            '<input type="number" ng-model="_minutes" min="0" max="59" class="minutes">' +
            '<br />Child datetime1: {{datetime1}}' +
            '</div>',
        require: 'ngModel',
        scope: true,
        link: function (scope, element, attrs, ngModelCtrl) {
            var elDate = element.find('input.date');

            ngModelCtrl.$render = function () {
                var date = new Date(ngModelCtrl.$viewValue);
                var fillNull = function (num) {
                    if (num < 10) return '0' + num;
                    return num;
                };
                scope._date = fillNull(date.getDate()) + '.' + fillNull(date.getMonth() + 1) + '.' + date.getFullYear();
                scope._hours = date.getHours();
                scope._minutes = date.getMinutes();
            };

            elDate.datepicker({
                dateFormat: 'dd.mm.yy',
                onSelect: function (value, picker) {
                    scope._date = value;
                    scope.$apply();
                }
            });

            var watchExpr = function () {
                var res = scope.$eval('_date').split('.');
                if (res.length == 3) return new Date(res[2], res[1] - 1, res[0], scope.$eval('_hours'), scope.$eval('_minutes'));
                return 0;
            };
            scope.$watch(watchExpr, function (newValue) {
                ngModelCtrl.$setViewValue(newValue);
            }, true);
        }
    };
});

function TestController($scope) {
    $scope.datetime1 = new Date();
}

jsfiddle

В github: https://github.com/andreev-artem/angular_experiments/tree/master/ui-datetime

Насколько я понимаю, лучшей практикой при создании нового компонента является использование изолированной области.

Когда я пытался использовать изолированную область - ничего не работает. ngModel. $viewValue === undefined.

Когда я попытался использовать новую область (мой пример, не очень хороший вариант imho) - ngModel использует значение во вновь созданной области.

Конечно, я могу создать директиву с изолированной областью и работать с ngModel значением через "= выражение" (пример). Но я думаю, что работа с ngModelController является лучшей практикой.

Мои вопросы:

  • Можно ли использовать ngModelController с изолированной областью?
  • Если не возможно, какое решение лучше для создания такого компонента?

Ответ 1

Замена scope: true на scope: { datetime1: '=ngModel'} в вашей первой скрипке выглядит нормально - fiddle. К сожалению, ссылка на вашу "примерную" скрипку сломана, поэтому я не уверен, что вы там пробовали.

Итак, кажется, что ngModelController можно использовать с областью выделения.

Здесь более мелкая скрипта, использующая ng-модель в HTML/view, область выделения и $setViewValue в функции ссылок: fiddle.

Обновление. Я только что обнаружил что-то довольно интересное: если для свойства изоляционной области задано другое имя - например, скажем, dt1 вместо datetime1 - scope: { dt1: '=ngModel'} - он больше не работает! Я предполагаю, что когда мы require: 'ngModel', ngModelController использует имя в HTML/view (т.е. Значение атрибута ng-model) для создания свойства в области выделения. Поэтому, если мы укажем одно и то же имя в хеше объекта, все будет хорошо. Но если мы укажем другое имя, это новое свойство scope (например, dt1) не связано с требуемым ngModelController.

Здесь обновленная скрипка.

Ответ 2

Сделайте свою директиву более приоритетной, чем ngModel, и исправьте привязку модели к изолированной области. Я выбрал приоритет "100", который на том же уровне, что и директива ввода, после шаблонов с высоким приоритетом, таких как ngRepeat, но до значения по умолчанию 0, что и используется ngModel.

Вот пример кода:

myDirective = function() {
  return {
    compile: function(tElement, tAttrs, transclude) {
      // Correct ngModel for isolate scope
      if (tAttrs.ngModel) {
        tAttrs.$set('model', tAttrs.ngModel, false);
        tAttrs.$set('ngModel', 'model', false);
      }

      return {
        post: function(scope, iElement, iAttrs, controller) {
          // Optionally hook up formatters and parsers
          controller.$formatters.push(function(value) {
             // ...
          })

          // Render
          return controller.$render = function() {
            if (!controller.$viewValue) {
              return;
            }
            angular.extend(scope, controller.$viewValue);
          };
        }
      };
    },
    priority: 100,
    require: '^ngModel',
    scope: {
      model: '='
    },
  };
}

Во время компиляции директива проверяет наличие атрибута ngModel. Эта проверка работает с нормализованным значением, используя Angular Attributes. Если атрибут присутствует, он заменяется на "model" (не "ngModel" ), который является именем данных, связанным с нашим изолятом. Однако мы также должны создать атрибут, чтобы Angular мог выполнять привязку данных для нас. Оба атрибута могут быть (по вашему выбору) изменены с помощью параметра false, который оставляет DOM неизменным.

Ответ 3

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

Итак, проблема состоит из нескольких частей:

  • ваша настраиваемая директива требует некоторых частных свойств, то есть изолированной области
  • DOM node может иметь только одну область действия, все директивы совместно используют ее
  • ngModel = "что-то" связывается с "чем-то" в этой общей (изолированной) области видимости, и это фактическая проблема.

Итак, первым моим шагом было переписать мою директиву, чтобы использовать scope:true вместо scope:{...} (на самом деле это было требование, потому что я хотел использовать некоторые свойства глобальной области видимости в моей директиве transcluded content): такие вещи, как attrs.$observe(), $scope.$parent.$watch() и т.д.

Затем в compile() я повторно привязал ngModel к свойству parent scope: attrs.$set('ngModel', '$parent.' + attrs.ngModel, false). И это все.

Вот моя директива, в которой отсутствует незаменимый код:

angular.module('App', []).directive('dir', function () {
    return {
        /* This one is important: */
        scope:true,
        compile:function (element, attrs, transclude) {
            /* The trick is here: */
            if (attrs.ngModel) {
                attrs.$set('ngModel', '$parent.' + attrs.ngModel, false);
            }

            return function ($scope, element, attrs, ngModel) {
                // link function body
            };
        }
    };
});

Ответ 4

Попробуйте следующую версию:

.directive('myDir', function() {
    return {
        restrict: 'EA',
        scope:    {
                    YYY: '=ngModel'
                  },
        require:  'ngModel',
        replace:  true,
        template: function render(element, attrs) {
            var type = attrs.type || 'text';
            var required = attrs.hasOwnProperty('required') ? " required='required'" : "";
            return "<input ng-model='YYY' type="' + type + '" + required + ' />';
                  }
    };
});