Service Initialization - Forum - Kendo UI Builder - Progress Community
 Forum

Service Initialization

This question is not answered

Hello,

I have a couple services in my KUIB2 app and am running into some timing issues.  We are new to Angular so I'm sure there are better ways to code this but for now, this is what I have.

Both of my services initialize data using $http put calls (invokes).  The issue is that these return promises that may or may not be resolved by the time the view tries using the data.  What is the recommended way (within the KUIB architecture) to guarantee that the services are completely initialized before the view loads?

The following is one of the services:

class MylibService {
    constructor($injector, $compile, $http, $location) {
        this.injector = $injector;
        this.compile = $compile;
        this.http = $http;
        this.providerService = this.injector.get('providerService');
        this.serviceUri = this.getServiceUri();
    }  // constructor

    getServiceUri() {
        return this.injector.get('$q')((resolve, reject) => {
            if (this.serviceUri) {
                resolve(this.serviceUri);
            }
            else {
                this.providerService
                .providers()
                    .then(
                        (res) => {
                            this.serviceUri = res.MYAPI.serviceUri;
                            resolve(this.serviceUri);
                        },
                        (res) => {
                            this.serviceUri = '';
                            reject(this.serviceUri);
                        }
                        );
            }  // !this.serviceUri
        });
    }  // getServiceUri

    getMenus(sModule) {
        return this.injector.get('$q')((resolve, reject) => {
            this.getServiceUri()
            .then(
                (serviceUri) => {
                    if (this.ttMenus[sModule]) {
                        console.log('Getting ' + sModule + ' menus from local data');
                        resolve({ ttMenus: this.ttMenus[sModule] });
                    } else {
                        console.log('Getting ' + sModule + ' menus from server');
                        let req = { headers: { 'Content-Type': 'application/json'},
                                    timeout: 3000,
                                    processData: false },
                            jsonReq = { module: sModule };
                        this.http
                            .put(serviceUri + 'web/pdo/UI/Prm/Menus', JSON.stringify(jsonReq), req)
                            .then(
                                (res) => {
                                    this.ttMenus[sModule] = res.data.response.ttMenus.ttMenus;
                                    resolve({ ttMenus: this.ttMenus[sModule] });
                                },
                                (res) => { reject({ ttMenus: [] }); }
                            );
                    }
                });
            },
            (res) => {
                return reject({ ttMenus: [] });
            }
        )
    } // getMenus

...
} // class MylibService MylibService.$inject = ['$injector', '$compile', '$http', '$location']; export default MylibService;

This is how I'm trying to use the service:

import BaseController from './controller.js'

class SystemInfoTenantStatusCtrl extends BaseController {
    constructor($scope, $injector, stateData, MylibService, MyDomainService) {
        super($scope, $injector);

        this.scope = $scope;
        this.injector = $injector;
        this.mylibService = mylibService;
        this.myDomainService = myDomainService;
        this.ttPrmBank = [];
    }

    // Fired when custom html section is loaded
    includeContentLoaded() {

    }

    // Fired when custom html section loading failed
    includeContentError(e) {

    }

    // Fired when view content is loaded
    onShow($scope) {
        var removeWatch = $scope.$watch(() => {
            return angular.element("#gridStatus").html();
        }, (gridPrmBank) => {
            if (gridPrmBank !== undefined) {
                this.setupGrid();
                removeWatch();
            }
        });
    }

    setupGrid() {
        let req = { headers: { 'Content-Type': 'application/json'}, processData: false };
        this.injector
            .get('$http')
            .put(this.mylibService.serviceUri + 'web/pdo/UI/Prm/GetStatus', '', req)
            .then(
                (res) => {
                    this.ttPrmBank = res.data.response.ttStatus.ttStatus;
                    let gridPrmBank = angular.element("#gridStatus").kendoGrid({
                        dataSource: {
                            data: this.ttStatus,
                            pageSize: 999999999,
                            schema: {
                                model: {
                                    fields: {
                                        ...
                                    }
                                }
                            }
                        },
                        pageable: false,
                        sortable: true,
                        filterable: true,
                        selectable: 'single, row',
                        columns: [
                            ...
                        ],
                    });
                    angular.element("#loadingTempLbl").hide();
                },
                (rej) => {
                    this.scope.$emit('notification', {
                        type: 'error',
                        message: 'An error occurred loading stats.<br>Try refreshing and if error continues, contact support for assistance.'
                    });
                }
            );
    }  // setupGrid

}

SystemInfoTenantStatusCtrl.$inject = ['$scope', '$injector', 'stateData', 'mylibService', 'myDomainService'];

export default SystemInfoTenantStatusCtrl

Not shown is a navigation module that also uses the ttMenus from mylibService.getMenus() to determine how to build the side navigation panel.

The whole app is hit and miss, sometimes it loads correctly and sometimes it doesn't.  When it doesn't, refreshing (sometimes a few times) will eventually result in the app loading correctly.

Thanks,

Louis

All Replies
  • Hi Louis,

    To achieve the desired functionality, you can try using following approach:

    Create a custom module in the app extension folder. When state is changed inject the mylibService and request your external

    resource:

    For example:

    export default angular.module('app.extensions.module', [
    ]).run(['$state', '$rootScope', 'mylibService', function($state, $rootScope, mylibService) {
        $rootScope.$on('$stateChangeStart', (event, toState, toParams) => {
            // extend the resolve method of each state
            toState.resolve.mylibServiceUris= ['$q', ($q) => {
                return mylibService.getServiceUri();
            }];
        });
    }])
    .name;

    Also, I suggest you to not execute http calls with promises in the service constructors, since testing such services with unit tests

    can be very tricky (to mock the http call).

    More about custom modules and services you can find here:

    https://community.progress.com/community_groups/openedge_kendo_ui_builder/w/openedgekendouibuilder/2924.how-to-add-a-custom-module-and-service-in-kendo-ui-builder-2-0

    Best,

    Rado

  • Rado,

    I added the state change event with the resolve as follows:

    'use strict';
    
    import angular from 'angular';
    import jtslib from './jtslib.module';
    import jtsNavigation from './jtsNavigation.module';
    import jtsDomain from './jtsDomain.module';
    
    export default angular.module('app.extensions.module', [
        'jtslib', 'jtsNavigation', 'jtsDomain'
    ])
    .run(['$state', '$rootScope', 'jtslibService', function($state, $rootScope, jtslibService) {
        $rootScope.$on('$stateChangeStart', (event, toState, toParams) => {
            console.log("Initializing Menus: toState.resolve.allMenus");
            toState.resolve.allMenus = ['$q', ($q) => {
                return jtslibService.getMenus();
            }];
        });
    }])
    .name;
    

    The issue now is that I'm using the menus returned by the jtslibService in the Navigation module, and this executes before the call from the toState.resolve.allMenus block.  When I comment out the code to generate the side-navigation from ttMenus and either use the default from KUIB or hard code something, I don't get the error which makes me think it's a timing issue.  The error I'm getting when using ttMenus is:

    Uncaught Error: only one instance of babel-polyfill is allowed
    at Object.eval (webpack:///./~/babel-polyfill/lib/index.js?:10:9)
    at eval (webpack:///./~/babel-polyfill/lib/index.js?:29:30)
    at Object.eval (eval at globalEval (webpack:///./~/jquery/dist/jquery.js?), <anonymous>:1560:1)
    at __webpack_require__ (eval at globalEval (webpack:///./~/jquery/dist/jquery.js?), <anonymous>:55:30)
    at eval (webpack:///./src/vendor.js?:3:1)
    at Object.eval (eval at globalEval (webpack:///./~/jquery/dist/jquery.js?), <anonymous>:1310:1)
    at __webpack_require__ (eval at globalEval (webpack:///./~/jquery/dist/jquery.js?), <anonymous>:55:30)
    at eval (webpack:///multi_(webpack)-dev-server/client?:2:18)
    at Object.eval (eval at globalEval (webpack:///./~/jquery/dist/jquery.js?), <anonymous>:3233:1)
    at __webpack_require__ (eval at globalEval (webpack:///./~/jquery/dist/jquery.js?), <anonymous>:55:30)

    Followed by:

    TypeError: Cannot read property 'bind' of undefined
    at eval (index.js:92)
    at Scope.$emit (angular.js:18414)
    at createIt (kendo.angular.js:261)
    at createWidget (kendo.angular.js:233)
    at Object.link (kendo.angular.js:797)
    at eval (angular.js:1346)
    at invokeLinkFn (angular.js:10426)
    at nodeLinkFn (angular.js:9815)
    at compositeLinkFn (angular.js:9055)
    at nodeLinkFn (angular.js:9809)
    (anonymous) @ angular.js:14525
    (anonymous) @ angular.js:11008
    $emit @ angular.js:18416
    createIt @ kendo.angular.js:261
    createWidget @ kendo.angular.js:233
    link @ kendo.angular.js:797
    (anonymous) @ angular.js:1346
    invokeLinkFn @ angular.js:10426
    nodeLinkFn @ angular.js:9815
    compositeLinkFn @ angular.js:9055
    nodeLinkFn @ angular.js:9809
    (anonymous) @ angular.js:10154
    processQueue @ angular.js:16832
    (anonymous) @ angular.js:16876
    $digest @ angular.js:17971
    (anonymous) @ angular.js:18200
    completeOutstandingRequest @ angular.js:6274
    (anonymous) @ angular.js:6554
    setTimeout (async)
    Browser.self.defer @ angular.js:6552
    $evalAsync @ angular.js:18198
    (anonymous) @ angular.js:16704
    scheduleProcessQueue @ angular.js:16876
    $$resolve @ angular.js:16903
    doResolve @ angular.js:16912
    Promise resolved (async)
    $$resolve @ angular.js:16899
    resolvePromise @ angular.js:16887
    Deferred.resolve @ angular.js:16782
    proceed @ angular-ui-router.js:480
    invoke @ angular-ui-router.js:476
    (anonymous) @ angular-ui-router.js:455
    resolve @ angular-ui-router.js:559
    (anonymous) @ angular-ui-router.js:3632
    forEach @ angular.js:417
    resolveViews @ angular-ui-router.js:3626
    processQueue @ angular.js:16832
    (anonymous) @ angular.js:16876
    $digest @ angular.js:17971
    $apply @ angular.js:18269
    done @ angular.js:12387
    completeRequest @ angular.js:12613
    requestLoaded @ angular.js:12541
    XMLHttpRequest.send (async)
    (anonymous) @ angular.js:12587
    sendReq @ angular.js:12332
    serverRequest @ angular.js:12084
    processQueue @ angular.js:16832
    (anonymous) @ angular.js:16876
    $digest @ angular.js:17971
    $apply @ angular.js:18269
    done @ angular.js:12387
    completeRequest @ angular.js:12613
    requestLoaded @ angular.js:12541
    XMLHttpRequest.send (async)
    (anonymous) @ angular.js:12587
    sendReq @ angular.js:12332
    serverRequest @ angular.js:12084
    processQueue @ angular.js:16832
    (anonymous) @ angular.js:16876
    $digest @ angular.js:17971
    $apply @ angular.js:18269
    bootstrapApply @ angular.js:1917
    invoke @ angular.js:5003
    doBootstrap @ angular.js:1915
    bootstrap @ angular.js:1935
    angularInit @ angular.js:1820
    (anonymous) @ angular.js:33367
    fire @ jquery.js:3187
    fireWith @ jquery.js:3317
    ready @ jquery.js:3536
    completed @ jquery.js:3552

    Side Navigation logic:

    'use strict';
    
    import angular from 'angular';
    
    var jtsNav = angular.module('jtsNavigation', []);
    
    jtsNav.run(['$rootScope', '$state', 'jtslibService',
                   ($rootScope, $state, jtslibService) => {
    
        console.log('jtsNav->run() start');
        let homeState = 'default.module.application.home';
        let lastState;
        let ttMenus = [];
        let state = $state.get('module.default');
    
        // Build custom Side Navigation based on users access
        if (state && state.views && state.views['side-navigation'] ) {
            console.log('jtsNav side-navigation setup: Parameters, calling getMenus()');
            state.views['side-navigation'].templateUrl = "";
            
            /* Send request to get menus */
            jtslibService.getMenus().then(
                (res) => {
                    ttMenus = res.ttMenus;
                    setupSideNav(ttMenus, state);
                },  // getMenus() successful
                (res) => {
                    console.log('side-navigation call to getMenus() failed.');
                }  // getMenus() failed
            )  // jtslibService.getMenus('All').then()
        } // if (state && state.views && ...)
    
        console.log('jtsNav->run() finish');
    }]);
    
    export default angular.module('app.extensions.module', [
        'jtsNavigation'
    ]).name;
    
    
    // Setup side navigation html template based on ttMenus contents.
    // Menu format taken from app\src\scripts\common\side-navigation\index.html
    // ------------------------------------------------------------------------
    var setupSideNav = (ttMenus, state) => {
        console.log('jtsNav side-navigation setup, Parameters: ' + ttMenus.length);
        let htmlMenu  = '<ul kendo-panelbar="widget" id="jts-side-nav">';
    
        // Parameters View
        htmlMenu += '<li data-state="module.default.parameters" title="Parameters">' +
                    '   <i class="fa fa-cog"></i>' +
                    '   <span>Parameters</span>' +
                    '   <ul>';
        ttMenus.forEach((menu, index) => {
            if (menu.module !== 'Parameters')
                return;
            htmlMenu += '      <li class="nav-item" data-state="' +
                                 menu.dataState + '" title="' + menu.menuTitle + '">' +
                        '         <a class="k-link" ui-sref="' + menu.dataState + '">' +
                        '            <span>' + menu.menuName + '</span>' +
                        '         </a>'
                        '      </li>';
        });
        htmlMenu += '</ul></li>';
    
        // System Info View
        htmlMenu += '<li data-state="module.default.systemInfo" title="System Info">' +
                    '    <i class="fa fa-exclamation-triangle"></i>' +
                    '        <span>System Info</span>' +
                    '        <ul>';
        ttMenus.forEach((menu, index) => {
            if (menu.module !== 'System Info')
                return;
            htmlMenu += '      <li class="nav-item" data-state="' +
                                 menu.dataState + '" title="' + menu.menuTitle + '">' +
                        '         <a class="k-link" ui-sref="' + menu.dataState + '">' +
                        '            <span>' + menu.menuName + '</span>' +
                        '         </a>'
                        '      </li>';
        });
        htmlMenu += '</ul></li></ul>';
    
        state.views['side-navigation'].template = htmlMenu;
    
        console.log('jtsNav side-navigation: template created');
    
    };
    

    Louis

  • Hi Louis,

    You cannot call getMenus() in the toState.resolve.allMenus, since this method tries to manipulate the html( and the DOM) which have not been loaded by angular yet. There you can only call the getServiceUri() (like in the code snippet which I sent you) in order to populate service property which holds the urls based on the providers.

    To customize the side navigation, you need to override its template and/or controller. In the following thread, you can find more details how to achive that:

    https://community.progress.com/community_groups/openedge_kendo_ui_builder/f/255/p/34581/107186#107186

     Best,

    Rado

  • Rado,

    The getMenus() doesn't manipulate the DOM/html, it makes a REST call to get the users valid menu options and returns ttMenus.  The navigation module is what updates the html template based on the records returned in ttMenus.

    My issue is that the navigation module runs immediately, before the lib service starts.  How do you recommend running the navigation module so that it doesn't run until after the lib service initializes, and before the view is rendered so that the side navigation html template can be set?

    Also, if I start from the landing page, everything appears to work just fine.  When I try refreshing the browser while on any view, I get errors when the side navigation template is set.  If I hard code something into the side navigation template, or let the default "side-navigation\index.html" file load, I don't get any errors. Building my custom template takes a little longer because of the extra REST call to get the ttMenus table.

    Louis

  • Hi Louis,

    In the current version of KUIB builder we did not design side navigation to be changed (especially with dynamic call for getting menus values and dynamically building the html). I will put this requirement in our back log and we will prioritize it for the next major version of the product.

    At meantime, you can try overriding the entire side-navigation view, not just its template and perform http calls for getting menus there and then build html of the custom side-navigation. For example:

    ...
    
    import sideNavigationTemplate from './../common/side-navigation-extended/index.html';
    
    import sideNavigationController from './../common/side-navigation-extended/index.js';
    
    // Import your custom modules here:
    
    export default angular.module('app.extensions.module', [
    
     
    
    ])
    
    .run(['$state', '$rootScope', function($state, $rootScope) {
    
        var state = $state.get('module.default');
    
        if (state && state.views && state.views['side-navigation']) {
    
            state.views['side-navigation'].templateUrl = sideNavigationTemplate;
    
            state.views['side-navigation'].controller = sideNavigationController;
    
        }
    
    }]).name;


    Please note that I got sideNavigationTemplate and sideNavigationController from common/side-navigation-extended folder but you can put the view in a different place.

    Then in the controller of this extended view you can call your endpoint for getting urls and build the html for your dynamic navigation (the html can be similar to the original code of the side navigation view placed under the common/side-navigation folder). 

    Please give it try and let me know if it works for you.

    Best,

    Rado

  • Rado,

    Do I need to do anything then to disable the default side-navigation so that code doesn't have to load?  Or for now will both load and the extended will simply override the original?

    Louis

  • Hi Louis,

    The default side-navigation view will never be loaded in this case (since we override the state which loads it). So you do not need to do anything to disable the default one. 

    Best,

    Rado

  • Rado,

    I have not been able to successfully override the side navigation.

    I created a new side navigation controller as you suggested.  If I leave the templateURL populated, then when I set the template later I get the "Uncaught Error: only one instance of babel-polyfill is allowed" error.  I also tried setting up a resolve function.  I've tried using the resolve with and without my custom controller and I get the same results.  I don't get any errors but the menu is never rendered.  The following is the main part of the code for the resolve.

    In jtsNav.run:

        if (state && state.views && state.views['side-navigation'] ) {
            console.log('jtsNav side-navigation setup: Parameters, calling getMenus()');
            // state.views['side-navigation'].controller = JTSSideNavigationCtrl;
            state.views['side-navigation'].templateUrl = '';
            state.views['side-navigation'].template = '';
            state.views['side-navigation'].resolve = { rslv1: resolve };
        } // if (state && state.views && ...)

    After export:

    var resolve = ($q, jtslibService, $state) => {
        console.log('jtsNav->resolving...');
        let deferred = $q.defer();
    
        /* Send request to get menus */
        jtslibService.getMenus().then(
            (res) => {
                console.log('jtsNav->resolve(): ttMenus set successful');
                let htmlMenu = setupSideNav(jtslibService.ttMenus);
                let defState = $state.get('module.default');
                defState.views['side-navigation'].template = htmlMenu;
                deferred.resolve({template: htmlMenu});
                console.log('jtsNav->resolve(): resolved');
            },  // getMenus() successful
            (res) => {
                console.log('jtsNav->resolve(): ttMenus set failed.');
                deferred.reject();
            }  // getMenus() failed
        )  // jtslibService.getMenus().then()
    
        return deferred.promise;
    };  // resolve

    Based on what I've found, it appears that using the resolve is what I'm going to need to do but I'm not quite there yet.

    I noticed that if I set state.views['side-navigation'].template to a short hard coded menu instead of blank, this is what gets rendered, even though the template value is getting set to the full menu.  When I set the template to blank, this is what is rendered.

    Since the side-navigation renders before the new template gets generated, how do I force it to re-render after setting the template?

    Louis

  • Hi Louis,

    You do not need to build the template in the resolve of the state, since your code is not synchronous and your template will be populated in the future when the http call’s response is returned. You need to build it directly into the view and pass the data got from external call in the controller. For example:

    extensions/index.js

    'use strict';
    
    import angular from 'angular';
    import sideNavigationTemplate from './../common/side-navigation-extended/index.html';
    
    import sideNavigationController from './../common/side-navigation-extended/index.js';
    
    // Import your custom modules here:
    
    export default angular.module('app.extensions.module', [
        // Put your custom modules here:
    ]).run(['$state', '$rootScope', function($state, $rootScope) {
    
           var state = $state.get('module.default');
    
           if (state && state.views && state.views['side-navigation']) {
    
               state.views['side-navigation'].templateUrl = sideNavigationTemplate;
    
               state.views['side-navigation'].controller = sideNavigationController;
    
           }
    
       }]).name;
    

    side-navigation-extended/index.html

    <ul>
        <li class="nav-item" ng-repeat="item in data track by $index" data-state="{{item.state}}">
            <a ui-sref="{{item.state}}">
                <span>{{item.label}}</span>
            </a>
        </li>
    </ul>
    

    side-navigation-extended/index.js

    'use strict';
    
    const SideNavigationCtrl = function($scope, $element, $state, $q) {
        var data = [
            {
                id: 1,
                label: 'blankOne-extended',
                state: 'module.default.moduleOne.blankOne'
            },
            {
                id: 2,
                label: 'blankTwo-extended',
                state: 'module.default.moduleOne.blankTwo'
            },
            {
                id: 3,
                label: 'blankOne-second-extended',
                state: 'module.default.moduleTwo.blankOne'
            },
            {
                id: 4,
                label: 'blankTwo-second-extended',
                state: 'module.default.moduleTwo.blankTwo'
            }
        ];
    
        populateAditionalMenus();
    
        function populateAditionalMenus() {
            getServiceData().then((data) => {
                $scope.data = data;
            });
        }
    
        function getServiceData() {
            var deferred = $q.defer();
            setTimeout(() => {
                deferred.resolve(data);
            }, 500);
            return deferred.promise;
        }
    };
    
    SideNavigationCtrl.$inject = ['$scope', '$element', '$state', '$q'];
    
    export default SideNavigationCtrl
    

    Please note that in the code above I simulate http call with timeout, but on your case you need to execute similar logic in 
    jtslibService.getMenus().then(() => {
    // custom logic for populating model which is bound in html
    }) 
    

     

    I hope this helps.

    Best,

    Rado