Angularjs - $scope.$destroy doesn't remove watchers - javascript

I am trying to figure out how to create my own "one time binding", for Angularjs <= 1.2.
I found this answer, describing how to create your own bindOnce directive. I do see that when using the following directive:
app.directive('bindOnce', function() {
return {
scope: true,
link: function( $scope ) {
setTimeout(function() {
$scope.$destroy();
}, 0);
}
}
});
Data is binded once. But, I can see that the $$watchers is still on. Take a look at the following JSBin - running the commented watcher count code on the console will reveal that the watchers are still alive.
EDIT: for some reason, when using the same directive with angular 1.3, the watcher count dod changed to be 0!!!

Use the cleanup function to remove watchers:
function cleanup(element)
{
element.off().removeData();
var injector = currentSpec.$injector;
element.$injector = null;
// clean up jquery's fragment cache
angular.forEach(angular.element.fragments, function(val, key) {
delete angular.element.fragments[key];
});
angular.forEach(angular.callbacks, function(val, key)
{
delete angular.callbacks[key];
});
angular.callbacks.counter = 0;
}
Use a self-destructing service as a simple bind once:
function onetime()
{
/*...*/
onetime = Function;
}
angular.module('myApp').constant('bindonce', onetime);
angular.module('myApp').controller('foo', function($bindonce){
this.$inject = '$bindonce';
$scope.mybind = $bindonce();
}
Use the migration guide for reference to find breaking changes:
References
testabilityPatch.js
angular-mocks.js
AngularJS Developer Guide: Migration from Previous Versions

Related

How to avoid using 'scope.$parent...' in angular 1.2

We're developing a set of (ideally) flexible, component-based re-usable templates in angularjs 1.2 to develop a series of e-learning modules.
Part of the spec requires the tracking of 'completable' components. At the moment the main controller looks like this:
app.controller('mainCtrl', ['$scope', function($scope) {
$scope.completables = [];
$scope.completed = [];
$scope.addCompletable = function (object) {
$scope.completables.push(object);
// also set correlating completed property to 'false' for each completable added
$scope.completed.push(false);
}
$scope.componentCompleted = function(id) {
// Set complete to 'true' for matching Sscope.completed array index
// We COULD use .indexOf on the completables array, but that doesn't work with IE8
var tempArray = $scope.completables;
var matchingIndex = -1;
for (var i=0; i<tempArray.length; i++) {
if (tempArray[i]==id) {
matchingIndex = i;
}
}
if (i>-1) {
$scope.completed[matchingIndex] = true;
}
}
}]);
We have a eng-completable attribute that triggers the following directive:
app.directive('engCompletable', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
// add the id of this element to the completables array in the main controller
scope.$parent.addCompletable(attrs.id);
}
}
});
So every time angular encounters an 'eng-completable' attribute on an element, it calls addCompletable on the parent scope which adds the element id to the 'completables' array and 'false' to the corresponding index of the 'completed' array.
In the eng-popup attribute directive, we have a function to check if it has been made visible:
app.directive('engPopup', function() {
return {
restrict: 'A',
replace: true,
templateUrl: 'components/popup.html',
link: function(scope, element, attrs) {
scope.$watch(function() { return element.is(':visible') }, function() {
scope.$parent.componentCompleted(attrs.id);
});
}
};
});
Which also uses the parent scope to trigger the 'componentCompleted' function. I've been told that referring to the parent scope is bad practise, and it is also messing up our unit tests, apparently.
I'd like to know what is the alternative. How can I let my app know that a specific component has been completed? And where should this state be tracked?
I'd really like to know HOW to do this - not just be told that I'm doing it the wrong way. Please let me know what the alternative is.
But, as always, any help will be much appreciated.
One alternative would be to create a Service to be responsible to track all the components and keep their states (complete/not completed).
It will remove the need for $scope.parent and the service can be injected into any controller or directive you need.
:)
If that completables list is application-wide yo could consider adding it to your $rootScope along with the addCompletable method —and any other relate methods— instead of adding it to your mainController's $scope.
This way you could substitude your scope.$parent.componentCompleted(attrs.id); with $rootScope.componentCompleted(attrs.id); and avoid to make calls to scope.$parent.

javascript - how to watch an object

I have a object and want to run a function if that object changes. I tried using angular $watch, but this only works without setters.
Searching for another solution i found obj.observe but mdn says it's obsolete and not widely supported. so is there any other way to run a function if my object changes?
Edit:
// watch using this doen't work, when update through ng-model
Object.defineProperties(self, {
'myprop': {
get: function () {return _myprop; },
set: function (value) {
_myprop = value;
}
},
I'm not sure what do you mean by $watch "working without setters" .
scope.$watch('name', function(newValue, oldValue) {
console.log('name has been changed: ' + $scope.name);
scope.counter = scope.counter + 1;
});
Just start simple, put the above code into a controller and have a simple field modifying the $scope.name variable. You'll see in the console that the watch is triggered.

How to watch component binding change using Angular component

How can I listen to angular component binding change and perform actions?
angular.module('myapp')
.component('myComponent', {
templateUrl: 'some.html',
controller: MyController,
controllerAs: 'myCtrl',
bindings: {
items: '<'
}
});
now when items changes I want to perform another action using this value,
How can I do it?
You can add the $onChanges method to the controller
$onChanges(changesObj) is called whenever one-way bindings are updated. The changesObj is a hash whose keys are the names of the bound properties that have changed, and the values are an object of the form.
Following example handles canChange change event.
angular.module('app.components', [])
.component('changeHandler', {
controller: function ChangeHandlerController() {
this.$onChanges = function (changes) {
if (changes.canChange)
this.performActionWithValueOf(changes.canChange);
};
},
bindings: {
canChange: '<'
},
templateUrl: 'change-handler.html'
});
Requires AngularJS >= 1.5.3 and works only with one-way data-binding (as in the example above).
Docs: https://docs.angularjs.org/guide/component
Reference: http://blog.thoughtram.io/angularjs/2016/03/29/exploring-angular-1.5-lifecycle-hooks.html
now when items changes I want to perform another action using this value,
How can I do it?
But I want to avoid using the dying $scope
If you don't want to use $scope you can use a property setter to detect any changes e.g. :
class MyController {
private _items: string[] = []
set items(value:string[]){
this._items = value;
console.log('Items changed:',value);
}
get items():string[]{
return this._items;
}
}
const ctrl = new MyController();
ctrl.items = ['hello','world']; // will also log to the console
Please note that you shouldn't use it for complex logic (reasons : https://basarat.gitbooks.io/typescript/content/docs/tips/propertySetters.html) 🌹
Here's an ES5.1 version of basarat's answer:
function MyController() {
var items = [];
Object.defineProperty(this, 'items', {
get: function() {
return items;
},
set: function(newVal) {
items = newVal;
console.log('Items changed:', newVal);
}
});
}
Using Object.defineProperty(). Supported by all major browsers and IE9+.
I've discovered a way but not sure it's the most efficient. First bring in $scope as a dependency and set it to this._scope or the like in your constructor. I have the following then in my $onInit function:
this._scope.$watch(() => {
return this.items;
},
(newVal, oldVal) => {
// Do what you have to here
});
It's highly inspired by the answer here: Angularjs: 'controller as syntax' and $watch
Hope it helps, it's what I'm going to use until I'm told otherwise.
Currently you can't use angular watchers without $scope as change detection is based around $scope. Even if you use expressions in HTML it would delegate watch functionality to $scope.
Even if you create some other mechanism to watch you will need to remember to unwatch manually - and with $scope it's done automatically.
This approach might help:
import { Input } from '#angular/core';
class MyComponent {
#Input set items(value) {
if (this._items !== value) {
console.log(`The value has been changed from "${this._items}" to "${value}"`);
this._items = value;
}
}
private _items;
get items() {
return this._items;
}
}

global communication in angular module: event bus or mediator pattern/service

So far I have seen many solutions of the problem. The simplest one is, of course, to $emit an event in $rootScope as an event bus e.g. ( https://github.com/btilford/anti-patterns/blob/master/angular/Angular.md )
angular.module('myModule').directive('directiveA', function($rootScope) {
return {
link : function($scope, $element) {
$element.on('click', function(event) {
$rootScope.$emit('directiveA:clicked', event);
});
}
}
});
angular.module('myModule').directive('directiveB', function() {
return {
link : function($scope, $element) {
$rootScope.on('directiveA:clicked', function(event) {
console.log('received click event from directiveA');
});
}
}
});
and another one is to declare a service with a mediator or pubsub functionality / an enclosed scope e.g. ( Communicating between a Multiple Controllers and a directive. )
module.factory('MessageService',
function() {
var MessageService = {};
var listeners = {};
var count = 0;
MessageService.registerListener = function(listener) {
listeners[count] = listener;
count++;
return (function(currentCount) {
return function() {
delete listeners[currentCount];
}
})(count);
}
MessageService.broadcastMessage = function(message) {
var keys = Object.keys(listeners);
for (var i = 0; i < keys.length; i++) {
listeners[keys[i]](message);
}
}
return MessageService;
}
);
The question are:
is there point to use the second one in an angular application?
and what are pros and cons of each of those in comparison to each other?
Creating your own implementation of event emitter is counter-productive when writing an AngularJS application. Angular already provides all tools needed for event-based communication.
Using $emit on $rootScope works nicely for global inter-service communication and doesn't really have any drawbacks.
Using $broadcast on a natural scope (one that is bound to a part of your DOM) provides scoped communication between view components (directives, controllers).
Using $broadcast on $rootScope brings the two previous points together (it provides a completely global communication platform). This is the solution used basically by any AngularJS-based library out there.
and
If you're worried about performance in the previous option and you really want your separate event emitter, you can easily create one by creating an isolated scope ($rootScope.$new(true)) and using $broadcast on it. (You can then wrap it into a service and inject it anywhere you want.)
The last option creates a full-fledged event emitter integrated into Angular (the implementation provided in your question would at least need to wrap all listener calls in $apply() to integrate properly) that can be additionally used for data change observation, if that fits a particular use-case.
However, unless your application is really humongous, or you're really paranoid about event name collisions, the first three options should suffice just fine.
I won't go into detail about other means of communication between your components. Generally speaking, when the situation calls for data sharing using scope, direct interaction of controllers, or communication through DOM Node attributes, you should know it.
I would say that broadcasting is an Angular way how to achieve this.
However your mediator can work, if you pass internal funcion of directive, in example I have used method on scope, but it can be done also with controller method.
I have used exact same factory as you post.
angular.module("sharedService", [])
.factory('MessageService',
function() {
var MessageService = {};
var listeners = {};
var count = 0;
MessageService.registerListener = function(listener) {
listeners[count] = listener;
count++;
return (function(currentCount) {
return function() {
delete listeners[currentCount];
};
})(count);
};
MessageService.broadcastMessage = function(message) {
var keys = Object.keys(listeners);
for (var i = 0; i < keys.length; i++) {
listeners[keys[i]](message);
}
};
return MessageService;
}
)
.directive("directiveA", function(MessageService) {
return {
link:function(scope) {
scope.click = function() {
MessageService.broadcastMessage("broadcasted message");
};
},
template: '<button ng-click="click()">Click</button>'
};
})
.directive("directiveB", function(MessageService) {
return {
link:function(scope) {
scope.callback = function(message) {
console.log(message);
};
MessageService.registerListener(scope.callback);
}
};
});
Full example: http://jsbin.com/mobifuketi/1/edit?html,js,console,output
Just to be complete, I would like to add, that angular also provides more posibilities how can directives communicate.
Require atribute
If your directives are connected in hierarchy, then you can use require attribute which let you to access other directives controller. This is ussually best solution for many cases.
.directive("directiveA", function() {
return {
require: "^directiveB",
link: function(scope, element, attrs, directiveCtrl) {
scope.click = function() {
directiveCtrl.call();
};
},
template: '<button ng-click="click()">Click</button>'
};
})
.directive("directiveB", function() {
return {
controller :function() {
this.call = function() {
console.log("method has been called");
};
}
};
});
Full example: http://jsbin.com/turoxikute/1/edit?html,js,console,output
Using $watch
If the functionality deppends on data and not on action, you cen use $watch and react on the changes of given model or model stored in shared service , its not like listener, its basicly checking of change. I have named method changeState() and log "state changed" for everybody see it clear.
angular.module("sharedService", [])
.service("MediatorService", function() {
this.state = true;
this.changeState = function() {
this.state = !this.state;
};
})
.directive("directiveA", function(MediatorService) {
return {
link:function(scope) {
scope.click = function() {
MediatorService.changeState();
};
},
template: '<button ng-click="click()">Click</button>'
};
})
.directive("directiveB", function(MediatorService) {
return {
link:function(scope) {
scope.mediator = MediatorService;
scope.$watch("mediator.state", function(oldValue, newValue) {
if (oldValue == newValue) {
return;
}
console.log("state changed");
});
}
};
});
Full example: http://jsbin.com/darefijeto/1/edit?html,js,console,output
I like an event bus.
Angular does provide $emit on $rootScope but I don't think that should bound your decision to use it for event-based flows if they are complex or foreseeably complex. Angular has lots of features and while most are great, even the authors admit they're mostly meant to compliment good software engineering principles, not replace them.
I like this post on using postal.js: An angular.js event bus with postal.js. The two main benefits are channels and envelopes, which will make for more explicit, understandable and flexible event-based logic.
I find service based approaches to be error prone if state is not managed tightly, which is hard with async calls and injections, where you can't be certain how a service will be multi-purposed in the future.

Angularjs does not update view on async model changes

I've been having a problem with trying to keep my model separate from my controller because of lack of sync between model and view. I have looked around and found that most of the time an apply would solve the issue. However, apply does not work at all for me (either when called from the root scope or the relevant scope using chrome). In this link I have a demo of pretty much the problem I have on my program but instead of intervals my program has asynchronous requests or just complicated functions that seem to also be missed by angular. In the demo I have 4 variables that should be getting updated on the view. One that is being watched by the scope, another that is being updated through a callback, another that is just plain dependent on the model and one that is being updated by passing the scope itself to the service. Out of the 4 only the callback and passing the scope to the service are the ones that update the view, even when I run apply after each update (on top of the one that already runs after each execution of $interval). What I'm trying to avoid is using tons of callbacks or promises whenever my data changes due to transformations since I have many different transformations that are possible. Is there anyway to do this or are callbacks and promises the only option?
var test = angular.module("tpg",[]);
test.controller("myctrl", function($scope, $interval, service)
{
$scope.$watch(service.list.name, function()
{
$scope.name=service.list.name;
});
$scope.op=service.list.op;
$scope.call=service.list.call;
$scope.scope=service.list.test;
$scope.update=function()
{
service.getValues(function(op){$scope.op=op}, $scope);
};
}).factory("service", function($interval, $rootScope)
{
return {
list:{name:"OPA", op:"TAN", call:"1", test:"scope"},
getValues:function(callback, $scope)
{
var self=this;
var interval = $interval(function()
{
if(self.count>2)
{
$interval.cancel(interval);
self.count=0;
self.list={name:"OPA", op:"TAN", call:"1"};
}
else
{
self.list=self.values[self.count];
callback(self.list.op);
$scope.scope=self.list.test;
console.log(self.list);
self.count++;
}
$rootScope.$$phase || $rootScope.$apply();
},2000);
},
values: [{name:"guy", op:"ungly", call:"2", test:"scope1"}, {name:"TAL", op:"stink", call:"3", test:"scope2"}, {name:"tes", op:"test", call:"4", test:"scope3"}],
count:0
};
});
You need only a callback function to be returned from a service. $scope.$apply is not required when dealing with angular services as the service itself triggers the digest run. So I modified the code to remove the $apply and the promise and had a simple callback returned from the service which is then updating the view with the returned data.
Code:
$scope.update=function()
{
service.getValues(function(data){
$scope.name = data.name;
$scope.op=data.op;
$scope.call=data.call;
$scope.scope=data.test;
});
};
}).factory("service", function($interval, $rootScope)
{
return {
list:{name:"OPA", op:"TAN", call:"1", test:"scope"},
getValues:function(callback){
var self=this;
var interval = $interval(function()
{
if(self.count>2)
{
$interval.cancel(interval);
self.count=0;
self.list={name:"OPA", op:"TAN", call:"1"};
}
else
{
self.list=self.values[self.count];
console.log(self.list);
callback(self.list);
self.count++;
}
},2000);
},
values: [{name:"guy", op:"ungly", call:"2", test:"scope1"}, {name:"TAL", op:"stink", call:"3", test:"scope2"}, {name:"tes", op:"test", call:"4", test:"scope3"}],
count:0
};
});
Working plunkr

Categories