What steps can I take to guarantee that a directive's link function is executed prior to a controller?

Our application features a view that is loaded through a basic route setup.

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

This configuration links the mainCtrl controller to the view, which contains the following structure:

<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>

Within this structure, there is a directive called <spinner>. This directive displays a spinner graphic while data is being loaded and interacts with an Angular service named spinnerApi. By registering the spinner with the spinner API, other services can inject the spinner API and utilize methods like show and hide, passing in an ID to control the visibility of specific spinners.

In the mainCtrl controller, there is a function responsible for initiating data loading as soon as the controller is activated.

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

The initial call to spinnerApi.show is currently commented out. This is done because the spinner directive has not been fully processed at that point, and the spinner has not been registered with the API. Uncommenting this line at this stage would result in an exception since the spinner named 'spinner1' is not yet available. However, once the query callback is executed, the spinner becomes operational, allowing the call to succeed without issues.

To avoid this race condition and ensure that the spinner directive is processed and registered with the API before data loading begins, how can this be achieved?

Answer №1

Consider a different approach:

Implement a method in your controller to set a flag on the scope indicating if the API is loading. Pass this flag to the directive within the template and utilize it in the directive's link function to toggle the spinner. It is recommended to use the prelink function over the postlink function for better performance when working with element display (DOM rendering optimization). Set the spinner visibility during prelink execution outside of the watcher to avoid waiting for the first digest loop to hide the spinner which has already been displayed once!

If multiple behaviors can affect the display flag, create a service to manage this flag. Share this service on the controller's scope and pass the flag as an input to the directive in the template. Avoid injecting this service directly into the directive to prevent excessive coupling between the spinner directive and its visibility condition.


Edit from question author:

This solution inspired me to develop a minimal and effective idea. I wanted to centralize any logic related to waiting in the 'spinnerApi' service instead of cluttering the controller or directive with such tasks (for reusability across the application).

Below is the updated 'spinnerApi' service, enhanced to queue hide/show/toggle events until the spinnerId registration occurs. The 'register' method now checks the queue for pending actions when invoked.

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
        };
    });

Answer №2

Compile is executed before the controller, which in turn runs before the link function. Therefore, any modifications or additions you are making in your link declaration should actually be done during the compile phase.

Given that you are utilizing a service to manage states shared between directives and controllers, you have the flexibility to add or remove spinners as needed, regardless of their sequence of execution. This means, based on your code snippet, instead of registering a spinner, you can simply toggle its state when reaching the link function 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){
          // Obtain the spinner ID, either specified manually or default to spinnerApi count.
          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 maintains a simple object with spinnerId as key 
          // and the spinner's isolate scope as 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;
});

In my opinion, services should not directly interact with scopes. Services should hold the "raw state", leaving it to directives to handle actions based on that state. Consequently, the $watch logic should ideally be integrated within the isolated scope of your directive.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Obtaining a cookie in Vue.js independently: a step-by-step guide

After setting a cookie using laravel, I'm looking to retrieve it in vue.js without relying on or installing any external dependencies. Can anyone please suggest a way to achieve this without extra tools? Your guidance would be greatly appreciated! ...

Explore ways to incorporate special symbols in a jQuery array

I'm looking to include special characters in a jQuery array. I'm not quite sure how to do this. Currently, my code is: $scope.categories = ['Red', 'White', 'Rose', 'Sparkling']; and I would like it to be: ...

Default modal overlay closure malfunctioning; encountering errors when manually managed through jQuery

A custom overlay has been designed and implemented. <div class="overlay modal" id="11"> <div class="background-overlay"></div> <div class="description"> <div class="hidden-xs"> <img src=' ...

Controller function failing to trigger

I'm new to asking questions, so if I've made a mistake, please let me know. I've searched for an answer here but couldn't find one. Any help would be greatly appreciated. I'm attempting to load a list of "Countries" using a contro ...

Having trouble retrieving information from the Redux store and displaying it in the user interface component

I'm currently diving into the world of Redux and working on a tracker app, but I've hit a roadblock that's had me stuck for days. Your help would be greatly appreciated. Thank you. store.js import { createStore, applyMiddleware, compose } f ...

What is the method for sending an AJAX request with a dynamically looping ID number parameter in the URL

I am looking to make multiple AJAX calls with a loop parameter named [id] in the URL, starting from request.php?id=1 and ending at id=9. I want to send each call after a 3-second delay. As a JavaScript beginner, I'm unsure of where to begin implementi ...

Does Koa.js have a nested router similar to Express?

Are there any libraries available that offer Express-style nested routers? For example: var koa = require('koa'); var app = koa(); var Router = require('???'); var restApiRouter = Router(); restApiRouter.get('/', function*() ...

Can you explain the significance of the res.render callback parameter in Express 4.0 for Node.js?

Can you explain the role of the res.render callback argument? When would it be necessary to use this callback argument, especially when there is already a template specified as the first argument? The following code snippet is taken from the official doc ...

Having trouble retrieving the $_SESSION variable accurately from AngularJS

I am working with angularjs and need to retrieve a php session variable. I have created a php file named session.php with the following content: $_SESSION['phone'] = '55551864'; return json_encode($_SESSION['phone']); In my ...

Using Jquery to target all elements except for their child elements

I have the following code: <div id="mn"> <span></span> <span> <span></span></span> <span></span> <span> <span></span></span> <span></span> </div& ...

What is the best way to transfer data from a clicked table row to another component?

I am currently working on developing an email inbox component for a system where the emails are displayed in a table format. When a user clicks on a specific row, it should lead to another component for further details. Since the information is not rende ...

What is the best way to find the average time in Typescript?

I am dealing with an object that contains the following properties: numberOfReturns: number = 0; returns_explanations: string [] = []; departure_time: string = ''; arrival_time: string = ''; The departure_time property hold ...

Best practices for locating unique symbols within a string and organizing them into an array using JavaScript

Here is an example string: "/city=<A>/state=<B>/sub_div=<C>/type=pos/div=<D>/cli_name=Cstate<E>/<F>/<G>" The characters A, B, C, and so on are variables, and their count is not fixed. Can you determine how many ...

Issues with managing multiple user sessions in express-session

I've been struggling with an issue for a few days now and haven't been able to find a solution. I've scoured forums and documentation, but nothing seems to work. I have a website built in Node.js, using express-session and passport for sessi ...

Is it possible to automate the firing of setTimeout events using WebDriver?

Looking to test pages with numerous setTimeout functions, I'm searching for a way to expedite the code execution upon page load rather than waiting for it to run on its own. One idea is to inject custom JavaScript like this into the page before evalu ...

What could be preventing me from exporting and applying my custom JavaScript styles?

This is my primary class import React, { Component } from 'react'; import { withStyles } from '@material-ui/core/styles'; import styles from './FoodStyles'; class Food extends Component { render () { return ( & ...

Developing a JavaScript program that automatically updates the score in a Tic Tac Toe

I am currently working on developing a "Tic Tac Toe" game, but I have encountered an issue. Everything is functioning properly except for one aspect: when the game concludes and all squares are filled with either O or X, or when either X or O wins, it doe ...

"Encountering a challenge when trying to fetch the value of an undefined or null

When it comes to validating the delivery date entered, I have implemented the following code to ensure it is not earlier than the current date... I have utilized custom validation using jQuery... Here is my model: [UIHint("Date")] [DeliveryDateC ...

How can I apply JavaScript to aggregate child node values and assign them to the parent in JSON data format?

I receive a dynamic JSON from the server, which has varying structures. Each data entry consists of a chapter with stages and/or review sets at the root level. If a stage exists, there will be either a review set array or another stage. The review set cont ...

Error: Unable to access the property 'fontSize' as it is undefined

<!DOCTYPE HTML> <html> <head> <title>Interactive Web Page</title> <link id="mycss" rel="stylesheet" href="mycss.css"> <script> function resizeText(size) { va ...