I have a Utils Service which is very heavy. I Want to use some of the functions defined in it on a particular user action. As this service is heavy I want to instantiate it lazily(on user action).
How do I achieve this?
Service
module.service('Utils', function (dep1, dep2) {
this.method1 = function () {
// do something
}
// other methods
});
Controller
module.controller('AppCtrl', function ($scope) {
// I don't want to inject Utils as a dependency.
$scope.processUserAction = function () {
// If the service is not instantiated
// instantiate it and trigger the methods defined in it.
}
});
Markup
<div data-ng-controller="AppCtrl">
<button data-ng-click="processUserAction()"> Click Me </button>
</div>
You can use $injector service to get services anywhere: https://docs.angularjs.org/api/auto/service/$injector. Inject the $injector into your controller, and whenever you need a service use:
This worked fine for me, the service is instantiated only on the $injector call, no error thrown.
angular.module('yp.admin')
.config(['$stateProvider', '$urlRouterProvider', 'accessLevels', '$translateWtiPartialLoaderProvider',
function ($stateProvider, $urlRouterProvider, accessLevels, $translateWtiPartialLoaderProvider) {
$stateProvider
.state('admin.home', {
url: "/home",
access: accessLevels.admin,
views: {
content: {
templateUrl: 'admin/home/home.html',
controller: 'AdminHomeController'
}
}
});
}])
.service('UtilsService', function() {
console.log('utilsSerivce instantiated');
return {
call: function() {
console.log('Util.call called');
}
};
})
.controller('AdminHomeController', ['$scope', '$rootScope', 'UserService', '$injector',
function($scope, $rootScope, UserService, $injector) {
$injector.get('UtilsService').call();
}]);
console gives me this:
stateChangeStart from: to: admin.home
stateChangeSuccess from: to: admin.home
utilsSerivce instantiated
Util.call called
If you want to delay loading the JS you should have a look at the ocLazyLoad Module: https://github.com/ocombe/ocLazyLoad. It addresses all sorts of lazy loading use cases and yours sounds like a good fit for it.
Related
I,m working on a quite old project which using Angular1.5.3 and now I want to add a new component. I have configed everything as the other component, now the newpage can be display(.html wroks fine) , but the data is not loaded(controller). and I got the error:
The controller with the name 'MyPageController' is not registered
here is some code of the project:
in mypage.controller.js:
(function () {
'use strict';
angular
.module('forecastAngular')
.controller('MypageController', MypageController);
/** #ngInject */
function MyPageController($scope, $rootScope, $state, $http, $timeout, usSpinnerService, dataFactory, $q, CommonServices, baseUrl) {
...
}
and in the index.route.js:
(function () {
'use strict';
angular
.module('forecastAngular')
.config(routerConfig);
/** #ngInject */
function routerConfig($stateProvider, $urlRouterProvider) {
$stateProvider
.state('mypage', {
url: '/mypage',
views: {
'body': {
templateUrl: 'app/myPage/mypage.html',
controller: 'MypageController',
}
},
data: {
authorizedRoles: ['Admin', 'Manager', 'Director', 'Registered User', 'HR'],
menuName: "mypage",
isShowSearch: false,
isShowDomain: false
}
});
}
and I also searched for this problem , someone said inject the controller use :
angular.module('myApp', []).controller('MyController', [function() {
// ...
}]);
but I tried . not work for me . and the other component is config like what I have paste above. and it works fine.
can any one tell me how can I register the controller in right way?
The function's name is wrong.
.controller('MyPageController', MyPageController);
Should be:
.controller('MyPageController', UtforcastController);
This function MyPageController does not exist.
you said your module name is forecastAngular, but in another code, later on you created another module, which reference to no other, and it does create an empty controller
Note:
module(x) is getter
module(x, []) is setter
So you trying to get a module that you may not defined in here:
angular
.module('forecastAngular')
.config(routerConfig);
I'm trying to unit test my states in an controller. What I want to do is stub out my items factory, since I have separate unit tests that cover that functionality. I'm having a hard time getting the $injector to actually inject the factory, but it seems like I'm letting the $provider know that I want to use my fake items object when it instantiates the controller. As a disclaimer I'm brand new to angular and would love some advice if my code looks bad.
Currently when I run the test I get the message:
Error: Unexpected request: GET /home.html
No more request expected
at $httpBackend (node_modules/angular-mocks/angular-mocks.js:1418:9)
at n (node_modules/angular/angular.min.js:99:53)
at node_modules/angular/angular.min.js:96:262
at node_modules/angular/angular.min.js:131:20
at m.$eval (node_modules/angular/angular.min.js:145:347)
at m.$digest (node_modules/angular/angular.min.js:142:420)
at Object.<anonymous> (spec/states/homeSpec.js:29:16)
It appears that my mocked items factory isn't being injected into the test. When I place a console.log line in the method I want to stub in the items factory I see that line being invoked.
The code I'm looking to test is as follows:
angular.module('todo', ['ui.router'])
// this is the factory i want to stub out...
.factory('items', ['$http', function($http){
var itemsFactory = {};
itemsFactory.getAll = function() {
// ...specifically this method
};
return itemsFactory;
}])
.controller('TodoCtrl', ['$scope', 'items', function($scope, items) {
// Do things
}])
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider){
$stateProvider
.state('home', {
url: '/home',
templateUrl: '/home.html',
controller: 'TodoCtrl',
resolve: {
items: ['items', function(items){
// this is the invocation that i want to use my stubbed method
return items.getAll();
}]
}
});
$urlRouterProvider.otherwise('home');
}]);
My test looks like this:
describe('home state', function() {
var $rootScope, $state, $injector, state = 'home';
var getAllStub = sinon.stub();
var items = {
getAll: getAllStub
};
beforeEach(function() {
module('todo', function($provide) {
$provide.value('items', items);
});
inject(function(_$rootScope_, _$state_, _$injector_) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
});
});
it('should resolve items', function() {
getAllStub.returns('getAll');
$state.go(state);
$rootScope.$digest();
expect($state.current.name).toBe(state);
expect($injector.invoke($state.current.resolve.items)).toBe('findAll');
});
});
Thanks in advance for your help!
Allowing real router in unit tests is a bad idea because it breaks the isolation and adds more moving parts. I personally consider $stateProvider, etc. stubs a better testing strategy.
The order matters in config blocks, service providers should be mocked before they will be injected in other modules. If the original modules have config blocks that override mocked service providers, the modules should be stubbed:
beforeAll(function () {
angular.module('ui.router', []);
});
beforeEach(function () {
var $stateProviderMock = {
state: sinon.stub().returnsThis()
};
module(function($provide) {
$provide.constant('$stateProvider', $stateProviderMock);
});
module('todo');
});
You just need to make sure that $stateProvider.state is called with expected configuration objects an arguments:
it('should define home state', function () {
expect($stateProviderMock.state.callCount).to.equal(1);
let [homeStateName, homeStateObj] = $stateProviderMock.state.getCall(0).args;
expect(homeStateName).to.equal('home');
expect(homeState).to.be.an('object');
expect(homeState.resolve).to.be.an('object');
expect(homeState.resolve.items).to.be.an('array');
let resolvedItems = $injector.invoke(homeState.resolve.items);
expect(items.getAll).to.have.been.calledOnce;
expect(resolvedItems).to.equal('getAll');
...
});
When my app starts I load some settings from a server. Most of my controllers need this before anything useful can be done. I want to simplify the controller's code as much as possible. My attempt, which doesn't work, is something like this:
app.factory('settings', ['$rootScope', '$http', '$q', function($rootScope, $http, $q) {
var deferred = $q.defer();
$http.get('/api/public/settings/get').success(function(data) {
$rootScope.settings = data;
deferred.resolve();
});
return deferred.promise;
}]);
app.controller('SomeCtrl', ['$rootScope', 'settings', function($rootScope, settings) {
// Here I want settings to be available
}]);
I would like to avoid having a lot of settings.then(function() ...) everywhere.
Any ideas on how to solve this in a nice way?
$http itself return promise you don't need to bind it inside the $q this is not a good practice and considered as Anti Pattern.
Use:-
app.factory('settings', ['$rootScope', '$http', '$q', function($rootScope, $http) {
return $http.get('/api/public/settings/get')
}]);
app.controller('SomeCtrl', ['settings',$scope, function(settings,$scope) {
settings.then(function(result){
$scope.settings=result.data;
});
}]);
Your way can be done as :-
app.factory('settings', ['$rootScope', '$http', '$q', function($rootScope, $http, $q) {
var deferred = $q.defer();
$http.get('/api/public/settings/get').success(function(data) {
deferred.resolve(data);
});
return deferred.promise;
}]);
app.controller('SomeCtrl', ['$scope', 'settings', function($scope, settings) {
settings.then(function(data){
$scope.settings=data;
})
}]);
Don't overload $rootScope if you wanted it you need to use $watch for the changes in $rootScope(Not recommended).
Somewhere you would need to "wait".
The only built-in way in Angular to completely absolve the controller from having to wait on its own for async data to be loaded is to instantiate a controller with $routeProvider's route's resolve property (or the alternative $stateProvider of ui.router). This will run controller only when all the promises are resolved, and the resolved data would be injected.
So, ng-route alternative - plunker:
$routeProvider.when("/", {
controller: "SomeCtrl",
templateUrl: "someTemplate.html",
resolve: {
settings: function(settingsSvc){
return settingsSvc.load(); // I renamed the loading function for clarity
}
});
Then, in SomeCtrl you can add settings as an injectable dependency:
.controller("SomeCtrl", function($scope, settings){
if (settings.foo) $scope.bar = "foo is on";
})
This will "wait" to load someTemplate in <div ng-view></div> until settings is resolved.
The settingsSvc should cache the promise so that it won't need to redo the HTTP request. Note, that as mentioned in another answer, there is no need for $q.defer when the API you are using (like $http) already returns a promise:
.factory("settingsSvc", function($http){
var svc = {settings: {}};
var promise = $http.get('/api/public/settings/get').success(function(data){
svc.settings = data; // optionally set the settings data here
});
svc.load = function(){
return promise;
}
return svc;
});
Another approach, if you don't like the ngRoute way, could be to have the settings service broadcast on $rootScope an event when settings were loaded, and controllers could react to it and do whatever. But that seems "heavier" than .then.
I guess the third way - plunker - would be to have an app-level controller "enabling" the rest of the app only when all the dependencies have preloaded:
.controller("AppCtrl", function($q, settingsSvc, someOtherService){
$scope.loaded = false;
$q.all([settingsSvc.load, someOtherService.prefetch]).then(function(){
$scope.loaded = true;
});
});
And in the View, toggle ng-if with loaded:
<body ng-controller="AppCtrl">
<div ng-if="!loaded">loading app...</div>
<div ng-if="loaded">
<div ng-controller="MainCtrl"></div>
<div ng-controller="MenuCtrl"></div>
</div>
</body>
Fo ui-router this is easily done with having an application root state with at least this minimum definition
$stateProvider
.state('app', {
abstract: true,
template: '<div ui-view></div>'
resolve: {
settings: function($http){
return $http.get('/api/public/settings/get')
.then(function(response) {return response.data});
}
}
})
After this you can make all application states inherit from this root state and
All controllers will be executed only after settings are loaded
All controllers will gain access to settings resolved value as possible injectable.
As mentioned above resolve also works for the original ng-route but since it does not support nesting the approach is not as useful as for ui-router.
You can manually bootstrap your application after settings are loaded.
var initInjector = angular.injector(["ng"]);
var $http = initInjector.get("$http");
var $rootScope = initInjector.get("$rootScope");
$http.get('/api/public/settings/get').success(function(data) {
$rootScope.settings = data;
angular.element(document).ready(function () {
angular.bootstrap(document, ["app"]);
});
});
In this case your whole application will run only after the settings are loaded.
See Angular bootstrap documentation for details
I'm currently working on an app where a button triggers a method that will emit an event to elsewhere. This works great, however I also want to add a url to trigger this action.
So currently my button looks like this
<a class="addJob" ng-click="addNewJob()" ng-controller="AddJobController"></a>
But what I really want is it to just be
<a class="addJob" href="/new"></a>
Now, I can't figure out how to do the routing for this. It would mean that when I go to /new, the AddJobController should be triggered.
When I go directly to http://www.example.com/new, it should still load the page properly and trigger that action.
I don't want to create a separate page for this route as it is an essential part of the app flow.
(Think of it like when you create a new note in trello.com)
One Option
If you are willing to move to uiRouter, this is a common pattern.
Copied and pasted directly from the uiRouter FAQ
https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-open-a-dialogmodal-at-a-certain-state
$stateProvider.state("items.add", {
url: "/add",
onEnter: ['$stateParams', '$state', '$modal', '$resource', function($stateParams, $state, $modal, $resource) {
$modal.open({
templateUrl: "items/add",
resolve: {
item: function() { new Item(123).get(); }
},
controller: ['$scope', 'item', function($scope, item) {
$scope.dismiss = function() {
$scope.$dismiss();
};
$scope.save = function() {
item.update().then(function() {
$scope.$close(true);
});
};
}]
}).result.then(function(result) {
if (result) {
return $state.transitionTo("items");
}
});
}]
})
Second Option
The second options would be to launch the modal the constructor of your controller. I have included a modalFactory. This is a common pattern. It allows your modals to be reusable, and cleans up your controllers. The uiRouter example above should use the factory pattern as well to abstract the modal setup out of the state config.
This example should work with ngRouter.
app.controller('addJobModalController', ['modalFactory', function(modalFactory) {
modalFactory.addJob();
}]);
app.factory('modalFactory', ['$modal', function($modal) {
return {
addJob: function() {
return $modal.open({
templateUrl: 'views/addjob-modal.html',
controller: 'AddJobController',
size: 'md'
});
}
}
}]);
The addJob() method returns the modal's promise. If you want, you can store this promise in the factory to be returned by another method so that another controller or service can act on the result of the modal.
I'd like to implement a setup where i can define a "root state" in the main module, and then add child states in other modules. This, because i need the root state to resolve before i can go to the child state.
Apparently, this should be possible according to this FAQ:
How to: Configure ui-router from multiple modules
For me it doesn't work:
Error Uncaught Error: No such state 'app' from ngBoilerplate.foo
Here is what i have:
app.js
angular.module( 'ngBoilerplate', [
'templates-app',
'templates-common',
'ui.state',
'ui.route',
'ui.bootstrap',
'ngBoilerplate.library'
])
.config( function myAppConfig ( $stateProvider, $urlRouterProvider ) {
$stateProvider
.state('app', {
views:{
"main":{
controller:"AppCtrl"
}
},
resolve:{
Auth:function(Auth){
return new Auth();
}
}
});
$urlRouterProvider.when('/foo','/foo/tile');
$urlRouterProvider.otherwise( '/foo' );
})
.factory('Auth', ['$timeout','$q', function ($timeout,$q) {
return function () {
var deferred = $q.defer();
console.log('before resolve');
$timeout(function () {
console.log('at resolve');
deferred.resolve();
}, 2000);
return deferred.promise;
};
}])
.run(function run( $rootScope, $state, $stateParams ) {
console.log('greetings from run');
$state.transitionTo('app');
})
.controller( 'AppCtrl', function AppCtrl ( $scope, Auth ) {
console.log('greetings from AppCtrl');
});
foo.js
angular.module( 'ngBoilerplate.foo', ['ui.state'])
.config(function config( $stateProvider ) {
$stateProvider
.state( 'app.foo', {
url: '/foo/:type',
views: {
"main": {
controller:'FooCtrl',
templateUrl: function(stateParams) { /* stuff is going on in here*/ }
}
}
});
})
.controller( 'FooCtrl', function FooCtrl( $scope ) {
console.log('deferred foo');
});
How do i make this work or what other approaches could i take to have something global resolved before every state (without defining a resolve on each state)?
I finally chose this approach which does the job for me:
// add all your dependencies here and configure a root state e.g. "app"
angular.module( 'ngBoilerplate', ['ui.router','templates-app',
'templates-common','etc','etc']);
// configure your child states in here, such as app.foo, app.bar etc.
angular.module( 'ngBoilerplate.foo', ['ngBoilerplate']);
angular.module( 'ngBoilerplate.bar', ['ngBoilerplate']);
// tie everything together so you have a static module name
// that can be used with ng-app. this module doesn't do anything more than that.
angular.module( 'app', ['ngBoilerplate.foo','ngBoilerplate.bar']);
and then in your app index.html
<html ng-app="app">
In the documentation the feature1 module depends on the application module. Try
angular.module( 'ngBoilerplate.foo', ['ngBoilerplate'])
I would of just commented but i do not have the rep. I know this is old but i had the same problem and came across this. One thing i am confused about is in app.js you do not import "ngBoilerplate.foo" but ngBoilerplate.library instead. I had the same problem and my solution was to inject sub modules into the top module instead of their parent.
My structure was module('ngBoilerplate'), module('ngBoilerplate.foo') and module('ngBoilerplate.foo.bar')'. I was injecting ngBoilerplate.foo.bar into ngBoilerplate.foo and the $stateProvider was failing. I needed to inject ngBoilerplate.foo.bar into top level ngBoilerplate.
I thought i would put this here in case anyone else sees this. The error i had was Uncaught TypeError: Cannot read property 'navigable' of undefined from ngBoilerplate.foo