I am building a dialog service. A dialog can have a controller, very similar to $mdDialog, like this:
myDialogService.show({
templateUrl: `<div ng-click="$ctrl.log()">Hello dialog</div>`,
controller: function() {
this.log = function() {
console.log("logged from myDialogController");
}
}
});
which works great. I invoke the controller that way:
locals.$scope = scope;
const invokeController = $controller(options.controller, locals, true);
const controller = invokeController();
if (options.controllerAs) {
scope[options.controllerAs] = controller;
} else {
const controllerAs = "$ctrl";
scope[controllerAs] = controller;
}
In angular-mock is the $componentController service, which can invoke component controllers. With my code, I only can invoke registered controllers, or given controller functions. This is not very helpful, as I only have components registered, not single controllers.
My question
Is it possible/recommended to use the $componentController in production? Or is there any AngularJS build in variant I have overseen?
$componentController belongs to ngMock module because it is useful for testing but is considered a hack in production. Since ngMock is big enough and isn't supposed to be available in production, it should be pasted in order to become available.
The proper way to solve this is to either have registered controllers that are reused as component controllers or import/export controller functions/classes with JS modules.
Since MdDialogController belongs to third-party module, isn't registered or exported and is small, it can be just pasted.
Related
I have an angular app where we have 4 different pages.Each page has its own controller. There is an home page which has a controller which routes to each page and its controller using
when('/a',{
templateUrl: './components/a.html',
controller:'aCtrl'
}).
when('/b',{
templateUrl: './components/b.html',
controller:'bCtrl'
}).
when('/c',{
templateUrl: './components/c.html',
controller:'cCtrl'
}).
when('/d',{
templateUrl: './components/d.html',
controller:'dCtrl'
}).
when('/home',{
templateUrl: './components/Home.html',
controller:homeCtrl'
}).
Now I want to share some data or some common functions between these controllers/pages. How can we do this? I googled it they say to use SERVICE. But I don't know in which controller I need to write the service. Can anybody give a good example for this.
A service in AngularJS is not written within a controller. It is bound to your app directly and can be used anywhere within your application. This is why Services are the recommended means of communication between controllers in AngularJS.
What you need to do is write a service like so:
angular.module('yourApp').service('serviceName', function () {....});
Within the service, you can:
Fetch data from an API end point (You can use the $http provider for this)
Define constant data (You can use Angular's constant provider for this)
Define some code that takes in some data and manipulates it and returns new data
Pretty much anything else you want to do with your data
Now, include the service in your controller as a dependency like so:
angular.module('yourApp').controller('yourController', function (serviceName) {
console.log(serviceName.getData());
// Do something with your data
});
Now within this controller, you have access to the data that the service has returned. Of course, the same service can be injected into multiple controllers, thereby making it possible to share data across controllers.
There are many ways you can share data.
event
services
$rootScope
Services provide an easy way for us to share data and functionality throughout our app. The services we create are singletons that can be injected into controllers and other services, making them the ideal place for writing reusable code.
var app = angular.module('app', []);
app.controller('leftCtrl', function ($scope,userService) {
left.btnClicked = function (object) {
userService.saveData(object);
}
});
app.controller('rightCtrl', function ($scope, userService) {
$scope.getData = userService.getData();
});
app.service('userService', function () {
var data = {};
this.saveData = function(object){
data = object;
}
this.getData = function(){
return data;
}
});
Dustin has the right approach. However there are times when you could use a different approach and that is to wrap the application in an AppController.
Everything that is in AppController can now be accessed. You could use this approach to put functions or constants that you want the child controllers of the application to have access to and don't have to inject services everywhere.
<body ng-controller="AppController">
<div ng-view></div>
</body>
The short story is that I'm trying to lever some data model code that's not written for angular in particular into an angular application. This code is written using ES6 import / export syntax for modules, and I'd like to keep using that. So I have something like:
export class DataModel {
//some stuff with promises
}
What I did was create a utility module that exposes the relevant Angular (1.5) services to the ES6 module system thusly:
import angular from 'angular';
export const services = {};
angular.injector(['ng', 'toastr']).invoke([
'$q',
'$http',
'$rootScope',
(
$q,
$http,
$rootScope
) => {
services.$q = $q;
services.$http = $http;
services.$rootScope = $rootScope;
},
]);
Then I can just import the $q library into my DataModel classes and hey presto, everything kind of works - I'm doing promises, and the appropriate scopes should update when the .then methods fire.
The problem is that this doesn't actually work. I'm 90% sure that the reason this doesn't work is that the $rootScope element I get from the angular.injector call isn't a singleton rootscope, it's a fresh new one that gets created just for this context. It does not share any scope linkage with the actual scope on the page (I can confirm this by selecting a DOM element and comparing services.$rootScope to angular.element($0).scope().$root). Therefore, when a promise resolves or a $http returns, I get the data but have the standard symptoms of not notifying a scope digest in the interface (nothing changes until I manually trigger a digest).
All I really want is a copy of the $q, $rootScope and $http services that angular uses live in the active page. Any suggestions are welcome. My next try will be to see if I can grab the relevant services from some .run block where I inject $q et al instead of doing it with the injector. That introduces some problematic timing issues, though, since I need to bootstrap angular, run the run block, and then expose the services to my data model. But the bootstrapping process requires the datamodel. It's a bit circular.
I'm answering this myself for now, but would love to see any other ideas.
I changed the angularServices code to look like:
import angular from 'angular';
import { Rx } from 'rx-lite';
export const servicesLoaded = new Rx.Subject();
export const services = {};
angular.module('app.services', []).run([
'$q',
'$http',
'$rootScope',
(
$q,
$http,
$rootScope
) => {
services.$q = $q;
services.$http = $http;
services.$rootScope = $rootScope;
servicesLoaded.onCompleted();
},
]);
Since I was already using rx-lite anyway. This allows me to do
import { services } from 'angularServices';
services.$http(options) // etc;
whenever I'm working in code that is run after the application bootstrap cycle. For the code that was running prematurely (it was just config stuff that was in a few places, I wrapped it inside the RxJS event thusly:
import { services, servicesLoaded } from '../../common/angularServices';
servicesLoaded.subscribeOnCompleted(() => {
services.$rootScope.$on('$stateChangeSuccess', () => {
//etc
That way I don't try to get in touch with $rootScope or $window before it actually exists, but the $q, $rootScope, and $http I've stashed a reference to in my services object is actually a real thing, and digests all fire properly.
And now hey presto, while my model layer references $http and $q, they'll be pretty easy to exchange with some other provider of promises and XHRs, making all the work I put into that not bound to angular 1.x. Whee.
I have developed an web application using Angular JS. I am getting few additional CR which needs to implemented in using TTD approach. We have return unit test cases using Jasmine and Karma. The challenge currently we face is when we try to write unit test case for multiple controllers. I have a main page return on Home Controller & its has an broadcast event in another controller. When i write a unit test case Object for the controller which has this broadcast event is not initialized.
Is there any way to inject the second controller as a dependent object. Answers with Reference Sample link or demo code is much appreciated.
You state you are using Jasmine and Karma so I assume you are unit testing. If you are "unit" testing you should test each controller individually while mocking, spy, all injected services.
beforeEach(inject(function ($rootScope, $controller) {
rootScope = $rootScope;
scope = $rootScope.$new();
controller = $controller('MyCtrl as ctrl', {
'$scope': scope
});
}));
it('', function(){
//Arrange
controller.counter = 0; // Your controller is listening on scope.$on to update this counter.
//Act
rootScope.$broadcast('xyz', {});
//Assert
expect(controller.counter == 1).toBe(true);
rootScope.$broadcast('xyz', {});
expect(controller.counter == 2).toBe(true);
rootScope.$broadcast('xyz', {});
expect(controller.counter == 3).toBe(true);
});
Just be careful with broadcast. Only a domain events (model updated/deleted/created), or something global (signin,signout) should travel over $broadcast. Otherwise, it should be replaced with a service + directive. An example is angular material https://material.angularjs.org/latest/api/service/$mdDialog that is 1 directive with a backing service that can be open/closed from anywhere.
You can inject any controller with the $controller service, e.g.
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller('MyCtrl', {
'$scope': scope
});
}));
see docs here:
https://docs.angularjs.org/api/ngMock/service/$controller
so what you do is inject that controller first, then your other controller. then the first controller will have been instantiated at the time the second gets instantiated.
I am new to angular, it seems to be possible to inject multiple controllers at once. However best practice is to generate a mock controller that behaves as you expect the second controller to behave. This reduces the number of things you are testing at once.
The following link may be helpful for creating a mock controller, http://www.powdertothepeople.tv/2014/08/28/Mocking-Controller-Instantiation-In-AngularJS-Unit-Test/ .
I'm trying to create directives on the fly, actually I achived that, but seams pretty hacky.
This was my first approach:
function create(myDir) {
angular.module("app").directive(myDir.name, function() {
return {
template:myDir.template
};
});
}
It didn't work because you can't register directives after application started.
based on this post: http://weblogs.thinktecture.com/pawel/2014/07/angularjs-dynamic-directives.html
I found out that I could use compileProvider to do the work, but since compileProvider isn't available outside config block, you need to put it out, so I did:
var provider = {};
angular.module("app",[]);
angular.module('app')
.config(function ($compileProvider) {
//It feels hacky to me too.
angular.copy($compileProvider, provider);
});
....
function create(myDir) {
provider.directive.apply(null, [myDir.name, function () {
return { template: myDir.template } }]);
render(myDir); //This render a new instance of my new directive
}
Surprisingly it worked. But I can't feel like being hacking the compileProvider, because I'm using it not in the way it was suppose to be, I would really like to know if is it possible to use the compileProvider properly after the application has started.
There is a list of dependencies that can be injected to config blocks (these are built-in $provide, $injector and all service providers) and a list of dependencies that can be injected to everywhere else (service instances and good old $injector). As you can see all that constant does is adding the dependency to both lists.
A common recipe for using providers outside config is
app.config(function ($provide, $compileProvider) {
$provide.constant('$compileProvider', $compileProvider);
});
I have been writing some Jasmine unit tests in Angular. In the first example I'm testing a controller.
myApp.controller('MyCtrl', function($scope, Config){
...
});
I have a configuration service (Config) that keeps configuration from the database and is injected into my controller. As this is a unit test, I want to mock out that configuration service altogether, rather than allowing execution to pass through it and using $httpBackend. Examples I found taught me about a $controller function I can use like this, in order to get an instance of my controller with my mocks injected in place of the usual collaborator:
beforeEach(inject(function($controller, $rootScope){
var scope = $rootScope.$new();
var configMock = {
theOnlyPropertyMyControllerNeeds: 'value'
};
ctrl = $controller('MyCtrl', {
$scope:scope,
Config: configMock
});
}));
But I also have other services that use the Config service. To help unit test them, I assumed there would be a similar $service function I could use to instantiate a service with whatever mocks I want to provide. There isn't. I tried $injector.get, but it doesn't seem to let me pass in my mocks. After searching for a while, the best I could come up with in order to instantiate a service in isolation (avoid instantiating its collaborators) is this:
beforeEach(function() {
mockConfig = {
thePropertyMyServiceUses: 'value'
};
module(function($provide) {
$provide.value('Config', mockConfig);
});
inject(function($injector) {
myService = $injector.get('MyService');
});
});
Is this the right way? It seems to be overriding the entire application's definition of the Config service, which seems maybe like overkill.
Is it the only way? Why is there no $service helper method?
For unit testing, it is common that you override a service for the sake of testing. However, you can use $provide to override an existing service instead of using inject, as long as you load the application before hand.
Assuming that you created Config using something like:
angular.moduel('...', [...]).factory('Config', function (...) {...});
If so, try this:
...
beforeEach(module("<Name of you App>"));
beforeEach(
module(function ($provide) {
$provide.factory('Config', function (...) {...});
});
);
...
After that, when you initialise your controller, it will get the mocked Config.