Communication between spread out AngularJS components - javascript

We have various components in the application that are not in parent/child or sibling relationships. Let's say a checkbox that when in checked state is supposed to change the state of another component which is in a completely different container.
The application is over 500 different views, so a controller for each one is not an option. Those interactions are also completely custom, so we would need tens of methods to cover all of them (checkbox to tab, multiple checkboxes to tab, multiple checkboxes to more checkboxes etc).
What is the best course of action here? So far we thought about a globally available service to register components by id and then subscribe the dependent components to listen for the status change on that particular id in the service (for example in an ng-if directive to toggle), or use Redux. We have no previous experience with complex relationships like that.
Any ideas or similar experiences would be greatly appreciated.

The Observer pattern as you describe it is being implemented in angularjs with event emmiters ($broadcast $emit) so there is no need to create an independent service.
The point of component based applications is to have some tree structured architecture. So in those cases the child component notifies the parent and then the parent notifies some other child maybe and goes on.
If your application is not structured like this you might consider a refactoring but for now you could just bind some event emitters.

To solve this issue use the publish/subscribe pattern that allow get a loosely-coupled architecture.
On an AngularJS application a great library is postaljs that allow implements this pattern easely:
Define at app.config a $bus $scope variable that will be accesible on all places of the application: controlers, directives, ...
app.config(function($provide) {
$provide.decorator('$rootScope', [
'$delegate',
function($delegate) {
Object.defineProperty($delegate.constructor.prototype,
'$bus', {
get: function() {
var self = this;
return {
subscribe: function() {
var sub = postal.subscribe.apply(postal, arguments);
self.$on('$destroy',
function() {
sub.unsubscribe();
});
},
channel: function() {
return postal.channel.apply(postal, arguments);
},
publish: function() { postal.publish.apply(postal, arguments); }
};
},
enumerable: false
});
return $delegate;
}
]);
});
Publish
Publish on item updated.
var channel = $scope.$bus.channel('myresources');
channel.publish("item.updated", data);
Publish on list updated
var channel = $scope.$bus.channel('myresources');
....
channel.publish("list.updated", list);
Subscribe
The controller/directive that needs be notified for an event on the "myresources" channel.
var channel = $scope.$bus.channel("myresources");
....
//The wildcard * allow be notified on item/list. updated
channel.subscribe("*.updated", function(data, envelopment) {
doOnUpdated();
});

Related

Angular: circular dependency of specific case

Some time ago, I started to refactor my code of the main project, decoupling the business logic from controllers to services, according to guidelines. Everything went well, until I faced the problem of circular dependency (CD). I read some resources about this problem:
Question 1 on Stack Overflow
Question 2 on Stack Overflow
Miško Hevery blog
Unfortunately, for me it is not clear now, how can I solve the problem of CD for my project. Therefore, I prepared a small demo, which represents the core features of my project:
Link to Plunker
Link to GitHub
Short description of components:
gridCtrl Does not contain any business logic, only triggers dataload and when the data is ready, show grid.
gridMainService This is the main service, which contains object gridOptions. This object is a core object of grid, contains some api, initialized by framework, row data, columns headers and so on. From this service, I was planning to control all related grid stuff. Function loadGridOptions is triggerd by gridCtrl and waits until the row data and column definitions are loaded by corresponding services. Then, it initializes with this data gridOptions object.
gridConfigService This is simple service, which used to load column definitions. No problem here.
gridDataService This service used to load row data with function loadRowData. Another feature of this service: it simulates the live updates, coming from server ($interval function). Problem 1 here!
gridSettingsService This service is used to apply some settings to the grid(for example, how the columns should be sorted). Problem 2 here!
Problem 1:
I have a circular dependency between gridMainService and gridDataService. First gridMainService use gridDataService to load row data with function:
self.loadRowData = function () {
// implementation here
}
But then, gridDataService receives the updates from server and has to insert some rows into grid. So, it has to use gridMainService:
$interval(function () {
var rowToAdd = {
make: "VW " + index,
model: "Golf " + index,
price: 10000 * index
};
var newItems = [rowToAdd];
// here I need to get access to the gridMainService
var gridMainService = $injector.get('gridMainService');
gridMainService.gridOptions.api.addItems(newItems);
index++;
}, 500, 10);
So, here I faced first CD.
Problem 2:
I have a circular dependency between gridMainService and gridSettingsService. First, gridMainService triggering, when the grid is initially loaded and then it sets up default settings by the following function:
self.onGridReady = function () {
$log.info("Grid is ready, apply default settings");
gridSettingsService.applyDefaults();
};
But, to make some changes, gridSettingsService need an access to the gridMainService and its object gridOptions in order to apply settings:
self.applyDefaults = function() {
var sort = [
{colId: 'make', sort: 'asc'}
];
var gridMainService = $injector.get('gridMainService');
gridMainService.gridOptions.api.setSortModel(sort);
};
Question:
How can I solve these CD cases in proper way? Because, Miško Hevery Blog was quite short and good, but I have not managed to apply his strategy to my case.
And currently, I don't like an approach of manual injection a lot, because I will have to use it a lot and the code looks a little bit fuzzy.
Please note:
I prepared only a demo of my big project. You can probably advice to put all code in gridDataService and ready. But, I already has a 500 LOC in this service, if I merge all services, it will be a ~1300 LOC nightmare.
In these kind of problems there are many solutions, which depend by the way you are thinking. I prefer thinking each service (or class) has some goal and needs some other service to complete its goal, but its goal is one, clear and small. Let’s see your code by this view.
Problem 1:
GridData: Here lives the data of grid. MainService comes here to get the data that needs, so we inject this to mainService and we use the loadRowData function to get the rowData data as you do, but in $interval you inject mainService inside gridData but gridData doesn’t need mainService to end its goal (get items from server).
I solve this problem using an observer design pattern, (using $rootScope). That means that I get notified when the data are arrived and mainService come and get them.
grid-data.service.js :
angular.module("gridApp").service("gridDataService",
["$injector", "$interval", "$timeout", "$rootScope",
function ($injector, $interval, $timeout, $rootScope) {
[…]
$interval(function () {
[..]
self.newItems = [rowToAdd];
// delete this code
// var gridMainService = $injector.get('gridMainService');
// gridMainService.gridOptions.api.addItems(newItems);
// notify the mainService that new data has come!
$rootScope.$broadcast('newGridItemAvailable');
grid-main.service.js:
angular.module("gridApp").service("gridMainService",
["$log", "$q", "gridConfigService", "gridDataService", '$rootScope',
function ($log, $q, gridConfigService, gridDataService, $rootScope) {
[..]
// new GridData data arrive go to GridData to get it!
$rootScope.$on('newGridItemAvailable', function(){
self.gridOptions.api.addItems(gridDataService.getNewItems());
})
[..]
When a real server is used, the most common solution is to use the promises (not observer pattern), like the loadRowData.
Problem 2:
gridSettingsService: This service change the settings of mainService so it needs mainService but mainService doesn’t care about gridSettings, when someone wants to change or learn mainService internal state (data, form) must communicate with mainService interface.
So, I delete grid Settings injection from gridMainService and only give an interface to put a callback function for when Grid is Ready.
grid-main.service.js:
angular.module("gridApp").service("gridMainService",
["$log", "$q", "gridConfigService", "gridDataService", '$rootScope',
function ($log, $q, gridConfigService, gridDataService, $rootScope) {
[…]
// You want a function to run onGridReady, put it here!
self.loadGridOptions = function (onGridReady) {
[..]
self.gridOptions = {
columnDefs: gridConfigService.columnDefs,
rowData: gridDataService.rowData,
enableSorting: true,
onGridReady: onGridReady // put callback here
};
return self.gridOptions;
});
[..]// I delete the onGridReady function , no place for outsiders
// If you want to change my state do it with the my interface
Ag-grid-controller.js:
gridMainService.loadGridOptions(gridSettingsService.applyDefaults).then(function () {
vm.gridOptions = gridMainService.gridOptions;
vm.showGrid = true;
});
here the full code: https://plnkr.co/edit/VRVANCXiyY8FjSfKzPna?p=preview
You can introduce a separate service that exposes specific calls, such as adding items to the grid. Both services will have a dependency to this api service, which allows for the data service to drop its dependency on the main service. This separate service will require your main service to register a callback that should be used when you want to add an item. The data service in turn will be able to make use of this callback.
angular.module("gridApp").service("gridApiService", function () {
var self = this;
var addItemCallbacks = [];
self.insertAddItemCallback = function (callback) {
addItemCallbacks.push(callback);
};
self.execAddItemCallback = function (item) {
addItemCallbacks.forEach(function(callback) {
callback(item);
});
};
});
In the above example, there are two functions made available from the service. The insert function will allow for you to register a callback from your main function, that can be used at a later time. The exec function will allow for your data service to make use of the stored callback, passing in the new item.
First of all, I agree with the other comments that the decoupling seems to be a bit extreme; nonetheless, I'm sure you know more than us how much is needed for you project.
I believe an acceptable solution is to use the pub/sub (observer pattern), in which the gridSettingsService and gridDataService services do not directly update the gridMainService, but instead raise a notification that gridMainService can hook into and use to update itself.
So using the plunker that you provided, the changes I would make would be:
Inject the $rootScope service into gridSettingsService, gridDataService and gridMainService.
From gridSettingsService and gridDataService, stop manually injecting the gridMainServiceand instead just $broadcast the notification with the associated data:
```
// in gridDataService
$rootScope.$broadcast('GridDataItemsReady', newItems);
// in gridSettingsService
$rootScope.$broadcast('SortModelChanged', sort);
```
In gridMainService, listed to the notifications and update using the passed in data:
```
// in gridMainService
$rootScope.$on('GridDataItemsReady', function(event, newItem) {
self.gridOptions.api.addItems(newItem);
});
$rootScope.$on('SortModelChanged', function(event, newSort) {
self.gridOptions.api.setSortModel(newSort);
});
```
And here is the updated plunker: https://plnkr.co/edit/pl8NBxU5gdqU8SupnMgy

Angularjs Best Practice for Data Store

My angular app have 2 controllers. My problem is that the controllers does not keep the data when the user navigates away from the page.
How can I store the selected data on of my controllers into a data store so it can be used between other controllers?
Option 1 - custom service
You can utilize a dedicated angular service to store and share data between controllers (services are single instance objects)
service definition
app.service('commonService', function ($http) {
var info;
return {
getInfo: getInfo,
setInfo: setInfo
};
// .................
function getInfo() {
return info;
}
function setInfo(value) {
info = value;
}
});
usage in multiple controllers
app.controller("HomeController", function ($scope, commonService) {
$scope.setInfo = function(value){
commonService.setInfo(value);
};
});
app.controller("MyController", function ($scope, commonService) {
$scope.info = commonService.getInfo();
});
Option 2 - html5 localStorage
You can use the built-in browser local storage and store your data from anywhere
writing
$window.localStorage['my-data'] = 'hello world';
reading
var data = $window.localStorage['my-data']
// ...
check out this awesome project:
https://github.com/grevory/angular-local-storage
Option 3 - via web server api
If you need to persist data among different users, you should save it somewhere in the server side (db / cache)
function getProfile() {
return $http.get(commonService.baseApi + '/api/profile/');
}
function updateProfile(data) {
var json = angular.toJson(data);
return $http.post(commonService.baseApi + '/api/profile/', json);
}
EDIT See Jossef Harush's answer where he has written an in-depth response that covers other methods including this one.
I'd recommend using either localStorage or sessionStorage - http://www.w3schools.com/html/html5_webstorage.asp.
HTML local storage provides two objects for storing data on the client:
window.localStorage - stores data with no expiration date
window.sessionStorage - stores data for one session (data is lost when the browser tab is closed)
This assumes that you don't want to POST/PUT the data to your web service (windows service mention in your question).
If you data is an array or some sort, you can convert it to JSON to store as a string and then when you need it you can parse it back as follows - How do I store an array in localStorage?:
var names = [];
names[0] = prompt("New member name?");
localStorage["names"] = JSON.stringify(names);
//...
var storedNames = JSON.parse(localStorage["names"]);
There is an option not mentioned in other answers (AFAIK).
EVENTS
You can use events for communication between controllers.
It's a straightforward communication that doesn't need a mediator
(like service) and can't be wiped by the user (like HTML storage).
All the code is written in controllers that you are trying to
communicate with and thus very transparent.
A good example how to leverage events to communicate between controllers can be seen below.
The publisher is the scope that wanna publish (in other words let others know something happened). Most don't care about what has happened and are not part of this story.
The subscriber is the one that cares that certain event has been published (in other words when it gets notified hey, this happened, it reacts).
We will use $rootScope as a mediator between publisher and a subscriber. This always works because whatever scope emits an event, $rootScope is a parent of that scope or parent of a parent of a parent.. When $rootScope broadcasts (tells everyone who inherits) about an event, everyone hears (since $rootScope is just that, the root of the scope inheritance tree) so every other scope in app is a child of it or child of a child of a child..
// publisher
angular.module('test', []).controller('CtrlPublish', ['$rootScope','$scope',
function ($rootScope, $scope) {
$scope.send = function() {
$rootScope.$broadcast('eventName', 'message');
};
}]);
// subscriber
angular.module('test').controller('ctrlSubscribe', ['$scope',
function ($scope) {
$scope.$on('eventName', function (event, arg) {
$scope.receiver = 'got your ' + arg;
});
}]);
Above we see two controllers communicating a message to each other using an event. The event has a name, it has to be unique, otherwise, a subscriber doesn't differentiate between events. The event parameter holds autogenerated but sometimes useful data, the message is the payload. In this example, it's a string but it can be any object. So simply put all the data you wish to communicate inside an object and send it via event.
NOTE:
You can avoid using root scope for this purpose (and limit the number of controllers that get notified of an event) in case two scopes are in direct inheritance line of each other. Further explanation below:
$rootScope.$emit only lets other $rootScope listeners catch it. This is good when you don't want every $scope to get it. Mostly a high level communication. Think of it as adults talking to each other in a room so the kids can't hear them.
$rootScope.$broadcast is a method that lets pretty much everything hear it. This would be the equivalent of parents yelling that dinner is ready so everyone in the house hears it.
$scope.$emit is when you want that $scope and all its parents and $rootScope to hear the event. This is a child whining to their parents at home (but not at a grocery store where other kids can hear). This is a shortcut to use when you wanna communicate from the publisher that is a child or n-th child of the subscriber.
$scope.$broadcast is for the $scope itself and its children. This is a child whispering to its stuffed animals so their parents can't hear.
EDIT: I thought plunker with a more elaborate example would be enough so I decided to keep is simple here. This elaborate explanation should be better.
To share data between two controllers on the same page, you can use factories/services. Take a look at Share data between AngularJS controllers for example.
However, if this is across page reloads/refreshes, you will need to store the data in local storage and then read it upon reloading. An example of that is here: How do I store data in local storage using Angularjs?
Checkout this library https://www.npmjs.com/package/angularjs-store
This can help you manage your application state much simpler as it will force you to have a one way data flow on your application.

Creating AngularJS object that calls method on app load

I am wondering if there is a convention within AngularJS for creating an object that lives within the app module, but is not attached directly to the view in any way, but is called when the view has loaded and the app starts up. In particular, I am trying to write an object that dispatches messages to listening controllers when they come in from the server.
Currently, I have implemented this by creating a "Controller" that attaches to the view. It has a monitor() function that is called when the page loads, and then listens in a loop for any incoming messages. I call the monitor() function from within the loaded view, by setting the ng-controller like so:
<div ng-controller="MyController">
{{ monitor() }}
</div>
This doesn't feel like the right thing to do. This "Controller" isn't interacting with the view in any way, so my gut tells me I am violating principles of AngularJS. But I haven't been able to turn up an easy solution that is endorsed by the AngularJS doc.
I am looking for a way to create an object that lives within the AngularJS world (in other words, it can use dependency injection to get access to services, and it can use $scope.$broadcast to send messages to other listening controllers), but that doesn't need to attach itself to the view in any way.
Ideally, I am looking for a way to say, "Here Angular, on startup, create this object, and run this method on it." Is there a way to do this?
You may use this as a starting point:
declaration of your object.
AngularJS: Service vs provider vs factory
myApp.factory('MessageBus', function() {
return {
listeners: [],
init: function() {
// do whatever you need at startup
},
pushMessage: function(msg) {
angular.forEach(this.listeners, function(listener) {
listener(msg);
});
},
subscribe: function(onMessageCallback) {
this.listeners.push(onMessageCallback);
}
};
});
calling a method on angular appilcation start
https://docs.angularjs.org/api/ng/type/angular.Module#run
myApp.run(function(MessageBus) {
MessageBus.init();
});
using this object within controllers
https://docs.angularjs.org/guide/di
myApp.controller('MessageCtrl', function($scope, MessageBus) {
$scope.messagesToShow = [];
MessageBus.subscribe(function(message) {
$scope.messagesToShow.push(message);
});
$scope.submitMessage = function(id, text) {
MessageBus.pushMessage({
type: 'TEXTMESSAGE',
id: id,
payload: text
});
};
});
Note that this is something to start with and nothing for any production code. For example the controller doesn't unsubscribe after being destroyed - if the page changes - and so you leak memory.
Don't use $broadcast-events for this
1: they are slow
2: if this MessageBus has a specific concern, than in should be an own object with a meaningfull name and api. Otherwise your $rootScope will be flooded with thousends of different events for different concerns when your application grows. A service is always easier to document and you have a clean dependency on that specific service. Only using events on the $rootScope hides this dependency from every developer reading and hopefully understanding your codebase,
Yeah you approach is really smelly. This function will be called every time a $apply/$digest invokes.
Maybe move the function into the run callback on the module.
var app = angular.module("YourApp", [//dependencies]);
app.run(function($YourUIService){
$YourUIService.monitor();
});
The run will be invoked, when your angularjs-module has loaded every dependency and is ready to run.
Didn't find the doc for this :/

Create a model based on a Database entity

I'm new to AngularJS and I would like to understand how to properly separate the model from the controller. Till now I've always worked with the models inside the controllers. For instance:
angular.module("app").controller("customerController", ["Customer", "$scope", "$routeParams",
function (Customer, $scope, $routeParams){
$scope.customer = Customer.find({ID:$routeParams.ID});
}]);
This function retrieves a customer from the database and exposes that customer to the view. But I would like to go further: for example I could have the necessity to ecapsulate something or create some useful functions to abstract from the row data contained in the database. Something like:
customer.getName = function(){
//return customer_name + customer_surname
};
customer.save = function(){
//save the customer in the database after some modifies
};
I want to create a model for the Customer and reuse that model in lots of controllers. Maybe I could then create a List for the customers with methods to retrieve all customers from the database or something else.
In conclusion I would like to have a model that reflects a database entity (like the customer above) with properties and methods to interact with. And maybe a factory that creates a Customer or a list of Customers. How can I achieve a task like this in AngularJS? I would like to receive some advices for this issue from you. A simple example will be very useful or a theoretical answer that helps me to undestand the right method to approch issues like these in Angular. Thanks and good luck with your work.
Angular JS enables you to have automatic view updates when a model change or an event occur.
TAHTS IT!
it does so by using $watches which are a kind of Global Scope java script objects and stay in primary memory through out the life cycle of the angular js web app.
1.Please consider the size of data before putting anything onto the $scope because each data object you attach to it does +1 to $watch. As you are reading from a database you might have 100+ rows with >4 columns and trust me it will eat up client side processing.Pls do consider the size of your dataset and read about angular related performance issues for huge data set
2.to have models for your database entity i would suggest having plain javascript classes i.e. dont put everything on $scope (it will avoid un necessay watches! ) http://www.phpied.com/3-ways-to-define-a-javascript-class/
3.You wish to fire up events when the user changes the values. For this best i would suggest that if you are using ng-repeat to render the data in your array then use $index to get the row number where the change was done and pass this in ng-click i.e. and use actionIdentifier to distinguish in the kinds of events you want
ng-click="someFunc($index,actionIdentifier)"
You need to create a factory/service to do do the job, check jsfiddle
html:
<div ng-app="users-app">
<h2>Users</h2>
<div ng-view ></div>
<script type="text/ng-template" id="list.html">
<p>Users: {{(user || {}).name || 'not created'}}</p>
<button ng-click='getUser()'>Get</button>
<button ng-click='saveUser(user)'>Save</button>
</script>
</div>
js:
angular.module('users-app', ['ngRoute'])
.factory('Users', function() {
function User (user) {
angular.extend(this, user);
}
User.prototype.save = function () {
alert("saved " + this.name);
}
return {
get: function() {
return new User({name:'newUser'});
}
}
})
.config(function($routeProvider) {
$routeProvider
.when('/', {controller:'ListCtrl',templateUrl:'list.html'});
})
.controller('ListCtrl', function($scope, Users) {
$scope.getUser = function() {
$scope.user = Users.get();
}
$scope.saveUser = function(u) {
u.save();
}
})
Hope that help,
Ron

AngularJS: Communication between directives

I'm writing a directive which creates an mp3/audio player. The issue is that you can have many audio players in one page. What I would like to do is when one is playing and you start an other, that the one currently playing pauses. How can I achieve this with angular directives?
Thanks in advance!
Make a service that each directive uses and hold the state in there.
Something like this:
angular.module('MyPlayer' [])
.factory('playerState', function() {
var players = [];
return {
registerPlayer: function(player) {
players.add(player);
},
unregisterPlayer: function(player) {
var i = players.indexOf(player);
(i>-1) && players.splice(i,1);
},
stopAllPlayers: function() {
for(var i=0;i<players.length;i++) {
players[i].stop();
}
}
}
})
.directive('player', function(playerState) {
return {
...
link: function(scope, elem, attr) {
var player = {
stop: function() {
/* logic to stop playing */
},
play = function(song) {
playerState.stopAllPlayers();
/* logic to start playing */
}
}
playerState.registerPlayer(player);
scope.$on("$destroy", function() {
playerState.unregister(player);
});
scope.play = player.play;
scope.stop = player.stop;
...
}
}
})
Just to make the answers complete, next to broadcasting events, and exposing a service, you can also use directive controllers. These controllers are set through the controller property of a directive definition object and are shared between directives that require the same controller. This means you can have one controller for all the media players, where you can implement the logic you mentioned. See the documentation on directives (search for controller:) for more information.
I would recommend the service approach if you think there will be more consumers of the logic, or the directive controller approach if only the directives consume the logic. I would advise against broadcasting events on the root scope because of the uncoupled and global nature of it. Just my two cents! HTH
How are your directives setup? Please provide some code.
This depends on the scope of your directives, I'm going to assume a child scope. To communicate between the directives, when a user clicked to start a player, I would call a $scope.$parent.$broadcast() - or $rootScope.$broadcast() if the directives are in different controllers or using isolated scopes, but then you need to inject $rootScope into your directive - to send an event to all child scopes. My directives would be watching for this event using $on and any players that were playing would stop. After this broadcast the player clicked would start.
$broadcast() and $on() scope documentation
You can also do $rootScope.$broadcast events like playerStarted. This event can be subscribed by all directives and they can react to this event by stopping themselves. The one thing that you need to do would be pass in the data about the player which is starting so that the new player does not stop itself as it too would subscribe to such event.

Categories

Resources