angularjs: controller hijacking another controller's variable - angularjs

I am facing a very strange issue with variable in one controller being hijacked by another controller. Here are the details:
In my HTML I have two ng-view tags. Each tag leads to a templateURL (an html) that has its own corresponding controller. Ctrl1 and Ctrl2
the two ng-views are at the same level in the html hierarchy - that is, one is NOT the child of another
Controller1 looks like this:
ngEpMod.controller('Ctrl1',[function() {
selfX = this;
abc = 'abc controller1';
console.log(abc); // break point 1
selfX.query = function() {
console.log("abc=");
console.log(abc); // break point 2
console.log("search query=");
console.log(selfX.searchQ);
lySP.searchHomes();
};
}]);
Controller2 looks like this:
ngEpMod.controller('Ctrl2',[function() {
self = this;
abc = 'abc controller2';
}]);
Both controllers are associated in the html using a "controller as" syntax.
The query() method in Ctrl1 is fired when user user clicks a button (ng-click)
Mystery: As I load the html page ($state) that has the two ng-views, I am observing the browser console. I note that abc value at break-point1 is "abc controller1", but when the query() method is fired, it mysteriously changes to "abc controller2". There is no global variable by that name! As I understand, when the page is being laid out, Ctrl1 is created first so at break-point 1 abc has the correct value, then Ctrl2 is created and somehow it high-jacks the abc variable! Stranger even is that I noticed this problem first with my self variable (self = this) and then I introduced abc just for additional check
Gurus, I am a newbie and would really appreciate your help.

By creating a variable without var (or let in ES6), you've created a global variable attached to window:
abc = 'abc controller1'; equals to window.abc = 'abc controller1';
When the 1st controller instantiates it declares the variable abc on window. When the 2nd controller instantiates, it changes the global abc variable content.
To avoid it in this case define var abc in both controllers.
To avoid it in the future add 'use strict'; to each function deceleration, for example:
ngEpMod.controller('Ctrl2',[function() {
'use strict';
self = this;
var abc = 'abc controller2';
}]);
Strict mode will throw error when you make this mistake (any many others). From MDN:
First, strict mode makes it impossible to accidentally create global
variables. In normal JavaScript mistyping a variable in an assignment
creates a new property on the global object and continues to "work"
(although future failure is possible: likely, in modern JavaScript).
Assignments which would accidentally create global variables instead
throw in strict mode:

I would drop the below code into your app above this instantiation (most modern browsers should understand this syntax for debugging). Call window.trackCtrl in every controller constructor and then pop open the console in chrome or firefox and type printCtrls() and you should get a print out of when they were created in order.
window.trackCtrl = (name) => {
var newCtrl = {}
newCtrl.name = name + '_' + performance.now()
window.trackingCtrls = window.trackingCtrls || []
window.trackingCtrls.push(newCtrl)
}
window.printCtrls = () => Array.isArray(window.trackCtrls) ? window.trackingCtrls.forEach(x => console.info(x)) : console.error('trackCtrls not defined')
This will find bugs such as the controllers getting loaded out of order or duplicate copies of your code or libraries getting loaded on the same page. Performance API helps a lot in these situations => https://developer.mozilla.org/en-US/docs/Web/API/Performance/now

Related

angular ionic fails to update for some variables

In my Ionic / Angular framework I update 2 variables inside a service (an $http.get().then() block):
for( var di = day; di <= endOfMonthDate; di++) {
var flavor = days[di - 1];
daysLeftCalendar.push( flavor[1]); // dates right away !
}
var todaysFlavorIndex = -1;
for (var i = 0; i < days.length; i++ ) {
if ((days[i])[0] == day) {
todaysFlavorIndex = (days[i])[1];
todaysFlavorName = flavors[todaysFlavorIndex]; // only updates if you change tabs
}
}
Then I have these accessor methods in my service that get called by my controller:
return {
// both of these are hit after switching to one of the two tabs which both reference these functions
remainingFlavorIndexes: function() {
return daysLeftCalendar
},
getTodaysFlavorName: function() {
return todaysFlavorName
}
};
Then in my only controller I expose these variables like this:
$scope.remainingFlavorIndexes = Calendar.remainingFlavorIndexes(); // this one copies over right away !!
$scope.todaysFlavorName = Calendar.getTodaysFlavorName(); // this one doesn't
Then in my view:
<div> <!-- this one shows up right away -->
{{remainingFlavorIndexes}}
</div>
<div> <!-- these two only show up after switching tabs and returning -->
<img class="scaled-image" src="img/{{todaysFlavorName[2]}}">
</div>
<div style="text-align: center;">
{{todaysFlavorName[1]}}
</div>
How is it that I'm handling these 2 variables exactly the same, but todaysFlavorName is empty (even after the .then call returns)?
And why is it that when I switch tabs and come back they are populated?
Edit:
What is supposed to go into remaining flavor indexes is something like this:
[21,20,13,0,27,12,9,18,1,3,30,29,25,7,6,4,9,18,21,13]
And it works every time.
What is supposed to go into todaysFlavorName is:
[21, "peanut butter", "peanut_butter.jpg", "some meaningless text here"]
And it works only after I switch tabs.
There is a big difference between both. In the case of daysLeftCalendar:
the service has an array
the controller calls the service function that returns a reference to that array
the http callback function pushes elements to this array
So, the controller has a reference to the array that is in the service. Whenever the array is modified in the service, it's also modified in the controller, since the controller and the service both share a reference to the same array.
In the case of todaysFlavorName:
the service defines a variable todaysFlavorName referencing an array
the controller calls the service function that returns a reference to that array
the http callback function doesn't modify this array. It assigns a new array to the variable todaysFlavorName.
So, in the end, the controller has a reference to the original array, whereas the service has a reference to the new array. Which explains why nothing changes in the view: the controller still references the old array.
When you change tab, I assume the controller is reinstantiated, the service function is called again, and the controller gets back the new value from the service.
The fix is quite easy: always get the value to display from the service, instead of caching the value in the controller. Replace
$scope.todaysFlavorName = Calendar.getTodaysFlavorName();
by
$scope.todaysFlavorName = function() {
return Calendar.getTodaysFlavorName();
};
and
{{todaysFlavorName[1]}}
by
{{todaysFlavorName()[1]}}

How to prevent a directive from binding to elements within a controllers scope in Angular?

I have an Angular app, MyApp, that depends on external modules (two different map solutions), and I need them both but in different controllers (different modules within MyApp even).
The problem is the two modules both have directives that bind to the same argument ('center' in this case), which causes them both do manipulate a single element. What I want is for one directive to be active inside one controller and the other directive to be active inside another controller - so not have them inpact my elements at the same time.
I don't want to change the code of the external modules to achive this.
I found this to be a very interesting question. The answer below is incomplete, and, frankly, a bit hackish, but it demonstrates a way to rename a directive in another module without modifying the source of the module itself. There is a lot of work to do to make this anywhere near production ready and it absolutely can be improved.
The caveats to the solution are that once a directive is renamed, the "old" name will no longer work. It also depends on some angular conventions that might be changed with future versions, etc, so it's not future proof. It also might fail for complex directives, and I haven't really done any testing on it.
However, it demonstrates that it can be done, and the concept might lead to a feature angular needs (the ability to namespace external modules in order to prevent conflicts such as the one your are experiencing).
I think that if your use case is fairly simple, this will solve your problem, but I wouldn't recommend using it in the general case yet.
(function () {
var util = angular.module('util', [], function ($compileProvider) {
util.$compileProvider = $compileProvider
})
.factory('$directiveRename', function () {
var noop = function () { return function () { }; };
return function (module, directive, newDirective) {
var injector = angular.injector(['ng', module]);
var directive = injector.get(directive + 'Directive');
if(directive)
{
//there can be multiple directives with the same name but different priority, etc. This is an area where this could definitely be improved. Will only work with simple directives.
var renamedDirective = angular.copy(directive[0]);
delete renamedDirective['name'];
util.$compileProvider.directive(newDirective, function () {
return renamedDirective;
});
}
//noop the old directive
//http: //stackoverflow.com/questions/16734506/remove-a-directive-from-module-in-angular
angular.module(module).factory(directive + 'Directive', noop);
};
});
})();
Example usage:
angular.module('app', ['module1', 'module2', 'util'])
.run(function ($directiveRename) {
$directiveRename('module1', 'test', 'testa');
$directiveRename('module2', 'test', 'testb');
});
An alternative, slightly less hackish answer.
Add the following immediately after the script tag that includes angular (before any other modules are loaded)
<script type="text/javascript">
var angularModule = angular.bind(angular, angular.module);
angular.module = function (moduleName, requires, configFn) {
var instance = angularModule(moduleName, requires, configFn);
var directive = instance.directive;
instance.directive = function (directiveName, directiveFactory) {
//You can rename your directive here based on the module and directive name. I don't know the module and directive names for your particular problem. This obviously could be generalized.
if (instance.name == 'module1' && directiveName == 'test') {
directiveName = 'testa';
}
if (instance.name == 'module2' && directiveName == 'test') {
directiveName = 'testb';
}
return directive(directiveName, directiveFactory);
}
return instance;
};
</script>
This works by intercepting calls to module.directive and allowing you the opportunity to rename the directive before it is created.

populate a form with a bookmarklet doesn't work in angularjs

I have a bookmarklet that just does some simple form input population but the angular form is still in an invalid state. Like nothing was ever changed.
I tried calling el.onchange() but that doesn't seem to do anything.
javascript:populate();
function populate(){
var name = document.querySelector('input[name=name]');
name.value = 'Fred';
name.click();
name.onchange();
}
Since I already have jQuery loaded, I was able to fix the issue by triggering the input event.
$('input[ng-model]').trigger('input');
I used something similar to the code below to create my bookmarklet.
Couple of things to consider.
Write your function (see example below)
Before creating your bookmarklet, test your function in the chrome console
Function tested, now remove the endlines (e.g. "replace all" in intellij (with regex enabled), replace "\n" with ""
create a new bookmark in the browser
edit the url, the url should begin with "javascript:", paste your one line url after that... you'll end up with something like "javascript:(function(){..." etc...
//nb: This function is not written to work in this page
(function () {
//get the scope object for your controller
var sc=angular.element("[ng-controller=myController]").scope();
//grab your model
var m=sc.mymodel;
//some 'dodgy' date strings :)
var today=new Date();
var todayAsString=today.toLocaleDateString("en-GB");
today.setDate(today.getDate()-183);
var sixMonthsAgoStr=today.toLocaleDateString("en-GB");
today.setDate(today.getDate()+365);
var sixMonthsLaterStr=today.toLocaleDateString("en-GB");
//pick a random oz state
var states=["WA", "VIC","SA", "NT","NSW", "ACT", "QLD"];
var ozState=states[Math.floor(Math.random()*(6)+1)];
//update the model with some semi random values
m.actionDate=todayAsString;
m.ausstralianState=ozState;
m.streetNum=Math.floor((Math.random() * 1000) + 1);
m.ID="CR" + Math.floor((Math.random() * 10000) + 1);
m.manufactureYear="" + Math.floor(Math.random()*(2016-1960+1)+1960); //approx... between 1960 and 2016...
m.endDate=sixMonthsLaterStr;
m.startDate=sixMonthsAgoStr;
//MUST call $scope.apply() at the end.
sc.$apply();
})();

Is it good practice to combine CREATE and EDIT controllers in AngularJS?

There are many duplicated code among CREATE and EDIT controllers.
These controllers could be combined into one for minimizing repetitive code.
The problem: I need to distinguish which method to use on form submitting - create() or edit() for example.
The solution: I could add $scope.mode for example and set $scope.mode='edit' if user clicked 'EDIT' button or set $scope.mode='add' if user clicked 'ADD' button.
I could use services for minimizing repetitive code, but there still will be duplicated code. For example in both controllers I have cancel() method which clears the form and hide it. I could store clearForm() and hideForm() in the service, but this code will be duplicated in both controllers:
$scope.cancel = function() {
Service.clearForm();
Service.hideForm();
};
Questions:
Is it good practice to combine CREATE and EDIT controllers in AngularJS?
Is there any good practices to minimize repetitive code?
Yes. Use 1 controller.
Here is the reason why use 1 controller
The job of the controller is to support the View. Your create view and the edit view is exactly same - just that one has data pre-populated (edit) and another does not (create).
Moreover the "purpose" of this View is to have the user change or enter new values in the form. Your only difference should be something like reset(). But even there you could start with an empty model object e.g. $scope.entity = {} in case of CREATE and you will start with $scope.entity = $http.get().
Repetition Problem with 2 Controllers
With 2 different controllers and services you are going to incur at least the following duplication:
$scope.cancel = function() {
Service.cancel();
};
$scope.validate = function() {
ValidtionSvc.validate();
}
.
.
.//other stuff similar
but the problem is why even this duplication like you stated.
(UDATED here onwards since above was the answer to the 1st question)
How to use 1 controller with repetition ?
Is there any good practices to minimize repetitive code?
Question redefined: Is there a good practice of eliminating repetitive code in CREATE and EDIT forms ?
No formal 'best practice' exist to my knowledge to avoid repetitive code in this specific situation. However I am advising against mode=edit/create. The reason being for controllers in this situation there should be almost no difference since their job is to purely to fetch/update the model as the user interacts.
Here are the difference you will encounter in this situation and how you can avoid if/then/else with mode=create/edit:
1) Populating the form with existing values vs. empty form for Create.
To fetch a existing entities you need some key/query data. If such key data is present you could do
var masterEntity = {};
if(keyData) {
masterEntity = MyEntityResourceFactory.getEntity(keyData);
}
$scope.entity = masterEntity;//for Create this would be {}
2) reset() form
should be simply
$scope.reset = function() {
$scope.entity = masterEntity;
}
3) Update/Create
$http.post()//should not be different in today's world since we are treating PUT as POST
4) Validation - this is a perfect reuse - there should be no differences.
5) Initial / Default Values
You can use masterEntity = Defaults instead of {}.
Is it good practice to combine CREATE and EDIT controllers in
AngularJS?
In my experience, yes it is a good idea for 99.9% of the time. I typically inject a formType variable into my controller via the $routeProvider resolve feature. So I would have something like the following:
$routeProvider
.when('/item/create', {
templateUrl: '/app/item/itemForm.html',
controller: 'itemFormController',
resolve: {
item: ['$route', 'itemRepository', function ($route, itemRepository) {
return itemRepository.getNew();
}],
formType: function () { return Enums.FormType.CREATE; }
},
})
.when('/item/edit/:itemId', {
templateUrl: '/app/item/itemForm.html',
controller: 'itemFormController',
resolve: {
item: ['$route', 'itemRepository', function ($route, itemRepository) {
return itemRepository.get($route.current.params.itemId);
}],
formType: function () { return Enums.FormType.EDIT; },
},
});
That way you get your entity and type of form action injected into the controller. I also share the same templates, so saving a form I can either rely on my repository/service to determine what REST endpoint to call, or I can do a simple check inside the controller depending on what formType was injected.
Is there any good practices to minimize repetitive code?
Some of the things I'm using to keep things DRY:
If you keep a common convention on your server API you can go a very long way with a base factory/repository/class (whatever you want to call it) for data access. For instance:
GET -> /{resource}?listQueryString // Return resource list
GET -> /{resource}/{id} // Return single resource
GET -> /{resource}/{id}/{resource}view // Return display representation of resource
PUT -> /{resource}/{id} // Update existing resource
POST -> /{resource}/ // Create new resource
etc.
We then use a AngularJs factory that returns a base repository class, lets call it abstractRepository. Then for each resource I create a concrete repository for that specific resource that prototypically inherits from abstractRepository, so I inherit all the shared/base features from abstractRepository and define any resource specific features to the concrete repository. This way the vast majority of data access code can be defined in the abstractRepository. Here's an example using Restangular:
abstractRepository
app.factory('abstractRepository', [function () {
function abstractRepository(restangular, route) {
this.restangular = restangular;
this.route = route;
}
abstractRepository.prototype = {
getList: function (params) {
return this.restangular.all(this.route).getList(params);
},
get: function (id) {
return this.restangular.one(this.route, id).get();
},
getView: function (id) {
return this.restangular.one(this.route, id).one(this.route + 'view').get();
},
update: function (updatedResource) {
return updatedResource.put();
},
create: function (newResource) {
return this.restangular.all(this.route).post(newResource);
}
// etc.
};
abstractRepository.extend = function (repository) {
repository.prototype = Object.create(abstractRepository.prototype);
repository.prototype.constructor = repository;
};
return abstractRepository;
}]);
Concrete repository, let's use customer as an example:
app.factory('customerRepository', ['Restangular', 'abstractRepository', function (restangular, abstractRepository) {
function customerRepository() {
abstractRepository.call(this, restangular, 'customers');
}
abstractRepository.extend(customerRepository);
return new customerRepository();
}]);
What you'll find if you use this base repository pattern is that most of your CRUD controllers will also share a lot of common code, so I typically create a base CRUD controller that my controllers inherit from. Some people dont like the idea of a base controller, but in our case it has served as well.
The answer to your first question probably depends on the specific circumstances.
If the two controllers share a substantial amount of operations, and the behavior of just one or two functions needs to be altered - why not! Maybe not the most elegant solution but hey, whatever works.
If the behavior of many or all controller operations is going to depend on '$scope.mode'...I'd say be careful. That seems like a dangerous path.
Angular services have always served me well when it comes to minimizing code replication between controllers. If there is a "good practice to minimizing repetitive code," I would say it would be services. They are global to your app and can be injected into multiple controllers without issue.
I hope that helps!

setting $rootScope but value then not visible from another controller

My application requires candidates to be set up with an agency and nationality (among other things). I want to set the list of agencies and nationalities in global variables so that they can be accessed by the profile screen rather than retrieved from the database every time (as they are currently). Thanks to the other questions here I've got so far... but a crucial puzzle piece is clearly missing. The data is being retrieved from the DB but then isn't visible elsewhere.
homecontroller.js which loads up the data...
//Home page Controller
function HomeCtrl($scope, $rootScope, SessionTimeoutService, GetAllAgencies, GetNationalityList){
GetAllAgencies.getData({}, function(agencieslist, $rootScope) {
SessionTimeoutService.checkIfValidLogin(agencieslist);
$rootScope.agencieslistglobal = agencieslist.data;
});
/* I tried hard-coding values here - that worked & was passed thro' ok
$rootScope.nationalitieslistglobal = [
{'nationality_id' : 0, 'name' : 'Unknown'},
{'nationality_id' : 1, 'name' : 'Known'},
{'nationality_id' : 2, 'name' : 'Pants'},
]; */
GetNationalityList.getData({}, function(nationalitieslist, $rootScope) {
SessionTimeoutService.checkIfValidLogin(nationalitieslist);
$rootScope.nationalitieslistglobal = nationalitieslist.data;
});
alert($rootScope.nationalitieslistglobal[8].name);
}
The alert doesn't fire at all.
From the candidatescontroller.js:
function CandidatesAddCtrl($scope, CandidateModel, GetNationalityList, SessionTimeoutService, $http, GetAllAgencies, $rootScope) {
/* commented out as this should now be done globally - this works when it's in place
GetAllAgencies.getData({}, function(agencieslist) {
SessionTimeoutService.checkIfValidLogin(agencieslist);
$scope.agencieslist = agencieslist.data;
});
*/
CandidateModel.getBlankCandidate();
$scope.candidateinfo = CandidateModel;
// get global agencies list
$scope.agencieslist = $rootScope.agencieslistglobal;
alert($scope.agencieslist); // shows 'undefined'
/*
GetNationalityList.getData({}, function(nationalitieslist) {
SessionTimeoutService.checkIfValidLogin(nationalitieslist);
$scope.nationalitieslist = nationalitieslist.data;
});
*/
// get global nationalities list
$scope.nationalitieslist = $rootScope.nationalitieslistglobal;
....
}
So... when I hard-code the data outside of the GetNationalityList.getData function (the bit that's commented out in the example above), it is populated & passed through OK & my drop-down list populates. When I don't do that, the $rootScope values are 'undefined'.
I have 2 theories -
somehow the homecontroller $rootScope isn't being recognised as the global I'm intending (that's just the name of the variable) and some other "passing back" action needs to be taken (I've tried several variations on "return nationalitieslist.data" and assigning it to other variables/handles). Also, nationalitieslist.data is in the same format as the hard-coded list, only it's longer.
I'm being caught out by the asynchronous nature of javascript and when I load the second page, the data just isn't there yet. I'm not convinced this is right as the DB call is finishing and I had an alert which showed me a random nationality name and it was there.
My frustration in part is coming from the fact that the GetNationalityList and GetAllAgencies code snippets work and assigns values correctly in the candidate controller (the bits that are now commented out), but are working subtly differently in the homecontroller.
Top tips, anyone, please?
I am sure this article can help.
http://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/
I had somewhat of a similar issue but I needed to emit from a factory.
My problem was answered here.
Angular $rootScope.$on Undefined
Your 2nd point is more your issue, although it's not JavaScript that's async in nature, it's Angular services such as your GetAllAgencies service. You're making async calls but not waiting for the response. That's why when you assign it manually it all works. Try putting your alert() inside the callback function to the getData() call. This should highlight how the process should work.

Resources