Могу ли я использовать 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 + ' />';
}
};
});