I am trying to understand Angularjs behaviors.
I am building a web-app, and I want the CurrentUser's info be shared among all the app components. To do that, I have created a CurrentUserController bound to $rootScope. This controller is used by a user directive utilized in the body html element, so that it is globally accessible and it's created just one time.
app.controller('CurrentUserController', function ($rootScope)
{
// initialization
$rootScope.userCtrl = self; //<- MAKE IT GLOBAL
this.islogged=false;
this.name="";
var self = this;
// functions
this.isLogged = function()
{ return self.islogged; };
this.setLoggedIn = function(credentials)
{ self.islogged = true; };
this.setLoggedOut = function()
{ self.islogged = false; };
}
);
app.directive('currentUser', function() {
return {
controller:'CurrentUserController'
};
})
and then my html page
<html>
...
<body current-user>
...
</body>
</html>
However I read that Services should be used to share data between controllers, since they are singleton.
So my question is:
is my approach wrong, or it is equivalent as I utilized services?
Moreover, right now I can utilize the directive ng-switch calling $rootScope.userCtrl functions, like this:
<div id="nav-right-side" class="navbar-right" ng-switch on="userCtrl.isLogged()">
<div ng-switch-when="false">
<login-button></login-button>
</div>
<div ng-switch-when="true">
<loggedin-button></loggedin-button>
</div>
</div>
If I utilize services, would I still be able to do that?
Thank you
The $rootScope is indeed shared across all the app and it is also best to store models into services.
Why bother with services ?
Because of the $digest cycle. Each time a watched value is modified, the digest is triggered. In angular, by default the digest is a loop that goes down all your scope from the $rootScope down to its leafs. On each element, it has to get if the value has been modified or not to update the view accordingly. This is pretty expensive, and it is the cause of why angular can be slow on big applications. Keeping the scope as light as possible is how you can build complex apps in angular. That's why storing things is always better in services, you do not pollute the scope with data you could put somewhere else.
That being said, auth is peculiar because you want to access the same data from the view and services. You can store it in the $rootScope as Asta puts it but I do not think that is consistant with best practices. This is opinionated
What can be done is creating a service that will hold you model and share it through a controller to have access to it from both the view and the other services/models.
Session.js
function Session(){
var
self = this,
_islogged=false,
_name = '';
// functions
this.isLogged = function() {
return self.islogged;
};
this.setLoggedIn = function() {
self.islogged = true;
};
this.setLoggedOut = function() {
self.islogged = false; };
}
// GetUsername, setUsername ... Whatever you need
}
angular
.module('app')
.service('Session', Session);
rootController.js
function rootController(Session){
// share the Session Service with the $scope
// this.session is like $scope.session when using the controllerAS syntax.
this.session = Session;
}
angular
.module('app')
.controller('rootController', rootController);
I would suggest you take a look at these articles:
Techniques for Authentification in AngularJs Applications
Comprehensive 10 000 words tutorial in angular
Diving into controllerAs syntax
Your best to use a Service to share data as you mention. In your approach you've used a Controller in a way that its not really intended.
You can call your controller from your HTML by using ng-controller so something like the following should work. This would be useful for a Login view for example or a logout directive.
<div ng-controller="userCtrl">
<div id="nav-right-side" class="navbar-right" ng-switch on="isLogged()">
<div ng-switch-when="false">
<login-button></login-button>
</div>
<div ng-switch-when="true">
<loggedin-button></loggedin-button>
</div>
</div>
</div>
In order to have your session available globally for use elsewhere you can use a service which you can initialise from your app. The session data can be added to $rootScope which you can then reference from any view or controller.
Service
angular.module('app').service('session', function($rootScope) {
$rootScope.sessionData.loggedIn = true
// extra logic etc..
});
Main App
angular.run(session)
angular.module('app').run(function(session) {});
Then to reference the variable from your view
<div id="nav-right-side" class="navbar-right" ng-switch on="sessionData.isLoggedIn">
Note: its good practice to use an object with scope variables to help avoid issues with inheritance.
Related
So, I have a ng-repeated list of items as such.
<li><a ng-click="{{person.id}}">Name of Person</a></li>
I would like to create a service wherein, on click, I can collect that person.id and pass it to another controller in a different route.
This would normally be very simple by just using the url and route params, however, in this case it is important that the person.id not be exposed within the browser url.
-- More Context
Whether service or not, I am needing to extract a {{person.Id}} that is data available via an ng-repeat on a list page of persons.
On click, I move from a persons controller to a new route with a "person" controller. I need that "person" controller to be able to pull the {{Person.ID}} that was clicked on the previous route in order to look up that person in a DB.
Any help would be really great!
Services aren't meant to interact directly with DOM elements. DOM should interact with directives/controllers. Controller should interact with models.
This example below demonstrates sending data from controller 1 to myFactory and then controller 2 gets it the value from myFactory.
angular
.module('app', [])
.factory('myFactory', myFactory)
.controller('myCtrl1', myCtrl1)
.controller('myCtrl2', myCtrl2);
function myFactory() {
var fromSender = null;
return {
setSender: function(sender) {
fromSender = sender;
},
getSender: function() {
return fromSender;
}
};
}
function myCtrl1(myFactory) {
var vm = this;
vm.setSender = myFactory.setSender;
}
function myCtrl2(myFactory) {
var vm = this;
vm.getSender = myFactory.getSender;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.js"></script>
<div ng-app="app">
<div ng-controller="myCtrl1 as ctrl1">
Controller 1: <br>
<button ng-click="ctrl1.setSender('from controller 1')">Send to myFactory</button>
</div>
<hr>
<div ng-controller="myCtrl2 as ctrl2">
Controller 2: <br>
value from ctrl1 via myFactory: {{ctrl2.getSender()}}
</div>
</div>
All services in Angular are singletons. So if you inject personService or something like that, in multiple controllers, then those controllers will be using the exact same object. So if you set a value on that service, then the other controllers will be able to see it.
With more code and context, I'll be able to give a more specific example.
I'm trying to follow John Papa's guidelines, in which he explains how using this combined with controllerAs is preferable to $scope.
The problem is that I can't find an easy way to get a variable (user) defined in ParentController (vm.user) and use it, even transform it in a ChildController.
Code for illustration :
controllers.js
app.controller('ParentController', function() {
var vm = this;
vm.user = {firstName:"John", lastName:"doe"};
});
app.controller('ChildController', function() {
var vm = this;
/* How can I access 'vm.user' defined in ParentController
without using $scope as John Papa's suggests ? */
});
index.html
<div ng-controller="ParentController as parent">
<div ng-controller="ChildController as child">
</div>
I could just put everything in one big controller but I want to keep my code clean and readable.
Thanks!
You shouldn't access data from one controller to another, it's not a good practice. In order to share data between controllers, you should use a service.
Here you have JSBin with an example.
I have a constant sidebar in my index.html file that lists projects using ng-include. When a project is created, or updated etc.. I would like the sidebar to automatically update along with it. I'm not sure which part of my code to provide, as hopefully it's a fundamental question that's easy to answer, though the solution eludes me.
Edit: feel I'm almost there, but src doesn't seem to pick up the controller property:
<div class="col col-md-4" data-ng-controller="ProjectsController" data-ng-include src="'{{sidebarUrl}}'"></div>
In my projects controller:
// Update existing Project
$scope.update = function() {
var project = $scope.project ;
project.$update(function() {
$location.path('projects/' + project._id);
$scope.$broadcast('projectUpdated');
}, function(errorResponse) {
$scope.error = errorResponse.data.message;
});
};
$scope.sidebarUrl = 'modules/projects/views/list-projects.client.view.html';
$scope.$on("projectUpdated",function(event,args) {
$scope.sidebarUrl=null;
$scope.sidebarUrl= 'modules/projects/views/list-projects.client.view.html';
});
This is where services are your friend. You should start by encapsulating your CRUD operations into a service.
function MyCrudService($http, ...){ ... }
angular.module('my-app')
.service('myCrudService', MyCrudService);
Now, there are several ways you could implement the updating.
Use $rootScope and broadcast a message saying something has changed, and listen for that event in your sidebar controller (assuming you have one).
//Inside your service
function updateProject(proj){
//Update project
$rootScope.$broadcast('project-updated', proj);
}
//Inside your controller
function MySidebarController($scope){
$scope.$on('project-updated', function(){ ... });
}
Encapsulate the eventing logic inside your service to avoid using $rootScope. Just maintain your own list of callbacks and execute them.
//Inside your controller
function MySidebarController(myCrudService){
myCrudService.onProjectChanged(function(){ ... });
}
Expose the shared data on your service that can be databound to.
//Inside your controller
function MySidebarController($scope, myCrudService){
$scope.projects = myCrudService.projects;
}
Personally, I try to avoid $scope in my controllers, but using it for eventing is OK. Still, I might write some kind of directive that would allow me to execute an expression whenever an event fired in order to avoid it.
<my-event-binding event='project-updated' expression='sideBar.updateProjects()' />
Okay, so I had the same requirement(dynamically changing menu items in an included side panel) what I did was to use a controller in the ng-include template. The template would then fetch the relevant menu items from a service and update the controller. The view had an ng-repeat directive to show all the menu items (projects in your case).
<div ng-controller="ProjectsCtrl">
<ul>
<li ng-repeat="project in projects">
<a ng-href="project.url">
{{project.name}}
</a>
</li>
</ul>
</div>
The controller function could look something like:
function($scope, projectsSvc){
$scope.projects = [];
loadProjects();
$scope.$on("updatedProjects", loadProjects);
function loadProjects(){
projectsSvc.getProjects.success(function(projects){
$scope.projects = projects;
});
}
}
Projects are fetched from a service. When you update a project, broadcast an event that triggers a load of the projects again.
So after the new projects have been committed into the service backend, the sidebar will update accordingly.
Inline in AngularJs is there a way to check if something is an array?
I would have thought this to work:
<div ng-show="Array.isArray(textStuff[0][1])">Hi</div>
I have verified it is in fact an array. Is there something I am missing or another way?
You can put angular.isArray on the scope...
$scope.isArray = angular.isArray;
<div ng-show="isArray(textStuff[0][1])">Hi</div>
Fiddle
You can create global filters to use in your JS or HTML to check against object types. This way you don't pollute your $rootScope or $scopes to use it everywhere, unlike the accepted answer... Angular also has some built in utility functions that can check object types:
angular
.module("yourModule")
.filter("isArray", function() {
return function(input) {
return angular.isArray(input);
};
});
In HTML:
<div ng-show="{{ textStuff[0][1]) | isArray }}">Hi</div>
You may also inject the $filter service into your Controller to access the custom filter by name and compute the filtered results when your controller instance is instantiated (and also when your data changes). This prevents performance issues due to the view expression getting computed rapidly.
angular
.module("yourModule")
.controller("MyController", MyController);
MyController.$inject = ["$filter", "$scope"];
function MyController($filter, $scope) {
this.testStuff = []; // your data
this.filteredResult = $filter("isArray")(this.testStuff[0][1]);
// or if you need to watch for data changes
var vm = this;
$scope.$watchCollection(
function() { return vm.testStuff },
function(newTestStuff) {
vm.filteredResult = $filter("isArray")(newTestStuff[0][1]);
}
);
}
<div ng-controller="MyController as my">
<div ng-show="my.filterResult">Hi</div>
</div>
I would separate logic from the view. Add state in scope and then check it
$scope.showHi = angular.isArray(textStuff[0][1]);
In view
<div ng-show="showHi">Hi</div>
in my app I have a wrapper controller that handles some properties dynamically based on other-controllers within it. everything works like a charm if the other-controllers are present/static on load, but as soon as I'm trying to make them dynamic, they stop working.
It was my understanding that the $rootScope is available from everywhere within the app, is that not true?
my JS looks like this:
var webApp = angular.module("webApp",[]);
webApp.controller("ControllerA", function($scope, $rootScope){
$rootScope.cnt = 0;
$rootScope.cntPlusPlus = function(){
$rootScope.cnt++;
};
$rootScope.controllerBs = [];
var template = $(".controller-b").html();
$scope.addControllerB = function(){
$rootScope.controllerBs.push(template);
};
});
webApp.controller("ControllerB", function($scope, $rootScope){
$scope.cntPlusPlus = function(){
console.log("overwrite plus plus");
}
});
Full example: http://plnkr.co/edit/tAcv1F9y7t9q9XsQ1EFL?p=preview
I know that this would be probably better with directives, but is there any way to make it work with Controllers?
thanks for the help
Don't try to access the DOM from controller code. Never. It is very bad practice which breaks AngularJS conventions and eventually provides you with bad architecture. This also means you should not create any DOM elements manually from a controller.
Better to manipulate with the scope itself, not with its visual representation. You can add new models to scope on your button's click, which will be translated to new elements by ng-repeat directive, each with its own controller (remember controllers are instances, not singletons, so that they have separated life cycles).
You might want to make use of <script type="text/ng-template"> and ng-include here instead of hidden divs.
Try to avoid using $rootScope when possible - it is global state which can be dangerous.
It might look like this then (plunker):
HTML:
<div class="controller-a" ng-controller="ControllerA">
Controller A
<div>
<button ng-click="cntPlusPlus()">cnt++</button> CNT: {{cnt}}
</div>
<button ng-click="addB()">Add B</button>
<div ng-repeat="B in Bs">
<div ng-include="'b-template'"></div>
</div>
</div>
<script type="text/ng-template" id="b-template">
<div ng-controller="ControllerB">this is controller b: <button ng-click="cntPlusPlus()">cnt++</button></div>
</script>
JS:
var webApp = angular.module("webApp",[]);
webApp.controller("ControllerA", function($scope){
$scope.cnt = 0;
$scope.cntPlusPlus = function(){
$scope.cnt++;
};
$scope.Bs = [];
$scope.addB = function(){
$scope.Bs.push({});
};
});
webApp.controller("ControllerB", function($scope){
$scope.cntPlusPlus = function(){
console.log("overwrite plus plus");
$scope.$parent.$parent.$parent.cnt++; //should be moved to service
}
});
</script>