Sharing scope between controller & directive in AngularJS - angularjs

I've created a directive to wrap a jQuery plugin, and I pass a config object for the plugin from the controller to the directive. (works)
In the config object is a callback that I want to call on an event. (works)
In the callback, I want to modify a property on the controller's $scope, which does not work. Angular does not recognize that the property has changed for some reason, which leads me to believe that the $scope in the callback is different than the controller's $scope. My problem is I just don't why.
Can anybody point me in the right direction?
Click here for Fiddle
app.js
var app = angular.module('app', [])
.directive('datepicker', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
// Uncommenting the line below causes
// the "date changed!" text to appear,
// as I expect it would.
// scope.dateChanged = true;
var dateInput = angular.element('.datepicker')
dateInput.datepicker(scope.datepickerOpts);
// The datepicker fires a changeDate event
// when a date is chosen. I want to execute the
// callback defined in a controller.
// ---
// PROBLEM:
// Angular does not recognize that $scope.dateChanged
// is changed in the callback. The view does not update.
dateInput.bind('changeDate', scope.onDateChange);
}
};
});
var myModule = angular.module('myModule', ['app'])
.controller('MyCtrl', ['$scope', function ($scope) {
$scope.dateChanged = false;
$scope.datepickerOpts = {
autoclose: true,
format: 'mm-dd-yyyy'
};
$scope.onDateChange = function () {
alert('onDateChange called!');
// ------------------
// PROBLEM AREA:
// This doesnt cause the "date changed!" text to show.
// ------------------
$scope.dateChanged = true;
setTimeout(function () {
$scope.dateChanged = false;
}, 5000);
};
}]);
html
<div ng-controller="MyCtrl">
<p ng-show="dateChanged">date changed!</p>
<input type="text" value="02-16-2012" class="datepicker" datepicker="">
</div>

There are a number of scope issues at work in your demo. First , within the dateChange callback, even though the function itself is declared inside the controller, the context of this within the callback is the bootstrap element since it is within a bootstrap handler.
Whenever you change angular scope values from within third party code , angular needs to know about it by using $apply. Generally best to keep all third party scopes inside the directive.
A more angular apprroach is to use ng-model on the input. Then use $.watch for changes to the model. This helps keep all the code inside the controller within angular context. Is rare in any angular application not to use ng-model on any form controls
<input type="text" class="datepicker" datepicker="" ng-model="myDate">
Within directive:
dateInput.bind('changeDate',function(){
scope.$apply(function(){
scope[attrs.ngModel] = element.val()
});
});
Then in Controller:
$scope.$watch('myDate',function(oldVal,newVal){
if(oldVal !=newVal){
/* since this code is in angular context will work for the hide/show now*/
$scope.dateChanged=true;
$timeout(function(){
$scope.dateChanged=false;
},5000);
}
});
Demo: http://jsfiddle.net/qxjck/10/
EDIT One more item that should change is remove var dateInput = angular.element('.datepicker') if you want to use this directive on more than one element in page. It is redundant being used in directive where element is one of the arguments in the link callback already, and is instance specific. Replace dateInput with element

The changeDate event bound to the input seems to be set up to fire outside of the Angular framework. To show the paragraph, call $scope.$apply() after setting dateChanged to true. To hide the paragraph after the delay, you can use $apply() again inside the function passed to setTimeout, but you're likely to keep out of further trouble using Angular's $timeout() instead.
Fiddle

Related

Html element with AngularJS attribute generate after init AngularJS

I've AngularJS issue when there are some elements(button) generate dynamically or generate after init AngularJS. Source code below shows the button not able to trigger the ng-click attribute. Any ideas that can trigger the AngularJS attribute after init the AngularJS. Thanks.
OperationFormatter: function (value, row) {
var operations = [];
operations.push('<button class="btn-mini glyphicon glyphicon-edit" ng-
click="myFunc()" title="Shared_Edit"></button>');
return operations.join(' ');
// This function only can execute after init the AngularJS due to certain
conditions.
}
$(function () {
MyCtrl();
})
function MyCtrl($scope) {
var app = angular.module("appAffiliateInfo", []);
app.controller("ctrlBankInfo", function ($scope) {
$scope.myFunc = function () {
alert('ok');
};
});
};
I assume you're trying to dynamically insert the generated HTML code in your template.
It cannot be done this way, as AngularJS already parsed your template at "runtime" and created all its watchers.
You have to use the $compile service, like so:
// scope: the scope containing the function called by ng-click
// htmlCode: your html code (as a string)
var newElem = $compile(htmlCode)(scope);
// use the proper code here, depending on what you want to achieve with the HTML element
document.body.appendChild(newElem[0]);
When "compiled" by $compile, the HTML code is parsed in order to create the required watchers. You must make sure that you provide the correct scope to $compile, or the function intended to ng-click will never be called.

AngularJS Directive doesn't update scope for callback

I am using directives to create a component library in AngularJS 1.5. Hence, my directives need to have isolate scopes.
Some of my directives have callbacks so you can pass in a function to get invoked by the directive. However, when that callback is invoked by the directive, it doesn't seem like the changes to $scope attributes are fully updated like I would expect them to be.
Here is a Plunker that shows this behavior:
http://embed.plnkr.co/Rg15FHtHgCDExxOYNwNa/
Here is what the code looks like:
<script>
var app = angular.module('myApp', []);
app.controller('Controller', ['$scope',function($scope) {
// initialize the value to something obvious
$scope.clickersValue = "BEFORE";
// when this call back is called we would expect the value to be updated by updated by the directive
$scope.clickersCallback = function() {
//$scope.$apply(); // $apply is not allowed here
$scope.clickersValueRightAfterCall = $scope.clickersValue;
console.log("clickersCallback: scope.clickersValue", $scope.clickersValue);
};
}
]);
app.directive('clicker', [function() {
return {
restrict: 'EA',
template: '<div ng-click="clicked()">click me!</div>',
controller: ['$scope', function($scope) {
$scope.clicked = function() {
console.log("you clicked me.");
$scope.newValue = 'VALID';
$scope.myUpdate();
}
}],
scope: {
"newValue": "=",
"myUpdate": "&"
}
};
}]);
</script>
So when clickersCallback gets invoked the clickersValue attribute still has the old value. I have tried using $scope.$apply but of course it isn't allowed when another update is happening. I also tried using controller_bind but got the same effect.
Wrap the code inside clickersCallback function in a $timeout function.
$timeout(function() {
$scope.clickersValueRightAfterCall = $scope.clickersValue;
console.log("clickersCallback: scope.clickersValue", $scope.clickersValue);
});
Updated plunker
The $timeout does not generate error like „$digest already in progress“ because $timeout tells Angular that after the current cycle, there is a timeout waiting and this way it ensures that there will not any collisions between digest cycles and thus output of $timeout will execute on a new $digest cycle.
source
Edit 1: As the OP said below, the user of the directive should not have to write any "special" code in his callback function.
To achieve this behavior I changed the $timeout from de controller to the directive.
Controller callback function (without changes):
$scope.clickersCallback = function() {
$scope.clickersValueRightAfterCall = $scope.clickersValue;
console.log("clickersCallback: scope.clickersValue", $scope.clickersValue);
};
Directive code (inject $timeout in the directive):
$scope.clicked = function() {
console.log("you clicked me.");
$scope.newValue = 'VALID';
$timeout(function() {
$scope.myUpdate();
});
}
Updated plunker

Event onclick in my controller

I wrote thi code in my controllers:
$('#categoryId').on('click', function(event){
$scope.items = $scope.getUpdatedItems ();
console.log('Items ',$scope.items );
});
In my directives I have this:
....
scope.$watch('items', function(items){
console.log('Directives - Items ',items );
....
The problem is that I never enter in the $watch declared in my directives.
Somebody can help me..
Thanks
You shouldn't mixed up jQuery with AngularJS that will mess up with the digest cycle on angular. Angular did provided its own directive to handle such activity like in your case you could us ng-click directive, this will call the mentioned function inside ng-click attribute value on click on that element.
Markup
<button type="categoryId" ng-click="updateAllItems()"></button>
Code
$scope.updateAllItems = function(){
$scope.items = $scope.getUpdatedItems ();
console.log('Items ',$scope.items );
});
Above code will call updateAllItems function which resides inside controller & then $scope.items will get updated. And then you watch inside directive will get call.

AngularJS scroll directive - How to prevent re-rendering whole scope

I have an AngularJS Application with a scroll directive implemented as the following:
http://jsfiddle.net/un6r4wts/
app = angular.module('myApp', []);
app.run(function ($rootScope) {
$rootScope.var1 = 'Var1';
$rootScope.var2 = function () { return Math.random(); };
});
app.directive("scroll", function ($window) {
return function(scope, element, attrs) {
angular.element($window).bind("scroll", function() {
if (this.pageYOffset >= 100) {
scope.scrolled = true;
} else {
scope.scrolled = false;
}
scope.$apply();
});
};
});
The HTML looks the following:
<div ng-app="myApp" scroll ng-class="{scrolled:scrolled}">
<header></header>
<section>
<div class="vars">
{{var1}}<br/><br/>
{{var2()}}
</div>
</section>
</div>
I only want the class scrolled to be added to the div once the page is scrolled more than 100px. Which is working just fine, but I only want that to happen! I don't want the whole scope to be re-rendered. So the function var2() should not be executed while scrolling. Unfortunately it is though.
Is there any way to have angular only execute the function which is bound to the window element without re-rendering the whole scope, or am I misunderstanding here something fundamentally to AngularJS?
See this fiddle:
http://jsfiddle.net/un6r4wts/
Edit:
This seems to be a topic about a similar problem:
Angularjs scope.$apply in directive's on scroll listener
If you want to calculate an expression only once, you can prefix it with '::', which does exactly that. See it in docs under One-time binding:
https://docs.angularjs.org/guide/expression
Note, this requires angular 1.3+.
The reason that the expressions are calculated is because when you change a property value on your scope, then dirty check starts and evaluates all the watches for dirty check. When the view uses {{ }} on some scope variable, it creates a binding (which comes along with a watch).

Detecting window focus the angular way?

Is there a good angular way to detect window focus? I am using html5 notifications and I would like to only fire if the window is out of focus.
Thanks!
There's a built-in angular directive ngFocus here maybe it helps if you attach it to the body
<window, input, select, textarea, a
ng-focus="">
...
</window, input, select, textarea, a>
Edit: For window focus, there's the $window wrapper and you can do something like:
$window.onfocus = function(){
console.log("focused");
}
Edit #CristiBerceanu is right - you should use the built-in ng-focus directive. However, take this answer as a guideline for any missing event you want to bind.
You must create a directive:
angular
.module('MyModule', [])
.directive('onFocus', function(){
return {
restrict: 'A',
scope: {
'focus': '&onFocus'
},
link: function($scope, $element, $attributes) {
var focus = function(event) {
$scope.focus({'$event': event});
};
$element.on("focus", focus);
$scope.$on('$destroy', function(){
$element.off('focus', onClick);
});
}
}
});
Notice how the event is bound in the directive by jquery and NOT directly in the controller. Additionally, notice that a bound expression is tied using the & prefix (evaluable expression binding) instead of regular prefixes like # (text-binding) or = (scope property reference, bi-directional, binding).
In Cristi Berceanu's answer, he suggests assigning a function to $window.onfocus, which does work. However, there is a problem with that... only one function can be assigned to $window.focus at a time. Thus, by assigning a function to $window.onfocus, you could accidentally overwrite a previous function, and your function will be vulnerable to being overwritten later, too.
Here's a different solution that allows multiple functions to run with the window's focus or blur events:
var onFocus = function () {
// do something
};
var onBlur = function () {
// do something else
};
var win = angular.element($window);
win.on("focus", onFocus);
win.on("blur", onBlur);
This will allow you to assign multiple functions to the focus and blur events for the $window object.
If you added the functions inside a controller and want to remove those functions when the controller is destroyed, you can do something like this:
$scope.$on("$destroy", function handler() {
win.off("focus", onFocus);
win.off("blur", onBlur);
$interval.cancel(interval);
});
Solution inspired by this post: https://www.bennadel.com/blog/2934-handling-window-blur-and-focus-events-in-angularjs.htm
you can write a directive to attach to the body element and inside it you can use $window.onfocus event to notify your angular app using events or a service, the same thing you can do from inside a service, it all depends on your architecture

Resources