How to watch component binding change using Angular component - javascript

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;
}
}

Related

Update Two Way Bound value $onDestroy in Angular 1.5 Component

I am creating a series of dynamic form components using AngularJS 1.5.
I'm using ng-if to show and hide a form compoenent. When the component is destroyed, I remove it from and array in it's parent, but I also want to set the value of the two way bound object that I have passed in to null.
The below is a simplified version of the code I am using:
app.component('textInput', {
bindings: {
model: '='
},
require: {
parent:'^formPanel'
},
templateUrl: 'app/shared/form/formFields/textInput/textInputView.html',
controller: function() {
var self = this;
this.$onInit = function() {
this.parent.addField(this);
},
this.$onDestroy = function() {
this.model = null;
console.log(this);
this.parent.removeField(this);
}
}
});
This logs the value as null, but outside the scope of the component, angular hasn't registered the change and I am unable to run a digest cycle because there is already one in progress for the $onDestroy event.
You should be able to use a callback to achieve this.
So a function similar to this in the parent scope (this is the callback):
$scope.setMyModel = function(val){
$scope.myModel = val;
};
And this in the component HTML file to pass the callback to the component:
<test-input
my-model="myModel"
my-callback="setMyModel "
/>
And this in the component's bindings to collect the callback:
bindings: {
myModel: '=',
myCallback: '='
},
Followed by a call to the callback in the $onDestroy function:
this.$onDestroy = function() {
this.myCallback(null);
this.parent.removeField(this);
}
There may be a better way, but this will ensure that the parent scope is always in control of setting the value that myModel holds.

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.

Sharing data from asynchronous calls amongst directives in AngularJS with TypeScript

I can find bits and pieces of how to solve this, but no concrete way to make it work.
I have an asynchronous call to a server to fetch data in AngularJS and wish to store it in a variable. This variable then needs to be accessible to all the directives in the app, but they obviously all need to wait for the variable to be assigned before they can use it. I'm also using TypeScript and its export functionality to spin directives from their own functions.
Controller
export class MainController{
fundData: Object;
constructor(scope, FundService) {
FundService.fetchData('some_param').then(d => {
let data = d[0],
fundProps = data.properties_pub;
this.fundData = {
'isin': data.clientCode,
'nav': fundProps.nav.value,
'nav_change': fundProps.nav_change.value.toFixed(2),
'nav_change_direction': change,
'total_aum': fundProps.net_asset.value.toFixed(2)
};
scope.ctrl = this;
});
}
}
Directive
class OverviewController {
scope: ng.IScope;
constructor(scope){
scope.$watch('data', newVal => {
console.log(newVal);
});
}
}
OverviewController.$inject = ['$scope'];
export function overview(): ng.IDirective {
return {
restrict : "C",
controller : OverviewController,
controllerAs : "overview",
template : require("../templates/overview"),
bindToController :{
data: '='
}
}
}
HTML
<div ng-controller="MainController">
<div class="overview" data="ctrl.fundData"></div>
</div>
Bootstrap Process
let module = angular.module(MODULE_NAME,[])
.controller('MainController', ['$scope','FundService', MainController])
.service('FundService', FundService)
.directive('overview', overview);
Things I've Tried:
$rootScope
I can set something static and share it, so this works:
$rootScope.data = 2;
This doesn't:
someFunction().then(data => { $rootScope.data = data });
Maybe there's something about promises in $rootScope I don't understand.
Setting in controller
I can set the result of the call to a variable in the controller, set that to an attribute in the directive, and then bind the attribute to its controller, but this doesn't work either, even if I use $watch on the variable.
What I would do is fetch the data, store it in a service (which I think you are already doing) and then broadcast an event when the data in the service is updated. Here's an example (in raw javascript)
module.service('DataService', function(rootScope) {
...
var data;
services.setData = function(newData) {
data = newData;
rootScope.$broadcast('DataUpdated');
};
...
});
And then in your directives all you would need to do is listen for the 'DataUpdated' event:
scope.$on('DataUpdated', ...);
Hope that helps!

How to react to change in data in a service in angularjs on a controller

I want to be able to share data between two controllers so that I can send a boolean to the service from the first controller which is turn triggers a change in the second controller.
Here is what the service looks like
exports.service = function(){
// sets Accordion variable to false ;
var property = true;
return {
getProperty: function () {
return property;
},
setProperty: function(value) {
property = value;
}
};
};
Now the first controller
exports.controller = function($scope, CarDetailsService, AccordionService ) {
$scope.saveDetails = function() {
AccordionService.setProperty(false);
}
}
and the second one
exports.controller = function($scope, AccordionService ) {
$scope.isCollapsed = AccordionService.getProperty();
}
The use case is that when i click on a button on the first controller,the service updates the data inside it, which is then served on the second controller, thus triggering a change in the second controller.
I have been looking around for quite some time but couldn't find a solution to this. Maybe im just stupid.
On the second controller you can $watch the variable you change in the first:
scope.$watch('variable', function(newValue, oldValue) {
//React to the change
});
Alternatively, you can use the $broadcast on the rootScope:
On the first controller:
$rootScope.$broadcast("NEW_EVENT", data);
On the other controller:
scope.$on("NEW_EVENT", function(event, data){
//use the data
});

Angular binding to service value not updating

I cannot get a binded service value to update when it is changed. I have tried numerous methods of doing so but none of them have worked, what am I doing wrong? From everything I have seen, this seems like it should work...
HTML:
<div class="drawer" ng-controller="DrawerController">
{{activeCountry}}
</div>
Controller:
angular.module('worldboxApp')
.controller('DrawerController', ['$scope', 'mapService', function($scope, mapService) {
$scope.$watch(function() { return mapService.activeCountry }, function(newValue, oldValue) {
$scope.activeCountry = mapService.activeCountry;
});
}]);
Service:
angular.module('worldboxApp').
service('mapService', function(dbService, mapboxService, userService) {
this.init = function() {
this.activeCountry = {};
}
this.countryClick = function(e) {
this.activeCountry = e.layer.feature;
};
this.init();
});
I put a break point to make sure the mapService.activeCountry variable is being changed, but all that ever shows in the html is {}.
If you work with objects and their properties on your scope, rather than directly with strings/numbers/booleans, you're more likely to maintain references to the correct scope.
I believe the guideline is that you generally want to have a '.' (dot) in your bindings (esp for ngModel) - that is, {{data.something}} is generally better than just {{something}}. If you update a property on an object, the reference to the parent object is maintained and the updated property can be seen by Angular.
This generally doesn't matter for props you're setting and modifying only in the controller, but for values returned from a service (and that may be shared by multiple consumers of the service), I find it helps to work with an object.
See (these focus on relevance to ngModel binding):
https://github.com/angular/angular.js/wiki/Understanding-Scopes
If you are not using a .(dot) in your AngularJS models you are doing it wrong?
angular.module('worldboxApp', []);
/* Controller */
angular.module('worldboxApp')
.controller('DrawerController', ['$scope', 'mapService',
function($scope, mapService) {
//map to an object (by ref) rather than just a string (by val), otherwise it's easy to lose reference
$scope.data = mapService.data;
$scope.setCountry = setCountry; //see below
function setCountry(country) {
// could have just set $scope.setCountry = mapService.setCountry;
// however we can wrap it here if we want to do something less generic
// like getting data out of an event object, before passing it on to
// the service.
mapService.setCountry(country);
}
}
]);
/* Service */
angular.module('worldboxApp')
.service('mapService', ['$log',
function($log) {
var self = this; //so that the functions can reference .data; 'this' within the functions would not reach the correct scope
self.data = {
activeCountry: null
}; //we use an object since it can be returned by reference, and changing activeCountry's value will not break the link between it here and the controller using it
_init();
function _init() {
self.data.activeCountry = '';
$log.log('Init was called!');
}
this.setCountry = function _setCountry(country) {
$log.log('setCountry was called: ' + country);
self.data.activeCountry = country;
}
}
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<div ng-app="worldboxApp">
<div ng-controller="DrawerController">
<button ng-click="setCountry('USA')">USA</button>
<br />
<button ng-click="setCountry('AUS')">AUS</button>
<br />Active Country: {{data.activeCountry}}
</div>
</div>
In some case $watch is not working with factory object. Than you may use events for updates.
app.factory('userService',['$rootScope',function($rootScope){
var user = {};
return {
getFirstname : function () {
return user.firstname;
},
setFirstname : function (firstname) {
user.firstname = firstname;
$rootScope.$broadcast("updates");
}
}
}]);
app.controller('MainCtrl',['userService','$scope','$rootScope', function(userService,$scope,$rootScope) {
userService.setFirstname("bharat");
$scope.name = userService.getFirstname();
$rootScope.$on("updates",function(){
$scope.name = userService.getFirstname();
});
}]);
app.controller('one',['userService','$scope', function(userService,$scope) {
$scope.updateName=function(){
userService.setFirstname($scope.firstname);
}
}]);
Here is the plunker
Note:- In Some case if broadcast event is not fired instantly you may use $timeout. I have added this in plunker and time depends on your needs. this will work for both factories and services.

Categories