How do I set ng-options in angular programmatically (directive, perhaps)?

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.