Angularjs: a factory to share data between controllers - javascript

I have an angular.js app with multiple views, and I need to preserve status among view change.
So I use a very simple factory to share data.
And, in the controllers, I use the factory name to use my data, instead of "$scope":
app.factory('share', function() {
return {};
}
app.controller('FirstCtrl', function ($scope, share) {
share.obj1['a'] = 'ABC';
});
app.controller('SecondCtrl', function ($scope, share) {
share.obj1['b'] = 'DEF';
if (share.obj1['a'] === 'ABC') {
...
}
});
And, my simplified html view is like this:
...
<div ng-repeat="(key, item) in obj1">
{{item}}
</div>
...
This of course doesn't work, because ng-* directives in views only have access to $scope...
Is it possible to keep my data in sync with share factory using $scope?

You can just do $scope.share = share at the end of each controller and of course modify the view:
<div ng-repeat="(key, item) in share.obj1">
Alternatively, and if the structure of share is NOT going to change (there is always an obj1), you could do angular.extend($scope, share). This is dangerous if share gets new properties, these will not be seen by the scope, until a new controller is created.

Related

What is the right way to send data between modules in AngularJS?

One of the great things about angular is that you can have independent Modules that you can reuse in different places. Say that you have a module to paint, order, and do a lot of things with lists. Say that this module will be used all around your application. And finally, say that you want to populate it in different ways. Here is an example:
angular.module('list', []).controller('listController', ListController);
var app = angular.module('myapp', ['list']).controller('appController', AppController);
function AppController() {
this.name = "Misae";
this.fetch = function() {
console.log("feching");
//change ListController list
//do something else
}
}
function ListController() {
this.list = [1, 2, 3];
this.revert = function() {
this.list.reverse();
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div class="app" ng-app="myapp" ng-controller="appController as App">
<div class="filters">
Name:
<input type="text" ng-model="App.name" />
<button ng-click="App.fetch()">Fetch</button>
</div>
<div class="list" ng-controller="listController as List">
<button ng-click="List.revert()">Revert</button>
<ul>
<li ng-repeat="item in List.list">{{item}}</li>
</ul>
</div>
</div>
Now, when you click on Fetch button, you'll send the name (and other filters and stuff) to an API using $http and so on. Then you get some data, including a list of items you want to paint. Then you want to send that list to the List module, to be painted.
It has to be this way because you'll be using the list module in diferent places and it will always paint a list and add some features like reordering and reversing it. While the filters and the API connection will change, your list behaviour will not, so there must be 2 different modules.
That said, what is the best way to send the data to the List module after fetching it? With a Service?
You should be using Angular components for this task.
You should create a module with a component that will be displaying lists and providing some actions that will modify the list and tell the parent to update the value.
var list = angular.module('listModule', []);
list.controller('listCtrl', function() {
this.reverse = function() {
this.items = [].concat(this.items).reverse();
this.onUpdate({ newValue: this.items });
};
});
list.component('list', {
bindings: {
items: '<',
onUpdate: '&'
},
controller: 'listCtrl',
template: '<button ng-click="$ctrl.reverse()">Revert</button><ul><li ng-repeat="item in $ctrl.items">{{ item }}</li></ul>'
});
This way when you click on "Revert" list component will reverse the array and execute the function provided in on-update attribute of HTML element.
Then you can simply set your app to be dependent on this module
var app = angular.module('app', ['listModule']);
and use list component
<list data-items="list" data-on-update="updateList(newValue)"></list>
You can see the rest of the code in the example
It is very simple. Please take a look at this small snippet. comments are added to highlight.
You can have a common module that contains all the data that needs to be shared across modules by two steps
adding module dependency
injecting corresponding provider in the respective module's controller
angular.module('commonAppData', []).factory('AppData',function(){
var a,b,c;
a=1;
return{
a:a,
b:b,
c:c
}
})
angular.module('list', ['commonAppData']).controller('listController', ListController);
var app = angular.module('myapp', ['list','commonAppData']).controller('appController', AppController);
function AppController(AppData) {
//assigning a variable
AppData.a=100;
this.name = "Misae";
this.fetch = function() {
console.log("feching");
//change ListController list
//do something else
}
}
function ListController(AppData) {
//Using the data sent by App Controller
this.variableA=AppData.a;
this.list = [1, 2, 3];
this.revert = function() {
this.list.reverse();
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div class="app" ng-app="myapp" ng-controller="appController as App">
<div class="filters">
Name:
<input type="text" ng-model="App.name" />
<button ng-click="App.fetch()">Fetch</button>
</div>
<div class="list" ng-controller="listController as List">
<b>Shared variable : {{List.variableA}}</b>
<br>
<button ng-click="List.revert()">Revert</button>
<ul>
<li ng-repeat="item in List.list">{{item}}</li>
</ul>
</div>
</div>
There are a few ways to handle objects acress multiple controllers. Here are two.
1. Using Angulars $rootScope
You can assign an object to $rootScopewhich will hold up all information. This object can be passed into every controller through Angulars dependency injection. Also you can listen up to changes on your object by watching it through $watch or $emit.
Using $rootScope is an easy way, but may lead to performance issues on larger applications.
2. Using services
Angular provides a possibility to share object through services. Instead of defining your object inside of your controller, you could also do that inside of a service. Doing so you could inject that service into any controller and use it's values across your application.
function AppController(listService) {
// reference to the injected data
}
function ListController(listService) {
// update data
}
There are many ways to pass data from one module to another module and many ppl have suggested different ways.
One of the finest way and cleaner approach is using a factory instead of polluting $rootScope or using $emit or $broadcast or $controller.If you want to know more about how to use all of this visit this blog on
Accessing functions of one controller from another in angular js
By simply inject the factory you have created in main module and add child modules as dependancy in main module, then inject factory in child modules to access the factory objects.
Here is a sample example on how to use factory for sharing data across the application.
Lets create a factory which can be used in entire application across all controllers to store data and access them.
Advantages with factory is you can create objects in it and intialise them any where in the controllers or we can set the defult values by intialising them in the factory itself.
Factory
app.factory('SharedData',['$http','$rootScope',function($http,$rootScope){
var SharedData = {}; // create factory object...
SharedData.appName ='My App';
return SharedData;
}]);
Service
app.service('Auth', ['$http', '$q', 'SharedData', function($http, $q,SharedData) {
this.getUser = function() {
return $http.get('user.json')
.then(function(response) {
this.user = response.data;
SharedData.userData = this.user; // inject in the service and create a object in factory ob ject to store user data..
return response.data;
}, function(error) {
return $q.reject(error.data);
});
};
}]);
Controller
var app = angular.module("app", []);
app.controller("testController", ["$scope",'SharedData','Auth',
function($scope,SharedData,Auth) {
$scope.user ={};
// do a service call via service and check the shared data which is factory object ...
var user = Auth.getUser().then(function(res){
console.log(SharedData);
$scope.user = SharedData.userData;// assigning to scope.
});
}]);
In HTML
<body ng-app='app'>
<div class="media-list" ng-controller="testController">
<pre> {{user | json}}</pre>
</div>
</body>
It depends on the circumstance. Is there a parent child relationship? Is the relationship unknown, or you simply want to avoid having to worry about it at all?
I think this post lays it out well (it always seems to be helpful):
http://mean.expert/2016/05/21/angular-2-component-communication/
AngularJS provides $on, $emit, and $broadcast services for event-based communication between controllers.
So, if we want to pass data from the inner controller (listController) to outer controller (appController) then we have to use $emit. It dispatches an event name upwards through the scope hierarchy and notify to the registered $rootScope.
Working Plunker : https://plnkr.co/edit/szf9jHvvOPLOvQc5sQI2?p=preview
This plunker is not as per the exact requirement as I don't know the API response but this sample plunker describe the problem statement.
I think you can use the publisher-subscriber pattern implemented in $rootScope.
In the ListController you have to inject $rootScope and after that you have to subscribe to an arbitrary called event that could have the pattern _data_received
$rootScope.$on('ingredients_data_received', function(ingredients) { prepare_recipe();});
so in your AppController you have to call $rootScope.$emit once the data of that list has been received
$rootScope.$emit('ingredients_data_received', ingredients);
This is just a way to pass data, you can also push those data or the promise in a $rootScope property but this is not a good practice, or you can create your own Service that manage the data for you (remember that you are working with a frontend framework so the controller has to control the view, the business logic has to be transfered to a service, not to a crontroller).
I like to use angular resource with a caching layer to persist data to multiple controllers using a singular method. This has a few benefits namely any controller has the same entry point to data. Keep in mind sometimes you need to fetch fresh data when navigating from one portion of the site to another so persisting http data is not always ideal.
'use strict';
(function() {
angular.module('customModule')
.service('BookService', BookService);
BookService.$inject = ['$resource'];
function BookService($resource) {
var BookResource = $resource('/books', {id: '#id'}, {
getBooks: {
method: 'GET',
cache: true
}
});
return {
getBooks: getBooks
};
function getBooks(params) {
if (!params) {
params = {};
}
return BookResource.getBooks(params).$promise;
}
}
})();
In any controller
BookService.getBooks().then(function(books) {
//books will be cached so invoking the call will return the same set of data anywhere
});
Services in angular are used to share functionality between components. Please try to keep them simple and with a single responsibility/purpose.
An idea would be creating a store service that will cache every response from the api in your application. Then you don't have to request it every time.
Hope it helps! ;)

Shared variables and multiple controllers AngularJS

I have multiple controllers on a small app I'm writing, and I have successfully shared a 'selected' variable between the controllers like so.
app.service('selectedEmployee', function () {
var selected = null;
return
{
getSelected: function() {
return selected;
},
postSelected: function(employee) {
selected = employee;
}
};
});
I have a side nav bar with a list of employees. When I click on an employee I call the postSelected function then the getSelected to set $scope.selected.
$scope.selectEmployee = function(employee) {
//Calling Service function postSelected
selectedEmployee.postSelected(employee);
$scope.selected = selectedEmployee.getSelected();
if ($mdSidenav('left').isOpen()) {
$mdSidenav('left').close();
}
}
I have a third controller for my main content area, and this is where I don't understand what to do. I want information from the selected employee to be displayed, but angular is compiling the whole page before the first employee has a chance to get set as selected, and subsequent selections of an employee aren't reloading the main content page (because I haven't told them to I think). Here's my main content controller:
app.controller('mainContentController', ['$scope','selectedEmployee',
function ($scope, selectedEmployee) {
$scope.selected = selectedEmployee.getSelected();
console.log($scope.selected);
}
]);
My main content view is very simple right now
<h2>{{selected.firstName}}{{selected.lastName}}</h2>
My question is how I can tell one controller to effectively update its partial view so that when I select an employee it displays information.
GitLab repo
Don't rely on messy broadcasts if your goal is simply to display & modify the data in the controller's template.
Your controllers do NOT need to "know" when the Service or Factory has updated in order to use it in the template as Angular will handle this for you, as you access the data via dot notation. This is the important concept which you should read more about.
This Fiddle shows both ways of accessing the data, and how using the container object in the template causes Angular to re-check the same actual object on changes - instead of the primitive string value stored in the controller:
http://jsfiddle.net/a01f39Lw/2/
Template:
<div ng-controller="Ctrl1 as c1">
<input ng-model="c1.Bands.favorite" placeholder="Favorite band?">
</div>
<div ng-controller="Ctrl2 as c2">
<input ng-model="c2.Bands.favorite" placeholder="Favorite band?">
</div>
JS:
var app = angular.module("app", []);
app.factory('Bands', function($http) {
return {
favorite: ''
};
});
app.controller('Ctrl1', function Ctrl1(Bands){
this.Bands = Bands;
});
app.controller('Ctrl2', function Ctrl2(Bands){
this.Bands = Bands;
});
First of all lets start by good practices, then solve your problem here...
Good Practices
At least by my knowledge, i dont intend to use services the way you do... you see, services are more like objects. so if i were to convert your service to the way i normally use it would produce the following:
app.service('selectedEmployee', [selectedEmployeeService])
function selectedEmployeeService(){
this.selected = null;
this.getSelected = function(){
return this.selected;
}
this.postSelected = function(emp){
this.selected = emp;
}
}
You see there i put the function seperately, and also made the service an actual object.. i would reccomend you format your controller function argument like this... If you want to disuss/see good practices go here. Anways enough about the good practices now to the real problem.
Solving the problem
Ok The Andrew actually figured this out!! The problem was:that he need to broadcast his message using $rootScope:
$rootScope.$broadcast('selected:updated', $scope.selected);
And then you have to check when $scope.selected is updated.. kinda like $scope.$watch...
$scope.$on('selected:updated', function(event, data) {
$scope.selected = data;
})
After that it autmoatically updates and works! Hope this helped!
PS: Did not know he anwsered already...
So after much research and a lot of really great help from Dsafds, I was able to use $rootScope.$broadcast to notify my partial view of a change to a variable.
If you broadcast from the rootScope it will reach every child controller and you don't have to set a $watch on the service variable.
$scope.selectEmployee = function(employee) {
selectedEmployee.postSelected(employee);
$scope.selected = selectedEmployee.getSelected();
$rootScope.$broadcast('selected:updated', $scope.selected);
if ($mdSidenav('left').isOpen()) {
$mdSidenav('left').close();
}
}
And in the controller of the main content area
function ($scope) {
$scope.$on('selected:updated', function(event, data) {
$scope.selected = data;
})
}
I don't think you have to pass the data directly, you could also just as easily call selectedEmployee.getSelected()
$rootScope also has to be included in the Parent controller and the broadcasting controller.

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

Instantiate custom angular controller

How to instantiate a custom controller from code and preserve scope inheritance. In other words I want to do something like this:
var controller = 'myCtrl';
var html = '<p>{{value}}</p>';
var validScope= $scope.$new({
value : 'Hello, custom controllers'
}); // Or something like this to get valid scopes inheritance
$(document.body).append(instantiate(controller, html, validScope));
So i need two answers: how to instantiate custom controller and how to do it like angular do.
UPD. I've tried do it this way:
$compile('<div ng-controller="myCtrl">'+html+'</div>')(validScope);
Controller was instantiated. But placeholded values was not binded.
I do not know the context of where you are trying to all this controller from but I am going to assume you are wither in another controller, a service, or a directive.
The code below will show how to create a controller from a service.
The example may cover more than what you would need to do but this is a pattern that will work.
Create an abstract controller, this sets the constructor parameters of the controller and insulates the rest of the dependencies.
module.factory('AbstractCtrl', ['dependencies...', function (dependencies...) {
var ctrl = function($scope) {
// Do controller setup.
};
return ctrl;
}]);
Now create a controller implementation based on the abstract
module.controller('CtrlImpl', ['$scope', 'AbstractCtrl', function ($scope, AbstractCtrl) {
// Initialize the parent controller and extend it.
var AbstractCtrlInstance = new AbstractCtrl($scope);
$.extend(this, AbstractCtrlInstance);
// … Additional extensions to create a mixin.
}]);
Now that you have a controller with a minimally defined constructor to create an instance of the controller you just need to call inject the $controller and do the following:
$controller('CtrlImpl', {$scope: $scope}));
I think that the best approach is to expose a function on the scope for retrieving your controller. (ngController can take a string or a function) Lets say you have different values which need different constructors... something vaguely like this:
<div ng-repeat="item in items">
<div ng-controller="controllerFor(item)">
// whatever
</div>
</div>
That controllerFor function will know how to do the mapping for you. Hopefully, you can avoid using $compile all together.

AngularJS: dynamically assign controller from ng-repeat

I'm trying to dynamically assign a controller for included template like so:
<section ng-repeat="panel in panels">
<div ng-include="'path/to/file.html'" ng-controller="{{panel}}"></div>
</section>
But Angular complains that {{panel}} is undefined.
I'm guessing that {{panel}} isn't defined yet (because I can echo out {{panel}} inside the template).
I've seen plenty of examples of people setting ng-controller equal to a variable like so: ng-controller="template.ctrlr". But, without creating a duplicate concurrant loop, I can't figure out how to have the value of {{panel}} available when ng-controller needs it.
P.S. I also tried setting ng-controller="{{panel}}" in my template (thinking it must have resolved by then), but no dice.
Your problem is that ng-controller should point to controller itself, not just string with controller's name.
So you might want to define $scope.sidepanels as array with pointers to controllers, something like this, maybe:
$scope.sidepanels = [Alerts, Subscriptions];
Here is the working example on js fiddle http://jsfiddle.net/ADukg/1559/
However, i find very weird all this situation when you might want to set up controllers in ngRepeat.
To dynamically set a controller in a template, it helps to have a reference to the constructor function associated to a controller. The constructor function for a controller is the function you pass in to the controller() method of Angular's module API.
Having this helps because if the string passed to the ngController directive is not the name of a registered controller, then ngController treats the string as an expression to be evaluated on the current scope. This scope expression needs to evaluate to a controller constructor.
For example, say Angular encounters the following in a template:
ng-controller="myController"
If no controller with the name myController is registered, then Angular will look at $scope.myController in the current containing controller. If this key exists in the scope and the corresponding value is a controller constructor, then the controller will be used.
This is mentioned in the ngController documentation in its description of the parameter value: "Name of a globally accessible constructor function or an expression that on the current scope evaluates to a constructor function." Code comments in the Angular source code spell this out in more detail here in src/ng/controller.js.
By default, Angular does not make it easy to access the constructor associated to a controller. This is because when you register a controller using the controller() method of Angular's module API, it hides the constructor you pass it in a private variable. You can see this here in the $ControllerProvider source code. (The controllers variable in this code is a variable private to $ControllerProvider.)
My solution to this issue is to create a generic helper service called registerController for registering controllers. This service exposes both the controller and the controller constructor when registering a controller. This allows the controller to be used both in the normal fashion and dynamically.
Here is code I wrote for a registerController service that does this:
var appServices = angular.module('app.services', []);
// Define a registerController service that creates a new controller
// in the usual way. In addition, the service registers the
// controller's constructor as a service. This allows the controller
// to be set dynamically within a template.
appServices.config(['$controllerProvider', '$injector', '$provide',
function ($controllerProvider, $injector, $provide) {
$provide.factory('registerController',
function registerControllerFactory() {
// Params:
// constructor: controller constructor function, optionally
// in the annotated array form.
return function registerController(name, constructor) {
// Register the controller constructor as a service.
$provide.factory(name + 'Factory', function () {
return constructor;
});
// Register the controller itself.
$controllerProvider.register(name, constructor);
};
});
}]);
Here is an example of using the service to register a controller:
appServices.run(['registerController',
function (registerController) {
registerController('testCtrl', ['$scope',
function testCtrl($scope) {
$scope.foo = 'bar';
}]);
}]);
The code above registers the controller under the name testCtrl, and it also exposes the controller's constructor as a service called testCtrlFactory.
Now you can use the controller in a template either in the usual fashion--
ng-controller="testCtrl"
or dynamically--
ng-controller="templateController"
For the latter to work, you must have the following in your current scope:
$scope.templateController = testCtrlFactory
I believe you're having this problem because you're defining your controllers like this (just like I'm used to do):
app.controller('ControllerX', function() {
// your controller implementation
});
If that's the case, you cannot simply use references to ControllerX because the controller implementation (or 'Class', if you want to call it that) is not on the global scope (instead it is stored on the application $controllerProvider).
I would suggest you to use templates instead of dynamically assign controller references (or even manually create them).
Controllers
var app = angular.module('app', []);
app.controller('Ctrl', function($scope, $controller) {
$scope.panels = [{template: 'panel1.html'}, {template: 'panel2.html'}];
});
app.controller("Panel1Ctrl", function($scope) {
$scope.id = 1;
});
app.controller("Panel2Ctrl", function($scope) {
$scope.id = 2;
});
Templates (mocks)
<!-- panel1.html -->
<script type="text/ng-template" id="panel1.html">
<div ng-controller="Panel1Ctrl">
Content of panel {{id}}
</div>
</script>
<!-- panel2.html -->
<script type="text/ng-template" id="panel2.html">
<div ng-controller="Panel2Ctrl">
Content of panel {{id}}
</div>
</script>
View
<div ng-controller="Ctrl">
<div ng-repeat="panel in panels">
<div ng-include src="panel.template"></div>
</div>
</div>
jsFiddle: http://jsfiddle.net/Xn4H8/
Another way is to not use ng-repeat, but a directive to compile them into existence.
HTML
<mysections></mysections>
Directive
angular.module('app.directives', [])
.directive('mysections', ['$compile', function(compile){
return {
restrict: 'E',
link: function(scope, element, attrs) {
for(var i=0; i<panels.length; i++) {
var template = '<section><div ng-include="path/to/file.html" ng-controller="'+panels[i]+'"></div></section>';
var cTemplate = compile(template)(scope);
element.append(cTemplate);
}
}
}
}]);
Ok I think the simplest solution here is to define the controller explicitly on the template of your file. Let's say u have an array:
$scope.widgets = [
{templateUrl: 'templates/widgets/aWidget.html'},
{templateUrl: 'templates/widgets/bWidget.html'},
];
Then on your html file:
<div ng-repeat="widget in widgets">
<div ng-include="widget.templateUrl"></div>
</div>
And the solution aWidget.html:
<div ng-controller="aWidgetCtrl">
aWidget
</div>
bWidget.html:
<div ng-controller="bWidgetCtrl">
bWidget
</div>
Simple as that! You just define the controller name in your template. Since you define the controllers as bmleite said:
app.controller('ControllerX', function() {
// your controller implementation
});
then this is the best workaround I could come up with. The only issue here is if u have like 50 controllers, u'll have to define them explicitly on each template, but I guess u had to do this anyway since you have an ng-repeat with controller set by hand.

Categories

Resources