I've got a potentially really dumb question, but how do I modify variables up in $rootScope in Angular? I've got a slide-in sidebar that I want to change the content on whenever someone clicks on a thumbnail, and I figured the easiest way to handle where the data in the sidebar comes from/the sidebar visibility would either be in global values, or in $rootScope. I'm trying to keep everything as simple as possible, but I just don't know how to handle modifying global variables.
My angular code surrounding this is:
app.run(function($rootScope) {
$rootScope.currentUrl = { value: 'visual/design/1/' };
$rootScope.detail_visible = { value: true };
});
app.controller('navController', ['$scope', '$rootScope',
function ($scope, $rootScope) {
$scope.isDetail = $rootScope.detail_visible.value;
$scope.url = $rootScope.currentUrl.value;
$scope.hide = function($rootScope) {
$rootScope.detail_visible.value = false;
};
}]);
and the connecting HTML is
<div id="detail_box" ng-class="{d_show: isDetail, d_hide: !isDetail}">
<div ng-include="url + 'detail.html'"></div>
</div>
In essence, I'm trying to make it so that when you click on a thumbnail, it changes the currentUrl value from 'visual/design/1/' to whatever they've clicked on (like, 'music/solo/2' or whatever) then changes the value of detail_visible to false, so that the classes on my sidebar switch and I get a nice little slide-in, with fresh content loaded via ng-include which I kind of love a thousand times more than I thought I would. I've been banging my head against this for about three hours now, breaking everything else on this app whenever I get the chance. What am I screwing up here? Alternatively, is there a better way of doing this?
My reason for using global variables is that I have multiple thumbnails in multiple controllers, and I want each one to be able to dynamically change the URL in my ng-include.
For your question, you change the $rootScope variable simple by referencing it with
$rootScope.detail_visible.value = newValue;
but you dont need to inject $rootScope to your function:
$scope.hide = function() { //without $rootScope
$rootScope.detail_visible.value = false;
};
But, I would suggest you to implement a service and not to pollute the rootscope for such task.
https://docs.angularjs.org/guide/services
Object properties of scopes are inherited -- in your controller, you should be able to modify $scope.detail_visible.value and see it affect the $rootScope. You still have to initialize it on the $rootScope in .run() though.
app.run(function($rootScope) {
$rootScope.currentUrl = { value: 'visual/design/1/' };
$rootScope.detail_visible = { value: true };
});
app.controller('navController', ['$scope', function ($scope, $rootScope) {
$scope.hide = function() { // don't need to pass an argument
$scope.detail_visible.value = false;
};
}]);
view:
<div id="detail_box" ng-class="{d_show: currentUrl.value, d_hide: !currentUrl.value}">
<div ng-include="currentUrl.value + 'detail.html'"></div>
</div>
Related
I have a factory that needs to listen for a broadcast event. I injected $scope into the factory so I could use $scope.$on. But as soon as I add $scope to the parameter list I get an injector error.
This works fine:
angular.module('MyWebApp.services')
.factory('ValidationMatrixFactory', ['$rootScope', function($rootScope) {
var ValidationMatrixFactory = {};
return ValidationMatrixFactory;
}]);
This throws an injector error:
angular.module('MyWebApp.services')
.factory('ValidationMatrixFactory', ['$scope', '$rootScope', function($scope, $rootScope) {
var ValidationMatrixFactory = {};
return ValidationMatrixFactory;
}]);
Why can't I inject $scope into a factory? And if I can't, do I have any way of listening for events other than using $rootScope?
Because $scope is used for connecting controllers to view, factories are not really meant to use $scope.
How ever you can broadcast to rootScope.
$rootScope.$on()
Even though you can't use $scope in services, you can use the service as a 'store'. I use the following approach inspired on AltJS / Redux while developing apps on ReactJS.
I have a Controller with a scope which the view is bound to. That controller has a $scope.state variable that gets its value from a Service which has this.state = {}. The service is the only component "allowed" (by you, the developer, this a rule we should follow ourselves) to touch the 'state'.
An example could make this point a bit more clear
(function () {
'use strict';
angular.module('app', ['app.accounts']);
// my module...
// it can be defined in a separate file like `app.accounts.module.js`
angular.module('app.accounts', []);
angular.module('app.accounts')
.service('AccountsSrv', [function () {
var self = this;
self.state = {
user: false
};
self.getAccountInfo = function(){
var userData = {name: 'John'}; // here you can get the user data from an endpoint
self.state.user = userData; // update the state once you got the data
};
}]);
// my controller, bound to the state of the service
// it can be defined in a separate file like `app.accounts.controller.js`
angular.module('app.accounts')
.controller('AccountsCtrl', ['$scope', 'AccountsSrv', function ($scope, AccountsSrv) {
$scope.state = AccountsSrv.state;
$scope.getAccountInfo = function(){
// ... do some logic here
// ... and then call the service which will
AccountsSrv.getAccountInfo();
}
}]);
})();
<script src="https://code.angularjs.org/1.3.15/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="AccountsCtrl">
Username: {{state.user.name ? state.user.name : 'user info not available yet. Click below...'}}<br/><br/>
Get account info
</div>
</div>
The benefit of this approach is you don't have to set $watch or $on on multiple places, or tediously call $scope.$apply(function(){ /* update state here */ }) every time you need to update the controller's state. Also, you can have multiple controllers talk to services, since the relationship between components and services is one controller can talk to one or many services, the decision is yours. This approach focus on keeping a single source of truth.
I've used this approach on large scale apps... it has worked like a charm.
I hope it helps clarify a bit about where to keep the state and how to update it.
I am trying to update the scope value on a view when the service data changes.
The problem is that, (a) if I update the data in a different controller, then (b) the value doesn't update on the view.
main.html
<!-- breadcrumb row -->
<div ng-controller="mainController">
<span ng-if="memberCompany !== null">{{ memberCompany }}</span>
</div>
<!-- / breadcrumb -->
main.js
// breadcrumb service
app.service('breadcrumb', function() {
// variables
var memberCompany = null;
return {
// get compnay
getCompany: function() {
return memberCompany;
},
// set company
setCompany: function(value) {
memberCompany = value;
}
}
});
// main controller
app.controller('MainController', ['$scope', 'breadcrumb', function($scope, breadcrumb) {
// get company to display in view
$scope.memberCompany = breadcrumb.getCompany();
}
]);
If I update the service value in a different controller, I would like to be able to display that updated value back on the index view so it's viable across the app
other controller
app.controller('otherController', ['$scope', 'breadcrumb', function($scope, breadcrumb) {
// update company
breadcrumb.setCompany('StackExchange');
// update the scope data in the view?
}]);
How can I display the updated value on the index view once it's changed?
You can use a $watch in your controller on that service:
$scope.$watch(function() {
return breadcrumb.getCompany()
}, function(newValue, oldValue) {
$scope.memberCompany = newValue;
});
If this is a site-wide necessity, I would definitely stay far, far away from adding a bunch of watches everywhere as they can be costly for development time as well as computer resources. I would first write the service accordingly:
angular.module('myModule').service('Breadcrumb', function() {
return {
company: null,
setCompany: function(company) {
this.company = company;
},
getCompany: function() {
return this.company;
}
};
});
Although in my opinion, the getters and setters are definitely very unnecessary ( you could just set by Breadcrumb.company = company and get by Breadcrumb.company). Moving on, you should assign the breadcrumb service to the root scope:
angular.module('myModule').run(function($rootScope, Breadcrumb) {
return $rootScope.breadcrumb = Breadcrumb;
});
Then at this point you can either inject this service into your controllers/directives and call upon it within any view in the application like so:
<span class="my-company-name" ng-bind="$root.breadcrumb.company.name" />
This saves you from having to call watchers on everything and allows for you to only inject the Breadcrumb service when you actually need it in the application logic. However, like I said at first, it really depends on how widely used this service is as you do not want to muddy up your $rootScope with arbitrary values. If you don't want to assign it to the $rootScope you could assign it in your controllers and directives:
angular.module('myModule').controller('ApplicationCtrl', function($scope, Breadcrumb) {
$scope.breadcrumb = Breadcrumb;
});
and access it from templates like this:
<span ng-bind="breadcrumb.company.whatever" />
Hope that helps!
I have an angularjs app with some controllers.
At some point, I need to access a function defined inside a controller, but the place where the function is gonna be called is not inside the angularjs APP.
I'll try to build a simple scenario:
app.controller('TaskController', function ($scope, $routeParams, $window, TaskEngine, PortalUtil) {
$scope.closeTask = function() {
$scope.openTask = false;
$scope.openedTaskUrl = undefined;
}
});
-- Some other place in my web app, outside of the angular APP.
<button onclick="closeTask();">
The "closeTask()" function is never accessible, because its out of scope.
I tried to define it in the window object
$window.closeTask = function() {
$scope.openTask = false;
$scope.openedTaskUrl = undefined;
}
});
Now, my function is visible, but the variables "openTask" and "openedTaskUrl" are not.
Is it possible to do what I want?
Basically, I have a controller, and I need to access one function anywhere, to control the behaviour of a menu.
Thanks!
I would not use $window for this. Instead, Angular has a nice feature which lets not Angular code to interact with it. Using Angular.element you can access data or functions defined in Angulars Scope.
var $scope = angular.element(document.querySelector('[ng-app=app]')).scope();
$scope.$apply(function() {
$scope.print("Hello world");
});
I am new using AngularJS, i am interesting about the fact that when we update a data, Angular automatically impacts the modifications everywhere the data is involving.
But unfortunately, i can't make it works.
The simple thing i am trying to do is to make a change on a controller B, and i want the changes to be achieve on the controller A, since the data is referering to the same Service.
The data is correctly impacting on the both controllers, but the DOM is not updating according to this modification, here is the test:
HTML
<body>
<div ng-controller="ACrtl">
<h1>{{is_logged}}</h1> <!-- Always false -->
<button ng-click="check()">Check</button> <!-- true/false -->
</div>
<div ng-controller="BCrtl">
<button ng-click="{{is_logged=!is_logged}}">Toggle throught the DOM</button> <!-- Doesn't change anything on the Javascript -->
<button ng-click="toggle()">Toggle throught the controller</button> <!-- Change the Javascript but doesn't impact the other controller's scope -->
</div>
</body>
JS
var app = angular.module('MyApp', []);
app.controller('ACrtl', function($scope, UserService) {
$scope.is_logged = UserService.is_logged;
$scope.check = function() {
console.log('is_logged='+UserService.is_logged); //The change is correctly made when changin is_logged on the controller B.
$scope.is_logged = UserService.is_logged;
};
});
app.controller('BCrtl', function($scope, UserService) {
$scope.is_logged = UserService.is_logged;
$scope.toggle = function() {
UserService.is_logged = !UserService.is_logged;
};
});
app.factory('UserService', function() {
var User = {
is_logged: false
};
return User;
});
I hope AngularJS is able to do this and it's something i am doing wrong in my code !
Here is a plunker
Primitive variables (like boolean) are passed by value in Javascript, and the variables $scope.is_logged are just copies of their values in the service. So, if the original service value is changed, then this won't affect any copies on the scopes.
A standard way or re-factoring this would be to share an object between the controllers, and not a primitive, so
app.factory('UserService', function() {
return {
status: {
is_logged: false
}
};
});
And then used in the controllers
$scope.status = UserService.status;
So the controller can change $scope.status.is_logged, and the changes will be seen in all the controllers.
You can see this at:
http://plnkr.co/edit/GLZmdsAnn3T5Xw80h4sV?p=preview
When you assign is_logged to the scope on each controller you are creating a new property on each controller, both of which are initialised to the value from UserService.
In your case what you can do is expose the service on the scope of each controller like so:
$scope.data = UserService
and in your view:
<h1>{{data.is_logged}}</h1>
Have a look at this answer and the links that it mentions.
I have the following service in my app:
uaInProgressApp.factory('uaProgressService',
function(uaApiInterface, $timeout, $rootScope){
var factory = {};
factory.taskResource = uaApiInterface.taskResource()
factory.taskList = [];
factory.cron = undefined;
factory.updateTaskList = function() {
factory.taskResource.query(function(data){
factory.taskList = data;
$rootScope.$digest
console.log(factory.taskList);
});
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.startCron = function () {
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.stopCron = function (){
$timeout.cancel(factory.cron);
}
return factory;
});
Then I use it in a controller like this:
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
$scope.taskList = uaProgressService.taskList;
});
}
);
So basically my service update factory.taskList every 5 seconds and I linked this factory.taskList to $scope.taskList. I then tried different methods like $apply, $digest but changes on factory.taskList are not reflected in my controller and view $scope.taskList.
It remains empty in my template. Do you know how I can propagate these changes ?
While using $watch may solve the problem, it is not the most efficient solution. You might want to change the way you are storing the data in the service.
The problem is that you are replacing the memory location that your taskList is associated to every time you assign it a new value while the scope is stuck pointing to the old location. You can see this happening in this plunk.
Take a heap snapshots with Chrome when you first load the plunk and, after you click the button, you will see that the memory location the scope points to is never updated while the list points to a different memory location.
You can easily fix this by having your service hold an object that contains the variable that may change (something like data:{task:[], x:[], z:[]}). In this case "data" should never be changed but any of its members may be changed whenever you need to. You then pass this data variable to the scope and, as long as you don't override it by trying to assign "data" to something else, whenever a field inside data changes the scope will know about it and will update correctly.
This plunk shows the same example running using the fix suggested above. No need to use any watchers in this situation and if it ever happens that something is not updated on the view you know that all you need to do is run a scope $apply to update the view.
This way you eliminate the need for watchers that frequently compare variables for changes and the ugly setup involved in cases when you need to watch many variables. The only issue with this approach is that on your view (html) you will have "data." prefixing everything where you used to just have the variable name.
Angular (unlike Ember and some other frameworks), does not provide special wrapped objects which semi-magically stay in sync. The objects you are manipulating are plain javascript objects and just like saying var a = b; does not link the variables a and b, saying $scope.taskList = uaProgressService.taskList does not link those two values.
For this kind of link-ing, angular provides $watch on $scope. You can watch the value of the uaProgressService.taskList and update the value on $scope when it changes:
$scope.$watch(function () { return uaProgressService.taskList }, function (newVal, oldVal) {
if (typeof newVal !== 'undefined') {
$scope.taskList = uaProgressService.taskList;
}
});
The first expression passed to the $watch function is executed on every $digest loop and the second argument is the function which is invoked with the new and the old value.
I'm not sure if thats help but what I am doing is bind the function to $scope.value. For example
angular
.module("testApp", [])
.service("myDataService", function(){
this.dataContainer = {
valA : "car",
valB : "bike"
}
})
.controller("testCtrl", [
"$scope",
"myDataService",
function($scope, myDataService){
$scope.data = function(){
return myDataService.dataContainer;
};
}]);
Then I just bind it in DOM as
<li ng-repeat="(key,value) in data() "></li>
This way you can avoid to using $watch in your code.
No $watch or etc. is required. You can simply define the following
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
});
$scope.getTaskList = function() {
return uaProgressService.taskList;
};
});
Because the function getTaskList belongs to $scope its return value will be evaluated (and updated) on every change of uaProgressService.taskList
Lightweight alternative is that during controller initialization you subscribe to a notifier pattern set up in the service.
Something like:
app.controller('YourCtrl'['yourSvc', function(yourSvc){
yourSvc.awaitUpdate('YourCtrl',function(){
$scope.someValue = yourSvc.someValue;
});
}]);
And the service has something like:
app.service('yourSvc', ['$http',function($http){
var self = this;
self.notificationSubscribers={};
self.awaitUpdate=function(key,callback){
self.notificationSubscribers[key]=callback;
};
self.notifySubscribers=function(){
angular.forEach(self.notificationSubscribers,
function(callback,key){
callback();
});
};
$http.get('someUrl').then(
function(response){
self.importantData=response.data;
self.notifySubscribers();
}
);
}]);
This can let you fine tune more carefully when your controllers refresh from a service.
Like Gabriel Piacenti said, no watches are needed if you wrap the changing data into an object.
BUT for updating the changed service data in the scope correctly, it is important that the scope value of the controller that uses the service data does not point directly to the changing data (field). Instead the scope value must point to the object that wraps the changing data.
The following code should explain this more clear. In my example i use an NLS Service for translating. The NLS Tokens are getting updated via http.
The Service:
app.factory('nlsService', ['$http', function($http) {
var data = {
get: {
ressources : "gdc.ressources",
maintenance : "gdc.mm.maintenance",
prewarning : "gdc.mobMaint.prewarning",
}
};
// ... asynchron change the data.get = ajaxResult.data...
return data;
}]);
Controller and scope expression
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.get.maintenance}}</span>
</div>
The above code works, but first i wanted to access my NLS Tokens directly (see the following snippet) and here the values did not become updated.
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService.get;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.maintenance}}</span>
</div>