AngularJS: Best way to handle global variables ($rootScope or global service?) - angularjs

I'm torn between using $rootScope and a global service.
$rootScope feels dirty and comes with all the problem of global variables, so I wanted to use a global service but the Angular FAQ says:
Of course, global state sucks and you should use $rootScope sparingly,
like you would (hopefully) use with global variables in any language.
In particular, don't use it for code, only data. If you're tempted to
put a function on $rootScope, it's almost always better to put it in a
service that can be injected where it's needed, and more easily
tested.
Conversely, don't create a service whose only purpose in life is to
store and return bits of data.
...and that's what I want to use the service for.
For example, I have a navbar that when toggled on mobile screens should add a class to the body and html elements. I handle this with a directive and a controller.
NavbarController:
function NavbarController($rootScope) {
var vm = this;
vm.toggleNav = toggleNav;
$rootScope.navCollapsed = false;
function toggleNav() {
$rootScope.navCollapsed = !$rootScope.navCollapsed;
}
}
As you can see there, I'm using rootScope for the navCollapsed value and in my directive I do this:
function navOpen($rootScope) {
return {
restrict: 'A',
link: function(scope, element) {
scope.$watch(function () {
return $rootScope.navCollapsed;
}, function () {
if ($rootScope.navCollapsed)
element.addClass('nav-open');
else
element.removeClass('nav-open');
});
}
};
}
It works fine, but using $rootScope feels dirty. I would much rather do something like this:
NavbarController:
function NavbarController(globalService) {
var vm = this;
vm.toggleNav = toggleNav;
globalService.navCollapsed = false;
function toggleNav() {
globalService.navCollapsed = !globalService.navCollapsed;
}
}
And in the directive:
function navOpen(globalService) {
return {
restrict: 'A',
link: function(scope, element) {
scope.$watch(function () {
return globalService.navCollapsed;
}, function () {
if (globalService.navCollapsed)
element.addClass('nav-open');
else
element.removeClass('nav-open');
});
}
};
}
Which does the exact same thing except now the value is in a single location and can't be unintentionally altered elsewhere which is better, but according to AngularJS team that is bad practice because it's "bits of data".
What is the best way of going about this? I have other variables with similar functionality that need to be global (able to be accessed in any controller for manipulation), without having to use $rootScope.

Related

How to apply watcher on a variable without using $scope?

Is there any way to watch changed value of a variable without using $scope?. My manager told me that we may migrate our code to angular2 which do not have $scope so we have to write the code in the way so that when we migrate it to angular2 it needs minimal changes. So now i want to keep track changes of a variable which we do in angular like :
$scope.$watch('myVar', function() {
alert('hey, myVar has changed!');
});
Now i want this to something like
app.controller('auditCtrl',
function($scope, $localStorage) {
var adc = this;
angular.extend(this, {
$state: $state,
count: 0
}
//What i want is something like
abc.$watch('count', function() {
alert('hey, count has changed!');
});
})
No, thats not possible. But ask yourself, what is the variable you want to $watch for? If it is a form field, you can simply use ng-change.
If you can use es2015 Syntax, you can add Getters and Setters on the Controllers Prototype which then triggers an update Function.
Something like this:
let auditCtrl = function () {
const adc = self;
adc.counterItem = null;
// etc.
};
Object.defineProperty(auditCtrl.prototype,
"counterItem", {
get: function () {
return this.counterItem;
},
set: function (newValue) {
this.counterItem = newValue;
// Call method on update
this.onCounterItemChange(this.counterItem);
},
enumerable: true,
configurable: true
});
Or, in my opinion the best approach, use a component architecture for your application. With components, you can use some built-in lifecycle-hooks like $onInit, $onChanges etc. This way, you are also thinking in the angular2 way, since you might want to migrate.

Controllers and directives, precedence

I've been with Angularjs a few days and I'm struggling with a few aspects of it. I'll do my best to try and explain what the issue is, and I'd really appreciate any help anyone can give me about it.
My situation (simplified) is this:
I have a service which loads some info from a json and stores it in an object. It also have some functions to be used for other controllers to retrieve that information.
var particServices = angular.module('particServices', []);
particServices.service('particSrv', function() {
var data = {};
this.updateData = function(scope) {
data = // http call, saves in data
}
this.getName = function(code) {
return data.name;
}
});
I have an html page assisted by a controller, which uses a directive board (no params, really simple). This is the controller:
var bControllers = angular.module('bControllers', []);
bControllers.controller('bController', ['$scope', 'particSrv', function ($scope, particSrv) {
$scope.getName = function(code) {
return particSrv.getName(code);
};
particSrv.updateData($scope);
}]);
As you can see, the controller makes the call to initialize the object in the service. As this is a singleton, I understand once that info is loaded no other call needs to be make to updateData and that info is available to others using the getters in the service (getName in this case).
I have a really simple directive board (which I simplified here), which uses another directive bio.
angular.module('tsDirectives', [])
.directive('board', ['dataSrv', 'particSrv', function(dataSrv, particSrv) {
return {
restrict: 'E',
replace: true,
scope: true,
controller: function($scope) {
$scope.getName = function(code) {
return particSrv.getName(code);
};
dataSrv.updateData($scope, 'board', 'U');
},
templateUrl: '<div class="board"><div bio class="name" partic="getName(code)"/></div></div>'
};
}]);
And this is the bio directive:
angular.module('gDirectives', [])
.directive('bio', function() {
return {
scope: {
partic: '&'
},
controller: function($scope) {
$scope.name = $scope.partic({code: $scope.athid});
},
template: '<a ng-href="PROFILE.html">{{name}}</a>'
};
})
Now, what I expected is that in the bio directive the info retrieved from party was displayed, but apparently this directive is processed before the partic is initialized in the main controller.
I was under the impression that even though this information was still not loaded when the directive is processed, as soon as the service finishes and the info is ready, automagically it would appear in my directive, but that does not seem to work like that. I've been reading about $watch and $digest, but I fail to see why (and if) I would need to call them manually to fix this.
Any hint will be much appreciated. I could provide more technical details if needed.
Directive will initialise when app is loaded and user opens the page where that directive is, if you have some property that is set later (from api for example), it will update that property in directive but that directive will not be reinitialised ($scope.partic({code: $scope.athid}) wont be called).
If you want for directive to wait for initialisation you should use ng-if. Something like this:
<div data-directive-name data-some-property="someProperty" data-ng-if="someProperty"></div>
In this case directive will be initialised when (if) you have some value in $scope.someProperty. But this is not very good if you can have false values for someProperty.
In that case you would need to use some kind of loaded flag.
You have not included "particServices" as a dependency in other modules which use the services of "particServices". Your modules should look like:
var bControllers = angular.module('bControllers', ['particServices']);
angular.module('tsDirectives', ['particServices']);
angular.module('gDirectives', ['particServices']);

Angular form 1.3 tightly coupled

Does anyone of you think that the form in angular 1.3 is causing tightly coupling between the controller and the DOM? This is an anti pattern of Angular.
For example, when giving a form the name='formExample' attribute, to set it to dirty or invalid programmily, in the controller we have to do $scope.formExample.$setDirty()
This is a bad practice!
Waiting to hear your thoughts!
Example :
this.onSaveClicked = function () {
that.saveMessage = that.SAVING_IN_PROGRESS;
//Update project with changes
ProjectsBLL.update($scope.entities.project.Model, function (data) {
$scope.entities.project.Model = data;
$scope.configurationForm.$setPristine();
that.saveMessage = that.SAVING_FINISHED;
}, null)
}
It sounds like you don't want the following line in the controller
$scope.configurationForm.$setPristine();
To achieve this, one way is to:
Change ProjectsBLL.update to return a promise rather than accept a callback (how to do this is probably beyond the scope of this question). Then also return the derived promise from then from the function in the controller:
this.onSaveClicked = function () {
that.saveMessage = that.SAVING_IN_PROGRESS;
//Update project with changes
return ProjectsBLL.update($scope.entities.project.Model).then(function (data) {
$scope.entities.project.Model = data;
that.saveMessage = that.SAVING_FINISHED;
});
}
And then use a custom directive rather than ng-click to call the save function. There are probably a few ways of doing this, but one is to add one that takes a function as an attribute:
<button type="button" my-submit="onSaveClicked()">Save</button>
which adds a manual click listener, calls the passed function on click, and after its returned promise is resolved, calls $setPristine() on a requireed form:
app.directive('mySubmit', function($parse) {
return {
require: '^form',
link: function(scope, element, attrs, formController) {
var callback = $parse(attrs.mySubmit);
element.on('click', function() {
callback(scope, {}).then(function() {
formController.$setPristine();
});
});
}
};
});
A version of this, just using a $timeout to simulate a call to $http can be seen at http://plnkr.co/edit/IdcfQ4N9MhDKdvyorzeO?p=preview

Using both controller and require option inside angular directive

I'm looking for a way to access both controllers inside the directive.
Currently it's not working and I don't know why or if it's even possible.
If I use both require and controller option, inside the link function ctrl property refers to whatever I requested via the require option.
I can't think of a way to access the controller inside the link function when the require option is present.
It seems that these two properties are mutually exclusive ?
angular.module('moduleName').directive('directiveName', [function () {
return {
controller: 'MediaController',
require:'ngController',
link: function (scope, element, attributes, ctrl) {
// I need access to both controllers here
}
}
}]);
If you want both controllers, then require both controllers:
angular.module('moduleName').directive('directiveName', [function () {
return {
controller: MediaController,
require:['directiveName', 'ngController'],
In this case ctrl is an array containing the two controllers.
Without really knowing why you need to access both controllers, I can only offer minimal advice here. My suggestion would be to create a service to handle cross controller needs. Services are singletons and they support data binding. Services are my preference for cross controller work every day. For example:
App.controller('Ctrl1', function Ctrl1 ($scope, TestService) {
$scope.someValue = TestService.getValue();
});
App.controller('Ctrl2', function Ctrl2 ($scope, TestService) {
$scope.someValue = TestService.getValue();
});
App.factory('TestService', function() {
var myVal = "I Bound";
return {
getValue: function() {
return myVal;
}
}
});
This method allows you to abstract a controllers need to directly access another controller. Your services can be pulled into these directives or other services too. I hope this helps a bit.
Thanks,
Jordan

Best way to communicate between directive instances

I have two instances of a directive. Is there a way communicate between the two instances?
Or can I set a global variable that all instances will share the same value.
I have tried to store the value in a service. When the value in one instance change, then other instances will be manually updated. But I am not sure whether this is the best way to it or not.
Thanks.
The directive factory itself is a singleton. Anything you declare outside of the definition object will be global to all instances. As each instance has it's own scope, instance-specific data should go in the scope. So, something like this:
angular.module("myApp", [])
.directive("myDir", function() {
var myGlobal = 0;
return {
template: '<div>Global: {{getGlobal()}}, Local: {{local}} -- Increment</div>',
scope: {},
link: function(scope, element, attrs) {
scope.local = 0;
scope.increment = function() {
scope.local++;
myGlobal++;
}
scope.getGlobal = function() {
return myGlobal;
}
}
}
});
http://jsfiddle.net/7YwDS/

Resources