Two way data bindings does not trigger $onChanges in Components - angularjs

Two way data bindings not updating between components
I am setting up inter component communication using two way data binding. I have one parent controller which fetches data from AJAX call and sends that data to 2 components.
I have tried to modify the data that is passed to the components, but if child1 component updates the data, child component is not getting the update data though the two way data binding is present. I read that $onChanges hook will not capture the change event for two way data binding.
<div ng-controller="ParentController as ctrl">
<child1 data="ctrl.data"></child1>
<child2 data="ctrl.data"></child>
</div>
Parent Controller:
var app = angular.module('app',[]);
app.controller('ParentController', function($scope, $get){
//get data from AJAX call
this.data = getDataFromAjaxCall();
}
Child1 Component:
app.component('child1',{
bindings : {
data : '='
},
controller: function($scope){
var self = this;
self.$onChanges = function(changes){
if(changes.data)
console.log('data changed');
}
self.addData = function(){
self.data.push({
id : 10,
name : 'abc'
});
}
}
});
Child2 Component:
app.component('child2',{
bindings : {
data : '='
},
controller: function($scope){
var self = this;
self.$onChanges = function(changes){
if(changes.data)
console.log('data changed');
}
self.addData = function(){
self.data.push({
id : 20,
name : 'pqr'
});
}
}
});
I expect to get the updated data in child1 component if the child2 component modified the data and vice versa.

The $onChanges life-cycle hook only trigger on changes to one-way ("<") and attribute ("#") bindings. It does not trigger on changes to two-way ("=") bindings.
With components, use one-way ("<") binding for inputs and expression ("&") binding for outputs:
app.component('child1',{
bindings: {
̶d̶a̶t̶a̶ ̶:̶ ̶'̶=̶'̶
facts: "<",
factsChange: "&",
},
controller: function(){
this.$onChanges = function(changes){
if(changes.facts)
console.log('facts changed');
}
}
});
Avoid using two-way ("=") bindings. They make migration to Angular 2+ more difficult.
For more information, see AngularJS Developer Guide - Component-Based Application Architecture.
Also be careful with bindings that start with data. Directive normalization will strip names that start with data-. See AngularJS Developer Guide - Directive Normalization.
Functions that do XHRs can't return data. They can only return promises from which data need to be extracted.
var app = angular.module('app',[]);
app.controller('ParentController', function($scope, $get){
//get data from AJAX call
̶t̶h̶i̶s̶.̶d̶a̶t̶a̶ ̶=̶ ̶g̶e̶t̶D̶a̶t̶a̶F̶r̶o̶m̶A̶j̶a̶x̶C̶a̶l̶l̶(̶)̶;̶
var promise = getDataFromAjaxCall();
promise.then( response => {
this.data = response.data;
});
}
JavaScript browsers uses a single-threaded non-blocking event-driven architecture for IO. Programmers familiar with imperative programming styles need to change the way they think about IO with JavaScript browsers.

Related

AngularJs : How to call child component method from parent components

I am doing Migration of AngularJS application to Angular 4.Currently preparing for migration that is conversion to Component based architechture.
My question is how can I call child component method from parent component.
I know in Angular 4 we can use #ViewChild to access the child method. Is there any equivalent for angularjs 1.6 or any other approach that can help in smoother migration.
In angularJS we use $broadcast event to communicate with parent controller to child controller. But this is a $scope method. Since angular 2 drops the scope, you cannot use this event.
The best solution is already mentioned in the question. ViewChild allows much more control to the user compare to the broadcasts. So my recommendation is to go with ViewChild
Since Angular2+ event streams are passed to components using RxJS observable objects, to make the migration to Angular 2+ smoother, use RxJS observables in AngularJS as well.
To add rxJS to AngularJS components:
<script src="//unpkg.com/angular/angular.js"></script>
<script src="//unpkg.com/rx/dist/rx.all.js"></script>
<script src="//unpkg.com/rx-angular/dist/rx.angular.js"></script>
var app = angular.module('myApp', ['rx']);
app.controller("parentCtrl", function($scope, rx) {
$scope.subject = new rx.Subject();
$scope.onEvent = function(message) {
$scope.subject.onNext(message);
};
});
app.component("childComponent", {
controller: "childCtrl",
bindings: { subject: "<" },
template: `<div>{{$ctrl.message}}</div>`
});
app.controller("childCtrl", function(rx) {
var subscription;
this.$onChanges = function(changes) {
if (changes.subject} {
subscription = subject.subscribe(function onNext(message) {
console.log(message);
$ctrl.message = message;
});
};
};
this.$onDestroy = function() {
if (subscription) {
subscription.dispose();
};
};
});

How to use bindings in controller in an AngularJs 1.6 component

Hello here is my component :
angular.module('myApp').component('dendroCtrl', {
templateUrl: '/templates/dendro.html',
bindings: {
id: '=',
type: '=',
mini: "="
},
controller: function ($scope, Api) {
//Dendro
var test = mini;
I tried this :
var test = mini;
var test = this.mini;
var test = $scope.mini;
Mini is everytime undefined.
How could I use my bindings in my controller ? Thanks
EDIT
This is how I call the component :
<dendro-ctrl id="149" type="Demand" mini="false"></dendro-ctrl>
From the documentation:
Components have a well-defined lifecycle Each component can implement "lifecycle hooks". These are methods that will be called at certain points in the life of the component. The following hook methods can be implemented:
$onInit() - Called on each controller after all the controllers on an element have been constructed and had their bindings initialized (and before the pre & post linking functions for the directives on this element). This is a good place to put initialization code for your controller.
$onChanges(changesObj) - 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
So you can just use:
controller: function(Api) {
var ctrl = this;
ctrl.$onInit = function() {
console.log(ctrl.mini);
}
}
if you want to display the initial value of the mini binding.

How to resolve Angular Service directly from an Angular Component

I am currently using Angular 1.5. I am using ui-router as my primary navigation mechanism. I am leveraging Angular components.
I understand that I can use .resolve on my states to instantiate services which are then passed down through my component hierarchy (mostly using one-way bindings).
One of my components is called literatureList and is used in more than one route/state. The literatureList component makes use of a specific service called literatureListService. literatureListService is only used by literatureList. literatureListService takes a while to instantiate, and uses promises etc.
In each of the .state definitions then I need to have a .resolve that instantiates literatureListService. This means that I need to refer to this literatureListService in each of the .state.resolve objects. This doesn't seem very DRY to me.
What I'd really like to do is remove the literatureListService references from the .state.resolve objects and 'resolve' the service from 'within' the literatureList component itself.
How do I code a 'resolve-style' mechanism within the literatureList component that will handle the async/promise nature of literatureListService? What is best practice for doing this?
Code snippets follow:
state snippets:
$stateProvider.state({
name: 'oxygen',
url: '/oxygen',
views: {
'spofroot': { template: '<oxygen booklist="$resolve.literatureListSvc"></oxygen>' }
},
resolve:{
literatureListSvc: function(literatureListService){
return literatureListService.getLiterature();
}
}
});
$stateProvider.state({
name: 'radium',
url: '/radium',
views: {
'spofroot': { template: '<radium booklist="$resolve.literatureListSvc"></radium>' }
},
resolve:{
literatureListSvc: function(literatureListService){
return literatureListService.getLiterature();
}
}
});
literatureListService:
angular.module('literature')
.factory('literatureListService',function($http,modelService){
// Remember that a factory returns an object, whereas a service is a constructor function that will be called with 'new'. See this for a discussion on the difference: http://blog.thoughtram.io/angular/2015/07/07/service-vs-factory-once-and-for-all.html
console.log('literatureListService factory is instantiating - this will only happen once for each full-page refresh');
// This is a factory, and therefore needs to return an object containing all the properties that we want to make available
var returnObject = {}; // Because this is a factory we will need to return a fully-formed object (if it was a service we would simply set properties on 'this' because the 'context' for the function would already have been set to an empty object
console.log('service instantiation reporting that modelManager.isDataDirty='+modelService.isDataDirty);
// The getLiterature method returns a promise, and therefore can only be consumed via a promise-based mechanism
returnObject.getLiterature = function($stateParams){
console.log('literatureService.getLiterature will now return a promise (via a call to $http)');
return $http({method: 'GET', url: 'http://localhost:3000/literature/'});
};
return returnObject;
});
oxygen component html:
<div>
This is the OXYGEN component which will now render a literature list, passing in bookList.data as books
<literature-list books="$ctrl.booklist.data"></literature-list>
</div>
oxygen component js
angular.module('frameworks')
.component('oxygen',{
templateUrl:"frontend/framework/frameworks/oxygenComponent.html",
controller:function($http){
var $ctrl = this;
console.log('Hello from the oxygen component controller with literatureListSvc='+$ctrl.booklist); // Bound objects NOT YET AVAILABLE!!!!!!
this.$onInit = function() {
//REMEMBER!!!! - the bound objects being passed into this component/controller are NOT available until just before the $onInit event fires
console.log('Hello from the oxygen component controller onInit event with bookList='+JSON.stringify($ctrl.booklist));
};
}
,bindings:{ // remember to make these lowercase!!!
booklist:'<'
}
});
literatureList component html:
<div>
{{$ctrl.narrative}}
<literature-line ng-repeat="literatureItem in $ctrl.books" wizard="fifteen" book="literatureItem" on-tut="$ctrl.updateItemViaParent(itm)">555 Repeat info={{literatureItem.title}}</literature-line>
</div>
literatureList component js
angular.module('literature')
.component('literatureList',{
templateUrl:'frontend/literature/literatureListComponent.html',
//template:'<br/>Template here33 {{$ctrl.listLocalV}} wtfff',
// controller:function(literatureListService){
controller:function(){//literatureListService){
var $ctrl=this;
this.narrative = "Narrative will unfold here";
this.updateItemViaParent = function(book){
this.narrative = 'just got notified of change to book:'+JSON.stringify(book);
};
this.$onInit = function(){
console.log('literatureList controller $onInit firing with books='+JSON.stringify($ctrl.books));
};
this.$onChanges = function(){
console.log('literatureList controller $onChanges firing');
};
},
bindings: {
books:'<'
}
});
As JavaScript in reference based, you can crete object in your service and access it in all three controllers that you need.
For Example:
function serviceA() {
var vm = this;
vm.testObject = {};
vm.promise1().then(function(response) {
vm.testObject = response;
})
}
function ControllerA($scope, serviceA) {
$scope.testA = service.testObject;
}
In this case, as soon as the promise is resolved, all the controllers will get the value of the response and can be used in the partials respecively

Inter-Controller communication, the angular way

I'm trying to figure out the "preferred" or "angular-way" of sharing properties or state between controllers/directives. There are several methods to implement this, but I want to keep with best-practice. Below are some banal examples of how this can be implemented:
1. Using $scope.$watch
// The parent controller/scope
angular.module('myModule').controller('parentController', ['$scope', function($scope) {
$scope.state = {
myProperty: 'someState'; // Default value to be changed by some DOM element
};
}]);
// The child controller/scope.
angular.module('myModule').controller('childController', ['$scope', function($scope) {
$scope.$watch('state.myProperty', function (newVal) {
// Do some action here on state change
});
}]);
Edit: Based on answers below, this is bad practice and should be avoided. It is untestable and places an unwanted DOM dependancy.
2. Using $broadcast
// The parent controller
angular.module('myModule').controller('parentController', ['$scope', function($scope) {
var myProperty = 'someState';
$scope.setState = function (state) {
myProperty = state; // Set by some other controller action or DOM interaction.
$scope.$broadcast('stateChanged', state); // Communicate changes to child controller
}
}]);
// The child controller.
angular.module('myModule').controller('childController', ['$scope', function($scope) {
$scope.$on('stateChanged', function (evt, state) {
// Do some action here
}
}]);
Edit: Equally bad practice as you need to know the placement of the controllers in the DOM in order to determine weather to use $broadcast (down the DOM) or $emit (up the DOM).
3. Using service
angular.module('myModule').factory('stateContainer', [function () {
var state = {
myProperty: 'defaultState'
},
listeners = [];
return {
setState: function (newState) {
state.myProperty = newState;
angular.forEach(listeners, function (listener) {
listener(newState);
});
},
addListener: function (listener) {
listeners.push(listener);
}
}
}]);
// The parent controller
angular.module('myModule').controller('parentController', ['$scope', 'stateContainer', function($scope, stateContainer) {
$scope.setState = function (state) {
stateContainer.setState(state);
};
}]);
// The child controller.
angular.module('myModule').controller('childController', ['$scope', 'stateContainer', function($scope, stateContainer) {
stateContainer.addListener(function (newState) {
// Do some action here
});
}]);
There are probably some approaches I've missed here, but you get the idea. I'm trying to find the best approach. Although verbose, I personally lean towards #3 in the list here. But I come from a Java and jQuery background where listeners are widely used.
Edit: Answers below are insightful. One talks of sharing state between parent/child directives using the require directive configuration. The other talks of sharing service or service properties directly to the scope. I believe that depending on the need, they are both right in what is or is not best practice in Angular.
Any of these will work if done correctly, but a variant on service is the preferred way AFAIK.
The question is, do you even need a listener in the service case? Angular itself will update any views (which is the purpose of the controller), so why do you need a listener or watch? It is sufficient to change the value itself for the view to be changed.
app.factory('stateService',function() {
return {
myState: "foo"
}
})
.controller('one',function($scope,stateService) {
$scope.changeState = function() {
stateService.myState = $scope.state;
};
})
.controller('two',function($scope,stateService) {
$scope.svc = stateService;
})
You can then do the following in your view (incomplete):
<div ng-controller="one">
<input name="state" ng-model="state"></input>
<button type="submit" ng-click="changeState()">Submit</button>
</div>
<div ng-controller="two">{{svc.myState}}</div>
Truth is, you don't even need to go that far with having a button and a function. If you just tie the ng-model together it will work:
<div ng-controller="one">
<input name="state" ng-model="svc.myState"></input>
</div>
<div ng-controller="two">{{svc.myState}}</div>
Try the following jsfiddle http://jsfiddle.net/cwt9L6vn/1/
There is no such thing as parent and child controllers in AngularJS. There are only parent and child directives, but not controllers. A directive can have a controller that it exposes as an API to other directives.
Controllers are not related to the DOM hierarchy so they can't have children. They also don't create their own scope. So you never know if you have to $broadcast or $emit to talk to other controllers.
If you start using $broadcast from a controller, then you're going to get stuck not knowing if the other controller is up or down. That's when people start doing stuff like $rootScope.$broadcast(..) which is a very bad practice.
What you are looking for are directives that require other directives.
var app = angular.modeul('myApp',[]);
// use a directive to define a parent controller
app.directive('parentDir',function() {
return {
controller: function($scope) {
this.myFoo = function() {
alert("Hello World");
}
}
});
// use a directive to enforce parent-child relationship
app.directive('childDir',function() {
return {
require: '^parentDir',
link: function($scope, $el, $attr, parentCtrl) {
// call the parent controller
parentCtrl.myFoo();
}
});
Using the require feature of a directive does two important things.
Angular will enforce the relationship if it's not optional.
The parent controller is injected into the child link function.
There is no need to $broadcast or $emit.
Another option that is also effective is to use directives to expose an API.
// this directive uses an API
app.directive('myDir',function() {
return {
scope: {
'foo': '&'
},
link: function($scope, $el, $attr) {
// when needed, call the API
$scope.foo();
}
});
// in the template
<div ng-controller="parentController">
<div my-dir foo="parentController.callMyMethod();"></div>
</div>

Communication impossible between controllers

PRELIMINARIES
I am developing a web app using angularjs. At some point, my main controller connects to a web service which sends data continuously. To capture and process the stream I am using (http://ajaxpatterns.org/HTTP_Streaming). Everything works like a charm. I would like to share these streaming data with another controller that will process and display them via a jquery chart library (not yet decided which one I gonna use but it is out of the scope of this question). To share these data I have followed this jsfiddle (http://jsfiddle.net/eshepelyuk/vhKfq/).
Please find below some relevant parts of my code.
Module, routes and service definitions:
var platform = angular.module('platform', ['ui']);
platform.config(['$routeProvider',function($routeProvider){
$routeProvider.
when('/home',{templateUrl:'partials/home.html',controller:PlatformCtrl}).
when('/visu/:idVisu', {templateUrl: 'partials/visuTimeSeries.html',controller:VisuCtrl}).
otherwise({redirectTo:'/home',templateUrl:'partials/home.html'})
}]);
platform.factory('mySharedService', function($rootScope) {
return {
broadcast: function(msg) {
$rootScope.$broadcast('handleBroadcast', msg);
}
};
});
PlatformCtrl definition:
function PlatformCtrl($scope,$http,$q,$routeParams, sharedService) {
...
$scope.listDataVisu ={};
...
$scope.listXhrReq[idVisu] = createXMLHttpRequest();
$scope.listXhrReq[idVisu].open("get", urlConnect, true);
$scope.listXhrReq[idVisu].onreadystatechange = function() {
$scope.$apply(function () {
var serverResponse = $scope.listXhrReq[idVisu].responseText;
$scope.listDataVisu[idVisu] = serverResponse.split("\n");
sharedService.broadcast($scope.listDataVisu);
});
};
$scope.listXhrReq[idVisu].send(null);
var w = window.open("#/visu/"+idVisu);
$scope.$on('handleBroadcast', function(){
console.log("handleBroadcast (platform)");
});
}
VisuCtrl definition:
function VisuCtrl($scope,$routeParams,sharedService) {
$scope.idVisu = $routeParams.idVisu;
$scope.data = [];
/* ***************************************
* LISTENER FOR THE HANDLEBROADCAST EVENT
*****************************************/
$scope.$on('handleBroadcast', function(event,data){
console.log("handleBroadcast (visu)");
$scope.data = data[$scope.idVisu];
});
}
Injection:
PlatformCtrl.$inject = ['$scope','$http','$q','$routeParams','mySharedService'];
VisuCtrl.$inject = ['$scope','$routeParams','mySharedService'];
PROBLEM DEFINITION
When running this code, it looks like only the PlatformCtrl controller listens for the handleBroadcast event. Indeed, having a look to the console all what is displayed is only handleBroadcast (platform) every time new data arrive. I am very surprised because I have read in the official documentation that the $broadcast function
dispatches an event name downwards to all child scopes (and their
children) notifying the registered ng.$rootScope.Scope#$on listeners.
Since all the scopes in a given app inherits from $rootScope, I do not get why the $on function in VisuCtrl is not launched every time new data are broadcasted.
What I think is that when you open a new browser window you are launching a new AngularJS instance. This way it's not possible that the two controllers are able to communicate via a service.
If you have problems with scopes communicating, you can inject the $rootScope and see whether all the scopes that should communicate are actually instanciated.
function VisuCtrl($scope, $routeParams, sharedService, $rootscope) {
console.log($rootScope);
}
Your request flow comes out of the angular, therefore it would not be recognized until the next $digest phase (see how angular handles two-way binding via dirty matching). To get in to the angular world you need to use $apply:
$scope.listXhrReq[idVisu].onreadystatechange = function() {
$scope.$apply(function () {
var serverResponse = $scope.listXhrReq[idVisu].responseText;
$scope.listDataVisu[idVisu] = serverResponse.split("\n");
sharedService.broadcast($scope.listDataVisu);
});
};
Could it be that your VisuCtrl hasn't been initialized yet, since you are using custom routing?
Is it still the same, when you navigate to /visu/:idVisu?

Resources