I have a select
element I use to create a list of timezones. The simple angular way to do it is:
<select ng-model="item.timezone" ng-options="timezone for timezone in timezones"></select>
and then make sure the controller has $scope.timezones
set to an array of timezone strings.
This select appears in lots of places, and I don't want each controller to have to load it. So I move to a directive:
<select ng-model="item.timezone" timezones="true"></select>
And then render the various options using a directive:
.directive('timezones',function () {
return {
restrict: 'A',
require: '?^ngModel',
link: function ($scope,element,attrs,ngModel) {
element.empty();
_.each(moment.tz.names(),function (name) {
element.append('<option value="'+name+'">'+name+'</option>');
});
// this formatter does nothing, is just there so I can be sure it is being called with the correct value
ngModel.$formatters.push(function(modelValue){
return(modelValue);
});
}
};
})
The problem is that angular now processes it using my "timezones" directive and the angular "select" directive. This leads to my setting for ng-model to be completely ignored, and the value of the select set to "".
How do I get angular to recognize the model value and select the right element if I do it in my directive, or is there a better method for doing this?
UPDATE:
I tried just setting timezones
on the scope and ng-options
on the element using the directive, but it is still blank:
.directive('timezones',function () {
return {
restrict: 'A',
require: '?^ngModel',
link: function ($scope,element,attrs,ngModel) {
$scope.timezones = moment.tz.names();
element.attr("ng-options","timezone for timezone in timezones");
}
};
})
In this case I get an empty select, just one blank choice.
I've been struggling with this one for a while on a directive of my own and I've finally solved the problem.
I knew the solution was on using the compile function of the "child attribute directive" as it gets fired before the compile of the "parent element directive", but I was focused on adding the ngOptions
attribute to the tElement
, without realizing that the tAttrs
is shared between all directive compile functions.
So I think you can solve it as follows:
.directive('timezones', function() {
return {
controller: function($scope) {
$scope.timezones = moment.tz.names();
},
compile: function (tElement, tAttrs) {
var ngOptions = 'timezone for timezone in timezones';
tAttrs.ngOptions = ngOptions;
},
restrict: 'A'
};
@deitch: Try having your select partial inside of your directive as a "template", and configure the directive using 'isolate scope' to pass in the timezone to the scope of the directive.
Then you're directive would look something like this:
<timezone zone="item.timezone"></timezone>
I wrote up an example as a plunker: http://plnkr.co/edit/7Ysn8K0Wwvo8CbtkMhVv?p=preview
.directive('timezone', function() {
return {
restrict: 'A',
require: 'ngModel',
scope: {
myTimezone: '=',
allTimezones: '@allTimezones'
},
template: '<p>Directive timezone: {{myTimezone}}</p>' +
'<p>{{ allTimezones }}</p>' +
'<select ng-model="myTimezone" ng-options="timezone for timezone in {{allTimezones}}"></select>'
};
});
<div data-timezone data-my-timezone="data.myTimezone" data-all-timezones="{{timezones}}"></div>
I ended up not being able to solve it, so I passed on the select
directive entirely, and used compile phase to generate my own options:
.directive('timezones',function () {
return {
restrict: 'A',
require: '?^ngModel',
compile: function (element,attrs) {
_.each(moment.tz.names(),function (name) {
element.append('<option value="'+name+'">'+name+'</option>');
});
}
};
})
It just works.
What you have to do is create your directive with an isolated scope and pass the model as a binding. You create your select in the directive template and you use the timezones in the directive scope.
.directive('timezonesdir', function (timeZonesFactory) {
return {
restrict: 'AE',
template: '<select ng-model="timezone" ng-options="timezone for timezone in timezones"></select>',
scope: {
timezone: '='
},
link: function (scope) {
scope.timezones = // timezones
}
};
As you said, this does not work if you use this directive as an attribute of a select element, you will have to use it in a span or div, or something else.
<span timezone="item.timezone" timezonesdir></span>
I have created a jsfiddle with an example for you.
http://jsfiddle.net/limowankenobi/xo4r9q3j/
I have added a factory for timezones to decouple them from the directive.