简体   繁体   中英

How do I ensure a directive's link function runs before a controller?

In our application we have a view being loaded via a simple route.

$routeProvider
    .when('/', {
        template: require('./views/main.tpl.html'),
        controller: 'mainCtrl'
    })
    .otherwise({
        redirectTo: '/'
    });

This associates the mainCtrl controller with the view. The view looks like this:

<left-rail>
    <filter-sub-rail
        id="filter-rail"
        show-all-filter="isAdmin"
        components="components">
    </filter-sub-rail>
</left-rail>

<div class="endor-Page-content endor-Panel">
    <action-bar
        selected-components="selectedComponents"
        toggleable-columns="toggleableColumns"
        refresh-component-list="refreshComponentList()">
    </action-bar>
    <filter-pills></filter-pills>
    <div class="js-endor-content endor-Panel-content endor-Panel-content--actionBarHeight">
        <component-table
            components="components"
            selected-components="selectedComponents"
            toggleable-columns="toggleableColumns">
        </component-table>
    </div>
    <spinner id="cmSpinner" large="true" center="true"></spinner>
    <modal></modal>
</div>

The third to last line contains a <spinner> directive. This directive creates a spinner graphic while data is being loaded. It has an associated Angular service called spinnerApi . The spinner directive registers the spinner with the spinner API, allowing other services to inject the spinner API and call show and hide methods, passing in an ID, to show/hide the desired spinner.

In mainCtrl there is a function that begins loading some data the minute the controller runs.

//spinnerApi.show('spinner1');
var data = Segment.query({
    /*...truncated for brevity...*/
}, function () {
    $scope.components = data;
    spinnerApi.hide('spinner1');
});

You can see the first call to spinnerApi.show is commented out. That's because at this point the spinner directive has not had its link function run and the spinner is not yet registered with the API. If I uncomment that line I get an exception because there is no spinner called spinner1 yet. However, by the time the callback runs from the query the spinner is available and the call succeeds.

How can I make sure that the spinner directive runs and registers the spinner with the API before I begin loading data in the controller and avoid this race condition?

Think it another way:

Make your controller publish a flag on the scope meaning if the api is loading. Pass this flag to the directive in the template and watch it for toggling the spinner in the directive link function. Prefer prelink function to postlink function for working with element display (DOM rendering optimization). Set the spinner visibility at prelink time outside the watcher ! otherwise it would wait for the first digest loop to occur to hide the spinner that you've already paid the cost to display once !

If several behaviours can impact this display flag, create a service to hold this flag, publish this service on the scope in your controller, and provide the flag as an input to the directive in the template. Injecting this service in the directive would imply too much coupling between your spinner directive and its visibility condition.


Edit from question author:

This answer gave me an idea that worked great and seems pretty minimal. I didn't want the controller or the directive to be doing any weird waiting logic so it made sense that something should happen in the spinnerApi service instead (this directive is supposed to be re-usable anywhere in the application).

Here is my spinnerApi service, modified to queue hide/show/toggle events if the spinnerId hasn't been registered yet. When the register method runs it looks in the queue to see if there is anything to do.

module.exports = angular.module('shared.services.spinner-api', [])
    // Simple API for easy spinner control.
    .factory('spinnerApi', function () {
        var spinnerCache = {};
        var queue = {};
        return {
            // All spinners are stored here.
            // Ex: { spinnerId: isolateScope }
            spinnerCache: spinnerCache,

            // Registers a spinner with the spinner API.
            // This method is only ever really used by the directive itself, but
            // the API could be used elsewhere if necessary.
            register: function (spinnerId, spinnerData) {

                // Add the spinner to the collection.
                this.spinnerCache[spinnerId] = spinnerData;

                // Increase the spinner count.
                this.count++;

                // Check if spinnerId was in the queue, if so then fire the
                // queued function.
                if (queue[spinnerId]) {
                    this[queue[spinnerId]](spinnerId);
                    delete queue[spinnerId];
                }

            },

            // Removes a spinner from the collection.
            unregister: function (spinnerId) {
                if (!this.spinnerCache[spinnerId]) throw new Error('Spinner "' + spinnerId + '" does not exist.');
                delete this.spinnerCache[spinnerId];
            },

            // Show a spinner with the specified spinnerId.
            show: function (spinnerId) {
                if (!this.spinnerCache[spinnerId]) {
                    queue[spinnerId] = 'show';
                    return;
                }
                this.spinnerCache[spinnerId].visible = true;
            },

            // Hide a spinner with the specified spinnerId.
            hide: function (spinnerId) {
                if (!this.spinnerCache[spinnerId]) {
                    queue[spinnerId] = 'hide';
                    return;
                }
                this.spinnerCache[spinnerId].visible = false;
            },

            // Hide/show a spinner with the specified spinnerId.
            toggle: function (spinnerId) {
                if (!this.spinnerCache[spinnerId]) {
                    queue[spinnerId] = 'toggle';
                    return;
                }
                this.spinnerCache[spinnerId].visible = !this.spinnerCache[spinnerId].visible;
            },

            // Show all spinners tracked by the API.
            showAll: function () {
                for (var key in this.spinnerCache) {
                    this.show(key);
                }
            },

            // Hide all spinners tracked by the API.
            hideAll: function () {
                for (var key in this.spinnerCache) {
                    this.hide(key);
                }
            },

            // Hide/show all spinners tracked by the API.
            toggleAll: function () {
                for (var key in this.spinnerCache)
                    this.spinnerCache[key].visible = !this.spinnerCache[key].visible;
            },

            // The number of spinners currently tracked by the API.
            count: 0
        };
    });

compile runs before your controller , that runs before your link function. So, whatever you are doing in your link declaration (changing the element, adding attributes, etc) should be done in your compile phase.

Since you are using a service that shares states between directives and controllers, you could just add and remove spinners at will, regardless of the order they are called. That means, taking from your code, that instead of registering a spinner, you'll change it's state when you actually reach your link in your directive:

module.exports = angular.module('shared.directives.spinner', [
  require('services/spinner-api').name
]).directive('spinner', function (spinnerApi){
  return {
    restrict: 'AE',
    template: '<div ng-show="visible" class="coral-Wait" ng-class="{ \'coral-Wait--center\': center, \'coral-Wait--large\': large }"></div>',
    replace : true,
    scope   : {},
    priority: 100,
    compile : function (){
      return {
        pre: function (scope, element, attrs, controller){
          // Get the spinner ID, either manually specified or
          var spinnerId = typeof(attrs.id) !== 'undefined' ? attrs.id : spinnerApi.count;
          // Set isolate scope variables.
          // scope.visible = !!attrs.visible || false;
          scope.center = !!attrs.center || false;
          scope.large = !!attrs.large || false;

          // Add the spinner to the spinner API.
          // The API stores a simple hash with the spinnerId as
          // the key and that spinner's isolate scope as the value.
          spinnerApi.register(spinnerId, scope);
        }
      };
    }
  };
});


app.factory('spinnerApi', function(){
   var spinnerApi;
   spinnerApi.repo = {};
   spinnerApi.show = function(name){
     if (!spinnerApi.repo[name]) {
       spinnerApi.repo[name] = {show: false}; 
     }
     spinnerApi.repo[name].show = true;
   } 
   spinnerApi.register = function(id, scope){ 
     if (!spinnerApi.repo[name]){
       spinnerApi.repo[name] = {show: false};
     }
     spinnerApi.repo[name].scope = scope;
     scope.$watch(function(){
       return spinnerApi.repo[name].show;
     }, function(newval, oldval){
        if (newval){
          spinnerApi.repo[name].scope.visible = newval;
        }
     });
   };
   /* "pseudo" code */
   return spinnerApi;
});

Although I think your service shouldn't be aware of your scopes. Services should hold "raw state", and your directives should act on that. That means, the $watch should really go inside your isolated scope in your directive if at all.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM