I 'm a bit worried If I am asking a noob question or if it is a javascript feature that I haven't been able to locate despite plenty of googling
I am adding a simple directive programmatically using $compile and all is working fine.
My question is this line
var el = $compile(newElement)($scope);
How do the double parenthesis work/ what do they do? Complete code below for reference but its just the parenthesis which I am not sure about.
var myApp = angular.module('myApp', []);
myApp.directive('myDirective', function() {
return {
template: 'Hello',
restrict: 'E'
}
});
myApp.controller('mainController', [
'$scope', '$compile', function($scope, $compile) {
$scope.addDirective = function() {
var newElement = angular.element(document.createElement('my-directive'));
var el = $compile(newElement)($scope);
angular.element(document.body).append(el);
};
}
]);
$compile returns another function. You can do something similarly:
function foo(greeting) {
return function(target) { console.log(greeting, target) };
}
foo('Hello, ')('world');
As you already know that parenthesis in javascript is an function invocation operator (and also grouping). In other words, with () operator you call a function. From here it is clear that the code
$compile(newElement)($scope);
means that result of $compile(newElement) is a function, so it can be executed. This returned function accepts one parameter - a scope object in which context new DOM element should be compiled.
$compile(tElement, tAttrs, transclude) returns the Directive link: (post-link) function.
app.directive('exampleDirective', [function () {
return {
restrict: 'A',
scope: { value: '=value'},
template: template,
link: function (scope, element, attr) {
scope.count = 0;
scope.increment = function() {
scope.value++;
};
}
};
}]);
In this case, $compile('<div example-directive></div>'); will return the link: function so you can call it with arguments (scope as first) and instanciate the context.
It's all just standard Javascript syntax for calling functions. What might be confusing is that $compile is a function that returns a function. So
$compile(newElement)
is itself function, and can be called like any other function, which is what's happening when writing
$compile(newElement)($scope);
You can break this up into separate lines if you wish, which might make it clearer:
var linkFunction = $compile(newElement);
linkFunction($scope);
You can refer to the usage of $compile in the docs.
As a side-note, I would be wary of using $compile directly: you might be overcomplicating things, and there could be simpler alternatives (it has been very rare that I've ever had to use it).
Related
I'm trying to figure out AngularJS directives. I've got the following JSFiddle with an example of something I'm trying to do. https://jsfiddle.net/7smor9o4/
As you can see in the example, I expect the vm.alsoId variable to be equal to vm.theId. In the template vm.theId displays the correct value but vm.alsoId does not.
What am I doing wrong? How could I accomplish my goal.
If it helps the final idea is to execute something as the following:
function directive(service) {
var vm = this;
vm.entity = null;
init();
function init() {
service.getEntity(vm.theId).then(function (entity) {
vm.entity = entity;
});
}
}
As you've noticed, the bindToController bindings are not immediately available in the controller's constructor function (unlike $scope, which are). What you're looking for is a feature introduced with Angular 1.5: Lifecycle Hooks, and specifically $onInit.
You had the right idea; simply replace your init function definition and invocation as follows:
vm.$onInit = function () {
service.getEntity(vm.theId).then(function (entity) {
vm.entity = entity;
});
};
And here is your updated fiddle.
(Alternatively, without this solution, you'd have needed a watch.)
Angular recommends that you bind a controller "only when you want to expose an API to other directives. Otherwise use link."
Here's a working fiddle using the link function.
angular.module('app', [])
.directive('directive', directive);
angular.element(function() {
angular.bootstrap(document, ['app']);
});
function directive() {
return {
restrict: 'E',
scope: {
theId: '<'
},
template: `
alsoId: <span ng-bind="alsoId"></span>
theId: <span ng-bind="theId"></span>`,
link: link
};
}
function link(scope, element, attrs) {
init();
function init() {
scope.alsoId = scope.theId;
}
}
The problem
I am unit testing a directive has no controller or template, only a link function. The directive requires ngModel and calls one of its functions in the link function. I want to spy on ngModel in my unit tests to ensure the right function is being called.
The code
Directive:
angular.module('some-module').directive('someDirective', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, controller) {
controller.doSomething(); //Calls some random function on the ngModel controller
}
};
});
What I've tried
I've tried to inject a spy ngModel like this:
beforeEach(module(function($provide) {
$provide.factory('ngModelDirective', function() {
return {};
});
$provide.factory('ngModelController', function() {
return function() {};
});
}));
As I discovered on this question, trying to override built-in properties causes an error to be thrown, and is bad practice.
So then I tried to test the directive the way the Angular docs say to:
var $scope = $rootScope.$new();
var element = $compile('<div some-directive></div>')($scope);
And spy on NgModelController like this:
var ngModelControllerSpyDoSomething = sinon.spy(element.controller('ngModel'), 'doSomething');
But this doesn't work, because one $compile is run, it executes the link function, so I'm not spying on it until it's too late (the spy is coming back as never having been called). This is also the case if I put $scope.$digest(); before or after creating the spy.
You will have to add your spy to the $scope you are injecting into the $compile-Function and then link it within the actual directives HTMLngModel`, so:
var $scope = $rootScope.$new();
$scope.mySpy = // create a stub function with sinon here
var element = $compile('<div some-directive ng-model="mySpy"></div>')($scope);
What I want to do:
I'm trying to create a function within a directive that can be called from the $rootScope.
The Problem:
It seems to only be working on the last item in the DOM which has this directive. I'm guessing that what's happening is rootScope.myFunction gets overwritten each time this directive runs.
The Question:
How can I create one function in the $rootScope which, when called, runs the internal function for each directive instead of just the last one?
The Relevant Code:
(function() {
angular.module('home')
.directive('closeBar', closeBar);
closeBar.$inject = ['$rootScope', '$window'];
function closeBar(rootScope, window) {
return {
scope: true,
restrict: 'A',
link: function(scope, element, attrs) {
var myFunction = function() {
// do stuff
};
myFunction();
rootScope.myFunction = function() {
myFunction();
};
}
};
}
})();
Then in a different script, I want to call:
rootScope.myFunction();
Note, I'm not allowed to share actual code from the project I'm working on so this is a more general question not tied to a specific use case.
A simple solution would be to create a special function in your rootScope that accepts functions as an argument, and pushes it into an array, and you will be able to invoke that function later which will call all the registered functions.
angular.module('myApp').run(['$rootScope', function($root) {
var functionsToCall = [];
$root.registerDirectiveFunction = function(fn, context) {
context = context || window;
functionsToCall.push({fn: fn, context: context});
}
$root.myFunction = function(args) {
functionsToCall.forEach(function(fnObj) {
fnObj.fn.apply(fnObj.context,args);
});
}
}
And in your directive:
link: function($scope, $el, $attr) {
function myFunct() {
}
$scope.$root.registerDirectiveFunction(myFunct, $scope);
//call it if you want
$scope.$root.myFunction();
}
This should cover your scenario.
JQuery to Directive
I want to call a method from the scope of this directive but can't seem to work it out (if possible).
$("my-directive").first().scope().loadData();
Directive Looks Something Like This
I would like to call the loadData function from the directive code below.
app.directive("myDirective", function () {
return {
restrict: "E",
templateUrl: "..."
scope: {},
controller: function ($scope, $element, $attrs) {
var self = this;
$scope.loadData = function () {
...
};
}
};
});
Scope is accessible inside the directive
You can get any child of the element of which directive is applied and get scope of it.
$('my-directive').first().children(":first").scope().loadData()
Strajk's answer is correct!
When Code is Added Dynamically setTimeout Needed
In the following example detail row has a nested directive (random-testees). I get the scope from that to dynamically compile the directive (dynamic and late-bound). The setTimeout is needed because it seems to take a bit before the
var detailRow = e.detailRow;
// Compile the directive for dyanmic content.
angular.element(document).injector().invoke(function ($compile) {
var scope = angular.element(detailRow).scope();
$compile(detailRow)(scope);
});
// Get some data from directive.
var testId = detailRow.find("random-testees").attr("testid");
// Wait, and call method on the directive.
setTimeout(function () {
var randomTesteesScope = $("random-testees").first().children(":first").scope();
randomTesteesScope.loadTestees(this);
}.bind(testId),200);
Not Very Confident About This
This seems a little brittle since I was getting mixed results with a 100 ms timeout sometimes and error when the randomTesteesScope returned undefined.
Problem
Dynamically add the ng-bind attribute through a custom directive to be able to use ng-bind, ng-bind-html or ng-bind-html-unsafe in a custom directive with out manually adding to the template definition everywhere.
Example
http://jsfiddle.net/nstuart/hUxp7/2/
Broken Directive
angular.module('app').directive('bindTest', [
'$compile',
function ($compile) {
return {
restrict: 'A',
scope: true,
compile: function (tElem, tAttrs) {
if (!tElem.attr('ng-bind')) {
tElem.attr('ng-bind', 'content');
$compile(tElem)
}
return function (scope, elem, attrs) {
console.log('Linking...');
scope.content = "Content!";
};
}
};
}]);
Solution
No idea. Really I can not figure out why something like the above fiddle doesn't work. Tried it with and with out the extra $compile in there.
Workaround
I can work around it might adding a template value in the directive, but that wraps the content in an extra div, and I would like to be able to that if possible. (See fiddle)
Second Workaround
See the fiddle here: http://jsfiddle.net/nstuart/hUxp7/4/ (as suggested by Dr. Ikarus below). I'm considering this a workaround for right now, because it still feels like you should be able to modify the template before you get to the linking function and the changes should be found/applied.
You could do the compiling part inside the linking function, like this:
angular.module('app').directive('bindTest', ['$compile', function ($compile) {
return {
restrict: 'A',
scope: true,
link: {
post: function(scope, element, attrs){
if (!element.attr('ng-bind')) {
element.attr('ng-bind', 'content');
var compiledElement = $compile(element)(scope);
}
console.log('Linking...');
scope.content = "Content!";
}
}
};
}]);
Let me know how well this worked for you http://jsfiddle.net/bPCFj/
This way seems more elegant (no dependency with $compile) and appropriate to your case :
angular.module('app').directive('myCustomDirective', function () {
return {
restrict: 'A',
scope: {},
template: function(tElem, tAttrs) {
return tAttrs['ng-bind'];
},
link: function (scope, elem) {
scope.content = "Happy!";
}
};
});
jsFiddle : http://jsfiddle.net/hUxp7/8/
From Angular directive documentation :
You can specify template as a string representing the template or as a function which takes two arguments tElement and tAttrs (described in the compile function api below) and returns a string value representing the template.
The source code tells all! Check out the compileNodes() function and its use of collectDirectives().
First, collectDirectives finds all the directives on a single node. After we've collected all the directives on that node, then the directives are applied to the node.
So when your compile function on the bindTest directive executes, the running $compile() is past the point of collecting the directives to compile.
The extra call to $compile in your bindTest directive won't work because you are not linking the directive to the $scope. You don't have access to the $scope in the compile function, but you can use the same strategy in a link function where you do have access to the $scope
You guys were so close.
function MyDirective($compile) {
function compileMyDirective(tElement) {
tElement.attr('ng-bind', 'someScopeProp');
return postLinkMyDirective;
}
function postLinkMyDirective(iScope, iElement, iAttrs) {
if (!('ngBind' in iAttrs)) {
// Before $compile is run below, `ng-bind` is just a DOM attribute
// and thus is not in iAttrs yet.
$compile(iElement)(iScope);
}
}
var defObj = {
compile: compileMyDirective,
scope: {
someScopeProp: '=myDirective'
}
};
return defObj;
}
The result will be:
<ANY my-directive="'hello'" ng-bind="someScopeProp">hello</ANY>