I have an angular component:
module.exports = {
bindings: {
vulnerability: '<',
},
controller: ['$scope', function($scope){
//want to get value of vulnerability from above
$scope.vulnerability // doesn't work
}],
};
which i reference in an ng-repeat
<div ng-repeat="vulnerability in vulnerabilities" class="vulnerability-item">
<vulnerability-item vulnerability="vulnerability"> </vulnerability-item>
</div>
The value of vulnerability isn't going to change once the directive is called so I don't need to watch that value, just to reference it once it gets set.
I did put a $scope.$watch on the property just to see if it worked and it did trigger but I couldn't see the new or old value in the handler of the watch so it didn't help me anyway.
I tried $scope.vulnerability and $ctrl.vulnerability which is how I'd reference that property in the directives template but neither worked. How do I get the bound value that is passed in?
adding this watch:
$scope.$watch('vulnerability', function (oldV, newV) {
console.log('old', oldV)
console.log('new', newV)
})
I get a new undefined and old undefined in the console for each instance of the component although if i change $scope.$watch('vulnerability' to $scope.$watch('nonsense') It still fires the logs once for each component.
If the value isn't supposed to be changed, it should be bound once, vulnerability="::vulnerability". This gives the context to the question (and also saves some time on optimization).
In this case $onInit hook may be used:
...
controller: function() {
this.$onInit = function () {
this.vulnerability
}
},
Related
I am using controllerAs syntax like so:
.state('operators', {
url: '/operators',
templateUrl: 'app/features/operators/operators.html',
controller: 'OperatorsController',
controllerAs: 'vm'
})
And have a variable in my controller vm.selectedOperators which I want to watch for changes on:
var vm = this;
vm.selectedOperators = [];
$scope.$watch(function() {
return vm.selectedOperators;
}, function(current) {
console.log(current.length);
});
In the view, an expression is used to show the count of items in the variable.
{{vm.selectedOperators.length}} item selected
This variable gets updated from elsewhere (Its being used by a multi-select table directive I have written which is used on the same view) and I see this expression updating fine in the view.
The problem is my watcher in the controller doesn't fire after the variable gets initialised, and I am not sure why.
Can anyone offer any help?
Thanks
UPDATE
I should have mentioned, I have already tried using the expression equivalent like so:
$scope.$watch('vm.selectedOperators', function (current){
console.log(current);
});
But this still does not fire for me
Concept: There is an optional third argument to the $watch function - objectEquality
$watch(watchExpression, listener, [objectEquality]);
You must set it to true when you are trying to watch an object otherwise angular will only match the reference of both the objects. By setting it true angular compares the old object and new using angular.equals
Read more about it here:
https://docs.angularjs.org/api/ng/type/$rootScope.Scope
$scope.$watch(function() {
return vm.selectedOperators;
}, function(current) {
console.log(current.length);
}, true);
Update
As #Martijn Welker pointed out in the comment below, $watchCollection can be useful if only single level equality has to be matched instead of complete object tree.
$scope.$watchCollection(function() {
return vm.selectedOperators;
}, function(current) {
console.log(current.length);
});
Try this (ES6 syntax):
$scope.$watch('vm.selectedOperators', (new, old) => {
console.log('new:', new, 'old:', old);
});
You should be able to see the changes.
Try using Deep Watch Array object..
Deep watch will watch each object in array list. It's little bit expensive in process than normal watch So use if normal watch doesn't work:-
Here is code
$scope.$watch('vm.selectedOperators', function (current) {
console.log(current.length);
},true);
I have this component in my app:
<actions-bar actions="$ctrl.actionsBarData"></actions-bar>
Here is the controller / component definition:
.component('actionsBar', {
controller: 'actionsBarController',
bindings: {
actions: '<'
}
})
.controller('actionsBarController', function() {
var vm = this;
vm.$onInit = function() {
console.log(vm.actions);
setTimeout(function() {
console.log(vm.actions);
}, 500);
};
});
Take note of the two console.log statements within $onInit. In the browser the first logging statement prints undefined. The second statement, wrapped in setTimeout correctly prints out the this.actions object.
According to the Angular docs:
$onInit() - Called on each controller after all the controllers on an
element have been constructed and had their bindings initialized
If the bindings have been initialized, why is the data not immediately available? Why must I set a delay of 500ms before it is available?
If $onInit isn't the solution here, how can I reliably access the bindings data within my controller? It appears that this is correct lifecycle hook to use, and none of the other hooks appear to be what I want in this case.
This is because while the bindings have been initialized in the controller, they haven't necessarily been initialized where they are coming from. If you are populating your binding with an async call, the object is going to be undefined or empty.
$onChanges allows you to inspect changes that happen to your bindings.
vm.$onChanges = function (changesObj) {
console.log(changesObj.actions);
}
If what I described above is happening, this will fix it for you.
EDIT: a word
I don't understand why $onChanges isn't kicked off when I change a bound primitive in an input. Can someone see what I've done wrong, and explain this in an uncomplicated way? I made a plunkr of a quick test application after I couldn't get it to work in my actual application either.
angular
.module('test', [])
.component('test', {
template: '<child application="vm.application"></child>',
controller: 'testCtrl as vm'
})
.controller('testCtrl', function() {
var vm = this;
vm.$onInit = function () {
vm.application = {
data: {
name: 'Test'
}
}
};
})
.component('child', {
template: '<input type="text" ng-model="vm.application.data.name">',
bindings: {
application: '<'
},
controller: 'childCtrl as vm'
})
.controller('childCtrl', function() {
var vm = this;
vm.$onChanges = function (changes) {
console.log('CHANGED: ', changes);
};
})
The $onChanges method is not called for changes on subproperties of an object. Default changes to objects generally follow this sequence within a components lifetime:
UNINITIALIZED_VALUE to undefined
undefined to {} or { someAttribute: someValue, .. }
({..} to undefined if you delete the object in a parent scope)
In order to watch subproperties you could use the $doCheck method that was added in 1.5.8. It is called on every digest cycle and it takes no parameters. With great power comes great responsibility. In that method you would put logic that detects whether a certain value has been updated or not - the new value will already be updated in the controller's scope, you just need to find a way to determine if the value changed compared to the previously known value.
You could set a previousValueOfObjectAttribute variable on the controller before you start to expect changes to this specific attribute (e.g. when subcomponent B calls an output binding function in component A, based on which the target object - which is an input binding to B - in A changes). In cases where it is not predictable when the change is about to occur, you could make a copy of the specific atributes of interest after any change observed via the $doCheck method.
In my specific use case, I did not explicitly check between an old and new value, but I used a promise (store $q.defer().promise) with the intention that any change I would 'successfully' observe in the $doCheck method would resolve that promise. My controller then looked something like the following:
dn.$doCheck = function () {
if (dn.waitForInputParam &&
dn.waitForInputParam.promise.$$state.status === 0 &&
dn.targetObject.targetAttribute !== false)
dn.waitForInputParam.resolve(dn.targetObject.targetAttribute);
}
dn.listenToInputChange = function () {
dn.waitForInputParam = $q.defer();
dn.waitForInputParam.promise.then(dn.onInputParamChanged);
}
dn.onInputParamChanged = function (value) {
// do stuff
//
// start listening again for input changes -- should be async to prevent infinite $digest loop
setTimeout(dn.listenToInputChange, 1);
}
(w.r.t. promise.$$state.status, see this post).
For all other intents and purposes, watching changes to primitive data types, you should still use $onChanges. Reference: https://docs.angularjs.org/guide/component
It's $onChanges and not $onChange.
Also, the onChange only updates when the parent value is changed, not the child. Take a look at this plunkr. Note the console.log only fires when you type in the first input.
As others said above, Angular does not watch for changes in object properties, however, you can make Angular believe that your object is changed by reference.
It is sufficient to do a shallow copy of the object in order to trigger an $onChanges event:
vm.campaign = angular.extend({}, vm.campaign);
Credits to #gkalpak
Dealing with $onChanges is tricky. Actually, thats why in version 1.5.8 they introduced the $doCheck, similar to Angular 2 ngDoCheck.
This way, you can manually listen to changes inside the object being listened, which does not occur with the $onChanges hook (called only when the reference of the object is changed). Its the same thing, but it gets called for every digest cycle allowing you to check for changes manually (but better then watches).
For more details, see this blog post.
I have a method in a controller which executes the following code:
this.StockService.GetByInvoicesID(this.SelectedInvoice.ID).success((StockItems) =>
{
this.StockItems = StockItems;
this.CreditNoteStockItems = new Array<ViewModels.CreditNoteStockItemViewModel>();
}
Before this service method is called, all members in the controller are defined. However, once the promise resolves, this.StockItems and this.CreditNoteStockItems are all undefined. Furthermore, the assignment of StockItems is not being reflected in the view. I'm guessing this is a scope issue and the promise is returning into a new scope. This has happened with other methods before, it almost seems to happen at random.
My questions are, can anyone tell me why exactly this is happening, and more importantly, how can I prevent it from occurring?
edit: This is a simplified version of my controller (all thats missing is several members and methods)
edit 2: more info about method in controller
export class CreditNoteController
{
static $inject = ['$scope', '$modalInstance', 'StockService'];
StockService: Services.StockService;
ModalInstance: ng.ui.bootstrap.IModalServiceInstance;
constructor($scope, $modalInstance, StockService: Services.StockService)
{
$scope.vm = this;
this.ModalInstance = $modalInstance;
this.StockService = StockService;
}
InvoicesSelectionChanged()
{
this.StockService.GetByInvoicesID(this.SelectedInvoice.ID).success((StockItems) =>
{
this.StockItems = StockItems;
this.CreditNoteStockItems = new Array<ViewModels.CreditNoteStockItemViewModel>();
});
}
}
The controller is injected through the angular UI modal service open method sitting in another controller:
this.ModalService.open(
{
templateUrl: URL,
controller: Controllers.CreditNoteController,
});
edit 2: The javascript that it generates
CreditNoteModalController.prototype.InvoicesSelectionChanged = function () {
var _this = this;
this.StockService.GetByInvoicesID(this.SelectedInvoice.ID).success(function (StockItems) {
_this.StockItems = StockItems;
_this.CreditNoteStockItems = new Array();
});
};
Thanks
After some back and forth in the comment thread, turns out there were 2 things going on here, so consolidating the info into the answer:
When inspecting 'this' in a debugger in a .ts file, know that you will be seeing the actual 'this' value and not the _this alias that is created in the .js file when you use arrow functions. This can sometimes make it look like variables aren't getting set when in reality, you are inspecting the wrong variable. Setting a watch on _this will show the right one. You also have the option of using console.log on 'this' in the TypeScript code itself, since it will compile to using the _this alias.
ng-options won't work without ng-model
is there a way to make the $broadcast propagate the variable to the $on during initialization phase?
<div ng-app='test'>
<div ng-controller='testCtrl'> <span>{{testContent}}</span>
</div>
<div ng-controller="testCtrl2">
<input type='text' ng-change="updateContent()" ng-model="testContent2" />
</div>
</div>
var app = angular.module('test', []);
app.factory('sharedContent', function ($rootScope) {
var standardContent;
var resizeProportion;
return {
setStandardContent: function (newStandardContent) {
standardContent = newStandardContent;
$rootScope.$broadcast('updateContent');
console.log('broadcast');
},
getStandardContent: function () {
return standardContent;
},
setResizeProportion: function (newResizeProportion) {
$rootScope.$broadcast('updateResizeProportion');
},
}
});
app.run(function (sharedContent) {
sharedContent.setStandardContent('haha');
});
function testCtrl($scope, sharedContent) {
$scope.testContent;
$scope.$on('updateContent', function () {
console.log('receive');
$scope.testContent = sharedContent.getStandardContent();
});
}
function testCtrl2($scope, sharedContent) {
$scope.testContent2 = 'test';
$scope.updateContent = function () {
sharedContent.setStandardContent($scope.testContent2);
};
}
Sample fiddle : http://jsfiddle.net/jiaming/NsVPe/
The span will display the value as the input changes, which is due to the ng-change function.
However, at initialization phase, the value "haha" was not propagated to the $scope.testContent and thus, nothing was shown at first runtime. Is there a way to make the value "haha" appear at the first runtime?
Thank you.
Just provide a little delay using $timeout function. Just update the code in the factory it will start working.
Please refer the code below for the factory:
app.factory('sharedContent', function ($rootScope,$timeout) {
var standardContent;
var resizeProportion;
return {
setStandardContent: function (newStandardContent) {
standardContent = newStandardContent;
$timeout(function(){
$rootScope.$broadcast('updateContent');
},0)
console.log('broadcast');
},
getStandardContent: function () {
return standardContent;
},
setResizeProportion: function (newResizeProportion) {
$rootScope.$broadcast('updateResizeProportion');
},
}
});
The reason for this is that the ng-change triggers upon subsequent changes to the model identified by testContent2. When the controller initializes, the value "test" is assigned to it. ng-change then keeps a track of subsequent changes - the initial assignment does not qualify for this, only subsequent changes do.
http://jsfiddle.net/vZwy4/ - I updated the fiddle provided by you. Here you can see that the span tag is correctly populated with the data.
What you needed to do was instead of using ng-change, you should use the scope's $watch functionality. So remove the ng-change directive from the input box and remove the updateContent method. Instead, replace it with the following code wherein you watch the changes to the testContent2 model:
$scope.$watch('testContent2', function () {
if ($scope.testContent2 === undefined || $scope.testContent2 === null) {
return;
}
sharedContent.setStandardContent($scope.testContent2);
});
You can now see that the word "test" (I could not find anything to do with 'haha') appears the moment the page loads. Subsequent changes to the input are also updated in the span. Hope this is what you were looking for.
The thing that you are not taking into account that the run phase of the app gets executed before the controllers are initialized. Because broadcasted messages don't get buffered and are only served to the listeners that are listening in the moment the message is created, the haha value gets lost.
In your case, however, it's quite easy to make it work with a small change in your controller:
function testCtrl($scope, sharedContent) {
updateTestContent();
$scope.$on('updateContent', updateTestContent);
function updateTestContent(){
$scope.testContent = sharedContent.getStandardContent();
}
}
I forked your JSFiddle here http://jsfiddle.net/y3w5r01d/2/ where you can see on the console when each function (run and controllers) gets executed.