I would like to build a service that could check if there were any directives in use on a controller and if so build a directives object and populate it with objects for each directive on the controller. The benefit of this would be that I could build a type of API for scope inheritance automatically for all of my directives. Right now I'm essentially doing the same thing but by hand. Is there some property on the scope object that I can use to find the names of directives in use? Here's an example of what I mean
app.controller('controller1' ['$scope', function($scope) {
$scope.directivesAPI = {};
$scope.directivesAPI.privateScope = {};
}]);
app.directive('privateScope', function() {
restrict: A,
scope: true,
link: function(scope, elem, attr) {
scope.directivesAPI.privateScope.what = "My Job is to provide new features";
scope.directivesAPI.privateScope.how = "I share the scope using this fancy API";
}
})
<div ng-controller="controller1" privateScope>
{{directivesAPI.what}}
{{directivesAPI.how}}
</div>
In this way I can share the scope without polluting the parent scope. So my question is, rather than having to build the directivesAPI by hand I'd like to be able to do something more like this
app.controller('controller1' ['$scope', 'directivesAPI' function($scope,directivesAPI) {
$scope.directivesAPI = directivesAPI.build();
//The service is responsible for finding the directives and building the object for me.
}]);
If anyone has any ideas I'd love to hear them.
So, from what I understand, what you want is to be able to have a common "scope" that you can use between all of your directives?
If that's the case, then what you could do instead is simply create a service. You could use the service across all of your directives, and treat it as if it were a private "scope". Something like what I have below:
var myApp = angular.module('myApp', []);
myApp.controller('GreetingController', ['$scope', 'myPrivateScope',
function($scope, myPrivateScope) {
$scope.greeting = myPrivateScope.greeting;
}
]);
myApp.service('myPrivateScope', function () {
var srv = {};
srv.greeting = 'Hola!';
return srv;
});
You can use $injector to get the service in your directive. See the answer to AngularJS. Passing Service as an argument to Directive.
Related
I made an App (build using AngularJS 1.X) that has a lot of directives, most of which have there own isolated scope. So, for every new $scope i assign Underscore.js + Underscore.string variables _ and s to them, like so:
controller: function($scope){
$scope._ = _;
$scope._s = s;
$scope.foo = 'my-example-here';
}
Then do some cool stuff like this within there isolated scope templates:
<div ng-bind='s.humanize(some_id)'></div> # output: "My example here"
Unfortunately you cannot access global variables in these scopes.
So, ultimately, how to add a consistent variable or function to every AngularJS $scope?
Try this:
app.controller('MyCtrl', function($rootScope) {
$rootScope.derp = "foo!";
});
app.directive('myDir', function($rootScope) {
return {
scope { ... },
controller: function($rootScope) {
console.log($rootScope.derp);
}
};
});
Make sure to put ng-controller="MyCtrl" directive somewhere on the parent DOM element of your custom directive like so:
<div ng-controller="MyCtrl">
<div my-dir></div>
</div>
Try it please.
EDIT
Also, you can use constants as described by #georgeawg in comment.
I'm curious as to how I would go about unit testing what I think is an anonymous controller inside of a directive.
directive.js
app.directive('directive',
function() {
var controller = ['$scope', function($scope) {
$scope.add = function() { ... };
}];
return {
restrict: 'A',
scope: {
args: '='
},
templateUrl: '...',
controller: controller
};
}
};
Is a controller defined as such able to be unit tested? I have tried to initialize it several different ways. Currently I have it setup like this:
describe('The directive', function() {
var element,
scope,
controller;
var args = {
...
}
beforeEach(module('app'));
beforeEach(module('path/to/template.html'));
beforeEach(function() {
inject(function($compile, $rootScope, $controller) {
scope = $rootScope.$new();
scope.args = args;
element = angular.element('<div directive></div>');
template = $compile(element)(scope);
scope.$digest();
controller = element.$controller;
});
});
// assertions go here
});
I keep getting TypeError: 'undefined' is not an object (evaluating ...) errors, so I don't think I am initializing the controller correctly. I mainly want to know if something like this is unit testable without changing the directive's source code at all.
I'm not sure if what you are trying to do is possible. However, I do know that there is a much easier way and that is to make it a standard controller. (You seem to be aware of this already but it's worth pointing out.)
The logic in a controller really shouldn't be dependent on the directive anyway so by making a named controller you are separating concerns which is a good thing. You can even see this used in recommended style guides for AngularJS. Once you have the controller set up properly you shouldn't have any issues testing it. Splitting it out like that also helps in doing proper dependency injection making for simpler code and simpler tests.
I have the following new service:
var SignatureService = function ($scope) {
this.announce = function () {
alert($scope.specificName);
}
};
SignatureService.$inject = ['$scope'];
This is declared for the app as follows:
MyAngularApp.service('SignatureService', SignatureService);
Several other services are added to the app in exactly the same way, and they all seem to work OK. I then have a controller declared and defined as follows:
MyAngularApp.controller('MyController', MyController);
...
var MyController = function ($scope, Service1, Service2, $location, $modal, SignatureService) {
...
}
MyController.$inject = ['$scope', 'Service1', 'Service2', '$location', '$modal', 'SignatureService'];
I am simply using the slightly unconvcentionaly manner of defining the servivce and injecting it that is standard in the app I am working on, as this works for all existing services, and I would prefer to simply slot mine in as per standard.
When the controller loads, I get an [$injector:unpr] in the browser console, with the error info:
$injector/unpr?p0=$scopeProvider <- $scope <- SignatureService
You can't inject $scope into your custom service. It just doesn't make sense since SignatureService can be injected anywhere including other services and other controlles. What $scope is supposed to be if you say inject it into two nested controllers, which one should be injected?
Scope object ($scope) is always associated with some DOM node, it is attached to it. That's why you see $scope in controllers and directives. And this is the reason why you can't have it in service: services are not related to specific DOM elements. Of course you can inject $rootScope but this is unlikely what you need in your question.
Summary: $scope is created from the $rootScope and injected in necessary controllers, but you can't injected it into custom service.
UPD. Based on comments you want to use service to define reusable controller methods. In this case I would go with what I call mixin approach. Define methods in the service and mix them in the necessary controllers.
app.service('controllerMixin', function() {
this.getName = function() {
alert(this.name);
};
});
and then extend controller scope with angular.extend:
app.controller('OneController', function($scope, controllerMixin) {
angular.extend($scope, controllerMixin);
// define controller specific methods
});
app.controller('TwoController', function($scope, controllerMixin) {
angular.extend($scope, controllerMixin);
// define controller specific methods
});
This is pretty effective, because controllerMixin doesn't have any notion of $scope whatsoever, when mixed into controller you refer to the scope with this. Also service doesn't change if you prefer to use controllerAs syntax, you would just extend this:
angular.extend(this, controllerMixin);
Demo: http://plnkr.co/edit/ePhSF8UttR4IgeUjLRSt?p=info
http://plnkr.co/edit/C4mFd5MOLBD2wfm8bMhJ?p=preview
Let's take a simple example and say you want to display the value of a cookie regardless of what it is, but this could be a customer name or whatever you want. There seem to be so many options available: directives, services, directives with services, controllers - and no matter how many docs I review or blog posts I read, I still have some fundamental questions about the appropriate way to access data and then update the scope accordingly.
What's clouding my thought right now is the fact that there doesn't seem to be the equivalent of NgModelController for non ngModel capable DOM elements like span or div or anything besides user input. Basically, seeing how ngModelCtrl is utilized in the link function of a directive seems to make a lot of sense, it doesn't allow you to drown in scope soup and it nicely organizes your thoughts, but how do we achieve this decoupling with ngBind elements?
I think the answer is just 'use services', but perhaps maybe not in all cases is the thing that's gnawing at me. Suppose you want to display a very specific cookie (or a customer name) and you don't know where you want to display it, you could continually inject your custom CookieService where ever you go, but what about a specific directive that cleans things up: <specific-cookie></specific-cookie> Would we just inject our CookieService into that directive, or just access it via $cookies like we've done elsewhere.
Does the answer simply lie in whether or not you'll be accessing more than one cookie in the future? That is, if you only need one <specific-cookie></specific-cookie>, then just use $cookies in you're directive and move on with your life, or it is always appropriate to abstract away this type of call into a service, or am I just being super pedantic about understanding this.
Directive
angular-myapp.js
var app = angular.module('myApp', ['ngCookies']);
app.directive('cookie', ['$cookies', function($cookies) {
return {
scope: true,
controller: function($scope, $element, $attrs) {
$scope.cookie = $cookies[$attrs.cookie];
}
}
}]);
index.html
<div cookie="__utma">Cookie: {{cookie}}</div>
Controller
angular-myapp.js
app.controller('CookieCtrl', function($attrs, $cookies) {
this.value = $cookies[$attrs['getcookie']];
});
index.html
<a ng-controller="CookieCtrl as cookie" getCookie="__utma" href="/{{cookie.value}}">{{cookie.value}}</a>
Service
angular-myapp.js
app.controller('SomeCtrl', function($scope, CookieService) {
$scope.cookie = CookieService.getCookie('__utma');
});
app.service('CookieService', function($cookies) {
var getCookie = function(cookie) {
return $cookies[cookie];
};
return ({ getCookie: getCookie });
});
index.html
<div ng-controller="SomeCtrl">Cookie: {{cookie}}</div>
Directive with service
angular-myapp.js
app.directive('specificCookie', function(CookieService) {
return {
scope: true,
template: 'Cookie: <span ng-bind="cookie"></span>',
controller: function($scope, $element, $attrs) {
$scope.cookie = CookieService.getCookie('__utma');
}
}
});
index.html
<specific-cookie></specific-cookie>
Unless I'm misunderstanding some of your scenarios, the simplest (and proper) way to do this is to create a reusable directive that displays a cookie based on a name passed to it via its attribute.
app.directive('cookie', ['$cookies', function($cookies) {
return {
scope: {},
template: "<span>{{cookie}}</span>",
restrict: "E",
link: function(scope, element, attrs) {
attrs.$observe("name", function(newVal){
scope.cookie = $cookies[newVal];
});
}
};
}]);
The usage would be trivial (and page controller-independent):
<cookie name="__utma"></cookie>
<input ng-model="cookieName" type="text">
<cookie name="{{cookieName}}"></cookie>
the resulting DOM would be:
<span class="ng-binding">137862001.838693016.141754...</span>
<span class="ng-binding">GA1.2.838693016.1417544553</span>
As of recently I have been declaring functions and properties for my angularJS controllers in the following way (app is set to the main apps angular module):
app.controller('myController', ['$scope', function($scope) {
$scope.myProperty = "hello world";
$scope.myFunc = function() {
// do stuff
};
}]);
After a while the controllers' $scope grew to contain many utility functions and properties that are not being used directly in my views and would not be applicable to other controllers, so I changed it to this:
app.controller('myController', ['$scope', function($scope) {
var myProperty = 0, addOne;
addOne = function(i) {
return i++;
};
$scope.myFunc = function() {
myProperty = addOne(myProperty);
// do other stuff
};
}]);
This is working fine but is it okay to declare functions and properties the way shown above or should I extract them out into a service? Can I unit test var declared functions in my controller from jasmine/karma(tried but did not succeed)? What is the best pattern to follow?
I have looked into using the 'this' keyword to accomplish the same thing shown above but I don't think the added value would outweigh the amount of time it would take to convert everything to this pattern. If this is totally wrong please advise a better strategy.
--- Update ---
To answer my own question about testing var declared functions in the controller: How can we test non-scope angular controller methods?
As a general rule, if you're not going to use the variable or function in the view, don't attach it to the scope. So declare your variables and functions within the controller or on the scope based on it's use. You don't want to put unnecessary stuff on the $scope, as this will slow down the digest cycle. As a best practise I follow this format in my controller:
app.controller('MainCtrl', function($scope) {
// controller variables
var age = 123;
// scope variables
$scope.name = 'John';
// controller functions
var helloWorld = function() {
console.log('do work');
}
// scope functions
$scope.isOld = function() {
return age > 50;
}
});
services are singletons while controllers are not.
in addition, services are used to share functionality between many different controllers/directives.
your second code block looks really good to me for internal, controller specific functionality which you don't need to share in other places in your app, and its much better than putting unneeded stuff in the scope.