Using scope.$watch to observe when a function executes - angularjs

Suppose I have a custom directive, named my-directive, that has the default scope settings (scope: false, which means it shares the scope of the (parent) controller enclosing the directive), and I have a function my_function, attached to the controller's scope. I wonder if in the custom directive's link function if I can use scope.$watch to observe when my_function executes?
Here's a code for illustration:
<div ng-controller="MyCtrl">
<my-directive></my-directive>
</div>
the controller:
app.controller('MyCtrl', function($scope) {
$scope.my_function = function() {
...
}
});
the custom directive:
app.directive('myDirective', function() {
return {
restrict: 'E',
[scope: false,]
...
link: function(scope, element, attrs) {
// can I scope.$watch my_function here to check when it executes?
}
}
});
Update:
Here's my reason for asking this:
When my_function (which essentially is a retrieve function through $http.get) executes, it should update an object, say my_data. If I try to scope.$watch my_data, there may be a situation where my_function executes and there is no change in my_data (such as when I edit an entry in my_data and decides to cancel the update anyway), thus a portion of the code that I want to execute in link function is not executed. So I want to make sure my code in link function executes whenever my_function executes, whether or not there is a change my_data.

Here are two ways to solve your problem that I could think of.
1. Use a counter
You can always increment a counter when invoking my_function:
// This will reside in your controller
$scope.counter = 0;
$scope.my_function = function () {
// do your logic
$scope.counter++;
};
// This will reside in your directive
$scope.$watch( "counter", function () {
// do something
});
2. Simply watch my_data
Assuming my_function will always override the my_data variable, you can use the default Angular behavior for $watch. You don't have to care if your backend has news for you or not, the reference will never be the same.
// in your controller
$scope.my_function = function () {
$http.get(...).then(function ( response ) {
$scope.my_data = response.data;
});
};
// in your directive
$scope.$watch( "my_data", function () {
// do something
});
Obviously, this is the cleaner way to do this, but you may get in trouble if you don't unit test because of one of the following:
You add any sort of logic around my_data that may conflict with your watcher;
You want to cache your HTTP calls.
I hope I was able to help you! Good luck.

Depending on what you're trying to do, you may not need $watch at all.
If the function isn't ever going to change, you can just make a dummy wrapper.
link: function(scope, element, attrs) {
var oldFunc = scope.my_function;
scope.my_function = function(sameArgs){
//method_was_called();
if(oldFunc){oldFunc(sameArgs);}
}
}
Then you can do whatever logic you want inside your wrapper function, either before or after you run the 'super' method.
Check also
Javascript: Extend a Function

Related

Variable is undefined in the scope, but it's actually defined

I'm really confused, I don't know what heck is going on!
.directive("blGraph", ['$http', function($http) {
return {
restrict: 'EA',
scope: {
botid: "=blGraph"
},
link: function(scope, element, attr) {
console.log(scope, scope.botid);
}
};
}]);
Result,
as you see in the picture, the botid exist in the scope, but when i try to get it by scope.botid it tell that it is not defined!
put a $watch on the scope variable. Control will go twice in the watch handler function. You will see what is happening.
Also what is your HTML markup, may be you are binding it to undefined.
Due to the sequence of running the controller and link functions, you need to use a $timeout in order to allow the variables to load onto the scope. Here's how I solved it:
controller: function($scope, $timeout) {
$timeout(function(){
console.log($scope.myVariable)
},0);
}
Most likely, scope.botid is not defined when you are printing it, and is defined after the console.log statement.
When you use console.log on an object, it shows the state of the object at the end of the JS execution.
Instead of just using scope in the console.log statement, try the following:
console.log(JSON.stringify(scope), scope.botid);

Use scope in filter angularjs

I'm trying to create filter that will watch safe and try to use $compile on variable
Right now I can use this with no problem
.directive('safe', ['$compile',
function ($compile) {
return function (scope, element, attrs) {console.log(scope, element, attrs)
scope.$watch(
function (scope) {
// watch the 'safe' expression for changes
return scope.$eval(attrs.bindUnsafeHtml);
},
function (value) {
// when the 'safe' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
}
])
The usage for above code in the html file is
<span safe="'_agree_to_terms_and_conditions_'">
Now I want to be able to do something like this instead
{{ gettext("_agree_to_terms_and_conditions_" | safe }}
Now when I try to create filter I don't have scope available in there...
Is it even possible to acheive something like above?
PS. The name of the filter needs to match exactly like the given example since I'm trying to create a common usage of the function with different app for i18n.
This seems like a bad idea. From a filter, you wouldn't want to try and modify the $scope in any ways. You'll probably get errors about being in a $digest cycle already. But it is possible to pass things into the filter.
{{ gettext("_agree_to_terms_and_conditions_" | safe:this }}
app.filter('safe', function () {
return function (input, scope) {
//scope will be *this* that you passed in from the HTML
};
});
Just found an article where this has been asked before: Access scope variables from a filter in AngularJS. They also give similar warnings about not trying to modify the $scope.

Function with args in parent scope is being binded but not being called in angular directive

I'm trying to call a function that is in the parent scope of a directive but it is not being called.
We want to pass the parameter 'arg' that is being modified to the parent scope and do some logic to it like 'arg = arg + 1', but this function is not being called.
function MyCtrl($scope) {
$scope.arg = 0
$scope.myFunction = function(arg) {
//do some logic like '+ 1'
arg++;
$scope.arg = arg;
}
}
myApp.directive('directive', function () {
return {
restrict: 'E',
scope: {
arg: '=arg',
fun: '&'
},
template: '<input ng-model="arg"/>',
link: function (scope) {
scope.$watch('arg', function (arg) {
scope.fun(arg);
});
}
};
});
Here is a little fiddle with a basic use case. http://jsfiddle.net/HB7LU/2677/
The $scope function isn't being called because you are parsing it wrong.
To parse a function you need to assign it like so:
<directive arg="arg" fun="myFunction()"></directive>
If you want to pass arguments to it, you need to be just a little more careful. First you need to describe the arguments in advance like so:
<directive arg="arg" fun="myFunction(arg)"></directive>
and when you're calling the function inside the directive you apply it like so:
scope.fun({arg: arg});
I created a jsFiddle that solves your issue: jsFiddle
Please note that your own fiddle had a recursive call in it: everytime you increase arg you also trigger the $watch and so it went into a loop. I changed that a bit (since I know you made it just to show your example.
If you want to learn a bit more about the & parsing in directives, check out this video which I highly recommend: egghead.io isolate scope &

create "nested" ng-click inside of Angular Directive

I have a directive which open up a bootstrap-tours on call of t.start():
app.directive('tourGuide', function ($parse, $state) {
var directiveDefinitionObject = {
restrict: 'E',
replace: false,
link: function (scope, element, attrs) {
var t = new Tour({container: $("#main"),
backdrop: false,
debug:true
});
t.addStep({
element: "#main",
title: "Title123",
content: "Content123"
});
t.init();
t.start();
}};
return directiveDefinitionObject;
});
I want to create a button which on click could call variable t.start(). Is it even possible? I want to achieve this so could be independent of functions inside controllers, because this directive will be on every single view of the application, so it would be nice if it could call a parameter inside itself. Ive tryed to create a template in directive with a button, and add a ng-clikc action with t.start() and ofcourse it failed because variable t is not known to controller where ever my directive is.
EXAMPLE:
Lets say i have 2 views ShowItems and CreateItem they have 2 dirfferent controllers. in those views i have 1 button/link, on click of it i want to show my TourGuide. Thats simple.
Now in my TourGuide i have 2 different Steps, and when i press on a button in CreateItem view i want to see the step in Tour Guide for CreateItem view, and vise versa.
Thats simple if i use functions inside my controller. But is it possible to use directive ONLY, because i could have 20 different controllers?
Based on a few assumptions - I assume what you want here is to dynamically call a routine in scope from a directive. Take the following code as an example
HTML/View Code
<div my-directive="callbackRoutine">Click Here</div>
Controller
function MyController($scope) {
$scope.callbackRoutine = function () {
alert("callback");
};
}
Directive
app.directive("myDirective", function () {
return {
restrict: 'A',
link: function (scope, element, attr){
element.bind('click', function (){
if (typeof scope[attr.myDirective] == "function"){
scope[attr.myDirective]();
}
});
}
};
});
In this, you specify the callback routine as part of the directive. The key to the equation is that the scope for the directive inherits from any parent scope(s) which means you can call the routine even from the scope passed to the directive. To see a working example of this, see the following plunkr: http://plnkr.co/edit/lQ1QlwwWdpNvoYHlWwK8?p=preview. Hope that helps some!

Access controller scope from directive

I've created a simple directive that displays sort column headers for a <table> I'm creating.
ngGrid.directive("sortColumn", function() {
return {
restrict: "E",
replace: true,
transclude: true,
scope: {
sortby: "#",
onsort: "="
},
template: "<span><a href='#' ng-click='sort()' ng-transclude></a></span>",
link: function(scope, element, attrs) {
scope.sort = function () {
// I want to call CONTROLLER.onSort here, but how do I access the controller scope?...
scope.controllerOnSort(scope.sortby);
};
}
};
});
Here's an example of some table headers being created:
<table id="mainGrid" ng-controller="GridCtrl>
<thead>
<tr>
<th><sort-column sortby="Name">Name</sort-column></th>
<th><sort-column sortby="DateCreated">Date Created</sort-column></th>
<th>Hi</th>
</tr>
</thead>
So when the sort column is clicked I want to fire the onControllerSort function on my grid controller.. but I'm stuck! So far the only way I've been able to do this is for each <sort-column>, add attributes for the "onSort" and reference those in the directive:
<sort-column onSort="controllerOnSort" sortby="Name">Name</sort-column>
But that's not very nice since I ALWAYS want to call controllerOnSort, so plumbing it in for every directive is a bit ugly. How can I do this within the directive without requiring unnecesary markup in my HTML? Both the directive and controller are defined within the same module if that helps.
Create a second directive as a wrapper:
ngGrid.directive("columnwrapper", function() {
return {
restrict: "E",
scope: {
onsort: '='
}
};
});
Then you can just reference the function to call once in the outer directive:
<columnwrapper onsort="controllerOnSort">
<sort-column sortby="Name">Name</sort-column>
<sort-column sortby="DateCreated">Date Created</sort-column>
</columnwrapper>
In the "sortColumn" directive you can then call that referenced function by calling
scope.$parent.onsort();
See this fiddle for a working example: http://jsfiddle.net/wZrjQ/1/
Of course if you don't care about having hardcoded dependencies, you could also stay with one directive and just call the function on the parent scope (that would then be the controller in question) through
scope.$parent.controllerOnSort():
I have another fiddle showing this: http://jsfiddle.net/wZrjQ/2
This solution would have the same effect (with the same criticism in regard to hard-coupling) as the solution in the other answer (https://stackoverflow.com/a/19385937/2572897) but is at least somewhat easier than that solution. If you couple hard anyway, i don't think there is a point in referencing the controller as it would most likely be available at $scope.$parent all the time (but beware of other elements setting up a scope).
I would go for the first solution, though. It adds some little markup but solves the problem and maintains a clean separation. Also you could be sure that $scope.$parent matches the outer directive if you use the second directive as a direct wrapper.
The & local scope property allows the consumer of a directive to pass in a function that the directive can invoke.
See details here.
Here is a answer to a similar question, which shows how to pass argument in the callback function from the directive code.
In your directive require the ngController and modify the link function as:
ngGrid.directive("sortColumn", function() {
return {
...
require: "ngController",
...
link: function(scope, element, attrs, ngCtrl) {
...
}
};
});
What you get as ngCtrl is your controller, GridCtrl. You dont get its scope though; you would have to do something in the lines of:
xxxx.controller("GridCtrl", function($scope, ...) {
// add stuff to scope as usual
$scope.xxxx = yyyy;
// Define controller public API
// NOTE: USING this NOT $scope
this.controllerOnSort = function(...) { ... };
});
Call it from the link function simply as:
ngCtrl.controllerOnSort(...);
Do note that this require will get the first parent ngController. If there is another controller specified between GridCtrl and the directive, you will get that one.
A fiddle that demonstrates the principle (a directive accessing a parent ng-controller with methods): http://jsfiddle.net/NAfm5/1/
People fear that this solution may introduce unwanted tight coupling. If this is indeed a concern, it can be addressed as:
Create a directive that will be side-by-side with the controller, lets call it master:
<table id="mainGrid" ng-controller="GridCtrl" master="controllerOnSort()">
This directive references the desired method of the controller (thus: decoupling).
The child directive (sort-column in your case) requires the master directive:
require: "^master"
Using the $parse service the specified method can be called from a member method of the master controller. See updated fiddle implementing this principle: http://jsfiddle.net/NAfm5/3/
There is another way to do this, although given my relative lack of experience I can't speak for the fitness of such a solution. I will pass it along anyhow just for informational purposes.
In your column, you create a scope variable attribute:
<sort-column data-sortby="sortby">Date Created</sort-column>
Then in your controller you define the scope variable:
$scope.sortby = 'DateCreated' // just a default sort here
Then add your sort function in controller:
$scope.onSort = function(val) {
$scope.sortby = val;
}
Then in your markup wire up ng-click:
<sort-column data-sortby="sortby" ng-click="onSort('DateCreated')">Date Created</sort-column>
Then in your directive you add the sortby attribute to directive scope:
scope: {
sortby: '=' // not sure if you need
}
And in your "link:" function add a $watch:
scope.$watch('sortby', function () {
... your sort logic here ...
}
The beauty of this approach IMO is that your directive is decoupled completely, you don't need to call back to onSort from the directive because you don't really leave onSort in the controller during that part of the execution path.
If you needed to tell your controller to wait for the sort to finish you could define an event in the controller:
$scope.$on("_sortFinished", function(event, message){
..do something...
});
Then in your directive simply emit the event then the process is done:
$scope.$emit('_sortFinished');
There's other ways to do that, and this kind of adds some tight-ish coupling because your controller has to listen for. and your directive has to emit a specific even... but that may not be an issue for you since they are closely related anyhow.
Call me crazy, but it seems easier to just get the controller from the element via the inbuilt method for that, rather than fiddling with require:
var mod = angular.module('something', []).directive('myDir',
function () {
return {
link: function (scope, element) {
console.log(element.controller('myDir'));
},
controller: function () {
this.works = function () {};
},
scope: {}
}
}
);
http://plnkr.co/edit/gY4rP0?p=preview

Resources