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
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
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:
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:
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
Thanks Rado, I got it working using ng-repeat and {{}} in the template.
Louis