Angular: Get aware of spelling mistakes in function call with ng-click - angularjs

My question is about discovering possible spelling mistakes in angular expressions, in particular spelling mistakes in the function name.
Consider the snippet bellow:
I have two buttons there, the first one with correct spelling, the second with a spelling mistake in the angular expression. Clicking the second button does nothing and gives no hints about a potential error.
My question is now: are there ways to detect erroneous calls to function that don't exist (while executing the application)?
I am not looking for some checking possibility in the build or unit test process but rather would like to see a way I could get aware of such a potential issue when running the erroneous expression in the browser when the application is executed.
angular.module("myApp", [])
.controller("TestController", function($scope){
$scope.myFunction = function() {
console.log("Hello World");
};
});
angular.element(document).ready(function () {
angular.bootstrap(document, ['myApp']);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
<section ng-controller="TestController">
<button ng-click="myFunction()">myFunction</button>
<button ng-click="myFunctio()">myFunctio</button>
</section>

I'm not familiar with a built in option in angular to do that (using binding to an "undefined" object is a legit UC as things may become "undefined" during program run) - but you may write your own "ng-click" directive which, in case not finding the function to bound to, raise an error (exception or better - console error / warning).

This is an extremely common complaint about Angular. Even when writing code for the Closure compiler, with all the type annotations and everything, these still fall right through the cracks.
You can kluge something together, I've seen things like bussing all events to a common broker and looking for the target handler in the bound scope, and so on. But it always appears to be more trouble than it's worth.
Your unit tests are where you catch this sort of thing. It's why being able to test template code via triggering events is such an important thing for an Angular developer to master. If you trigger that button click and your test fails (e.g. your spyOn the handler never gets called), check the template.

Protractor (and other end to end testing frameworks) will do that for you.

I'm not sure if this would work for function calls or not, but it would solve part of the problem of misspelling something. In Scott Allen's AngularJS Playbook course on Pluralsight, he suggests creating a decorator for the $interpolate service to see if any bindings are potentially incorrect. Here is the code for that:
(function(module) {
module.config(function ($provide) {
$provide.decorator("$interpolate", function ($delegate, $log) {
var serviceWrapper = function () {
var bindingFn = $delegate.apply(this, arguments);
if (angular.isFunction(bindingFn) && arguments[0]) {
return bindingWrapper(bindingFn, arguments[0].trim());
}
return bindingFn;
};
var bindingWrapper = function (bindingFn, bindingExpression) {
return function () {
var result = bindingFn.apply(this, arguments);
var trimmedResult = result.trim();
var log = trimmedResult ? $log.info : $log.warn;
log.call($log, bindingExpression + " = " + trimmedResult);
return result;
};
};
angular.extend(serviceWrapper, $delegate);
return serviceWrapper;
});
});
}(angular.module("common")));

Related

How $watch changes of a variable in a service from component's controller?

I have been through all related topics on SO, namely these two:
$watch not detecting changes in service variable
$watch not detecting changes in service variable
are tackling the same issue, but i failed to make it working. Unlike in the above cases, I am using a controller from a component, hence maybe this is related to lacking binding in components, idk. Hope for some experinced assistance.
Have a service:
(function (angular) {
'use strict';
angular
.module('Test')
.service('ShareData', ShareData);
ShareData.$inject = [];
function ShareData() {
let vm = this;
vm.indexes = [];
vm.setIndexes = function(firstIndexParam, lastIndexParam, message) {
if (leaderIndexParam !== undefined || partnerIndexParam !== undefined) {
vm.indexes.mainIndex = firstIndexParam;
vm.indexes.secondaryIndex = lastIndexParam;
vm.indexes.message = message;
}
};
vm.getIndexes = function() {
return vm.indexes;
};
}
})(angular);
It is used in 3 components. Two of them are sending data into the service, the third one uses this data. Sending of data is accomplished in the following way, works:
ShareData.setIndexes(firstIndex, secondIndex, 'update_indexes');
Now here is my problem. In main parent controller i can comfortably access the data by
ShareData.getIndexes();
But my issue is that I need changes in indexes to trigger certain actions in parent controller, so I tried so do as stipulated by relevant questions here on SO:
$scope.$watch('ShareData.getIndexes()', function(newVal) {
console.log('New indexes arrived', newVal);
});
In main controller, I am injecting the service:
TabController.$inject = ['ShareData'];
and using it like:
let indexService = ShareData.getIndexes();
As i said, I can get the data when I am explicitly calling the function. The issue is that it needs to be triggered by the service itself.
It does not work regardless of shamanistic ceremonies a made :) Now, obviously, I am missing something. Should I somehow bind this service to the component, and if yes how is it done? Or maybe the solution is totally dysfunctional and impossible to achieve in my circumstances? An advise is appreciated!
UPDATE: I already have a functional solution with the same service working with $rootScope.$broadcast, however my aim is to get rid of it and not work with the $rootScope.
The problem is that you never actually change the value of vm.indexes - it always points to the same array. setIndexes only modifies properties of this array. That's why $watch, which by default checks for reference equality only, fails to spot the changes.
There are (at least) two ways of solving this: either make $watch check the object equality instead, by adding a third param there:
$scope.$watch('ShareData.getIndexes()', function(newVal) {
console.log('New indexes arrived', newVal);
}, true);
... or (better, in my opinion) rewrite the set function so that it'll create a new instance of indexes instead when there's a change:
vm.setIndexes = function(firstIndexParam, lastIndexParam, message) {
if (leaderIndexParam === undefined && partnerIndexParam === undefined) {
return;
}
vm.indexes = vm.indexes.slice();
Object.assign(vm.indexes, {
mainIndex: firstIndexParam,
secondaryIndex: lastIndexParam,
message: message
});
};
As a sidenote, simply calling setIndexes() does not trigger the digest - and $watch listener only checks its expression when digest is triggered.

$scope variable is undefined when it is set inside a function

I have the following example code in my learning app. The service does his job and pulls some data out of a page with json code generated by php, so far so good.
service:
(function() {
'use strict';
angular
.module('app.data')
.service('DashboardService', DashboardService);
DashboardService.$inject = ['$http'];
function DashboardService($http) {
this.getFormules = getFormules;
////////////////
function getFormules(onReady, onError) {
var formJson = 'server/php/get-formules.php',
formURL = formJson + '?v=' + (new Date().getTime()); // Disables cash
onError = onError || function() { alert('Failure loading menu'); };
$http
.get(formURL)
.then(onReady, onError);
}
}
})();
Then i call the getFormules function in my controller and put all the data inside my $scope.formuleItems and test if everything succeeded and 'o no'... $scope.formuleItems = undefined! - Strange because my view is showing data?
part of the controller:
dataLoader.getFormules(function (items){
$scope.formuleItems = items.data;
});
console.log('+++++++++++++++++', $scope.formuleItems); // gives undefined
The first thing i did was search around on stackoverflow to look if someone else had the same issue, and there was: Undefined variable inside controller function.
I know there are some walkarounds for this, i've done my own research, but something tells me that this (see example below) isn't the best way to solve this problem.
solution one: put $watch inside of the controller
$scope.$watch('formuleItems', function(checkValue) {
if (checkValue !== undefined) {
//put the code in here!
}
}
or even:
if($scope.formuleItems != null) {}
The rest of the controller is relying on $scope.formuleItems. Do i really have to put everything into that $watch or if? Can i fix this with a promise? I never did that before so some help would be appreciated.
The code in your callback
function (items){
$scope.formuleItems = items.data;
}
is evaluated asynchronously. That means you first fire the request, then javascript keeps on executing your lines of code, hence performs
console.log('+++++++++++++++++', $scope.formuleItems); // gives undefined
At this point the callback was not invoked yet, because this takes some time and can happen at any point. The execution is not stopped for this.
Therefore the value of $scope.formuleItems is still undefined, of course.
After that - at some not defined time in the future (probably a few milliseconds later) the callback will be invoked and the value of $scope.formuleItems will be changed. You have to log the value INSIDE of your callback-function.
You urgently have to understand this concept if you want to succeed in JavaScript, because this happens over and over again :)

How can I let Angular show an error on a Angular error?

I've taken over an Angular application from another developer.
Now I've been playing around with it and making my first edits.
Question is: when I bind to non-existing elements (or make any other mistake) I don't see any error, which sounds good but isn't because I want to be notified when I do something wrong.
How can I make Angular show errors?
To began, I recommend not using the minified version of angular, as the unminified version allows more coherent and clear errors to be logged in the console.
Next, I think the best way to handle angular errors is to write a custom wrapper to better handle them. Here is an example of how you could write a wrapper.
The first step would be to write a function that will handle the error in a way that you want. This is how I current handle angular errors. Note: this could be modified in many different ways to make the error handling more customized.
function HandleAngularError(Exception, AppName){
try {
var AppName = (window.parent._.isEmpty(AppName) ? "Angular App Unspecified" : AppName) + " - ";
if (window.parent._.isUndefined(Exception)) {
console.log(strAppName + "error: exception undefined", "AngularJs");
} else {
console.log(strAppName + "error: " + Exception.toString() + " " + JSON.stringify(Exception), "AngularJs");
}
} catch (e) {
alert("Handle Angular Error: " + Exception.toString() + " " + JSON.stringify(Exception));
}
}
The next step is to include the error handling function in the any of the Modules in you project and rely on the $exceptionHandler to then pass angular errors into your custom wrapper like so:
angular.module("someApp",[], function(){
//setup stuff here
}).factory( '$exceptionHandler', function () {
return function (exception) {
HandleAngularError(exception, "someApp");
};
});
By default, AngularJS is forgiving, that is to say it prefers to show nothing rather than throwing an exception if you binded an undefined value.
From Angular doc :
In JavaScript, trying to evaluate undefined
properties generates ReferenceError or TypeError. In Angular,
expression evaluation is forgiving to undefined and null.
One way of displaying an error would be to decorate the $eval function, which is used to evaluate binded expressions.
You could try something like this :
app.run(function($rootScope) {
var origRootScope = $rootScope,
origEval = origProvider.$eval;
//Override rootScope's $eval with our own
origProvider.$eval = function(expression, locals) {
// Test the expression value and add the behavior you like
if(typeof expression === 'undefined') {
// Log some kind of error
console.log('Some kind of error')
}
// Call the original $eval function
var returnValue = origEval.apply(this, arguments);
return returnValue;
}
});
I haven't tried that code but you should be able to add custom logging to $eval this way.

Why is $log always silenced by angular-mocks?

$log service is recommended over console.log for AngularJS apps. One common use case for such logging is seeing debug print when running tests. The problem is, angular-mocks silences $log by default replacing it by a mock. Well, I do sometimes need to test my debug print, but I just need to see it much more often. The problem is, the default behavior insists on using dummy logging and I don't even see a proper way to revert back to real $log. I've made a jsfiddle example to illustrate it, try running it while looking at devtools console http://jsfiddle.net/ivan4th/EnvL9/
var myApp = angular.module('myApp', []);
describe('myApp', function () {
var element, rootScope;
beforeEach(module('myApp'));
it('does something', inject(function ($log) {
$log.log("this message gets eaten by angular-mocks");
console.log("this message is visible though");
}));
});
The first message is skipped, while second is shown as expected.
Why such strange behavior is used and is there any way to fix it besides not using $log?
The mock $log service is designed to allow you to easily test and assert whether messages are logged or not, and ironically, to prevent log noise to the console when running tests.
If you don not like this behavior you can easily change it by doing:
$log.log = function(message){ console.log(message);};
Alternative you could output all previously logged messages to the console by doing:
console.log($log.log.logs)
You could add either method into a global before or after in your jasmine.
Based on an idea from dtabuenc's answer, I've implemented workaround that
dumps log outputs for Jasmine tests that fail, in the form of global
afterEach():
afterEach(inject(function ($log) {
// dump log output in case of test failure
if (this.results().failedCount) {
var out = [];
angular.forEach(["log", "info", "warn", "error", "debug"], function (logLevel) {
var logs = $log[logLevel].logs;
if (!logs.length)
return;
out.push(["*** " + logLevel + " ***"]);
out.push.apply(out, logs);
out.push(["*** /" + logLevel + " ***"]);
});
if (out.length) {
console.log("*** logs for: " + this.description + " ***");
angular.forEach(out, function (items) { console.log.apply(console, items); });
}
}
$log.reset();
}));
Unfortunately without replacing angular-mocks' $log implementation
the order of messages with different log levels is lost, but I guess
I'll be able to live with that. Silencing all log messages including
Angular's own errors and warnings during tests still sound like bad
design choice though.

Angularjs and Meteor "Session" reactivity, is there a way?

I'm trying to work with Meteor and Angularjs. I'm using Meteor_angularjs package, which works OK with Collections.
Now I'm trying to use Session and my reactive data store:
TestCtrl = [
"$scope",
function($scope){
$scope.value = Session.get('someValue');
}
]
This does not work.
QUESTION: Any suggestions on how to tie down Meteor's Session and Angular?
As far as I understand, I can write directive that will be polling Session every so ofter, however I don't think that's a good choice.
Thanks
UPDATE:
I've tried the following:
TestCtrl = [
"$scope",
function($scope){
Meteor.autorun(function(){
$scope.config = Session.get('testsConfig');
if (!$scope.$$phase){
$scope.$digest();
}
});
}
]
and it sort of works, however I get the following error:
Error: INVALID_STATE_ERR: DOM Exception 11
Error: An attempt was made to use an object that is not, or is no longer, usable.
at derez (http://localhost:3000/test:95:41)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30) angular.js:5526
$get angular.js:5526
$get angular.js:4660
$get.Scope.$digest angular.js:7674
(anonymous function) controllers.js:46
Meteor.autorun.rerun deps-utils.js:78
_.extend.run deps.js:19
Meteor.autorun.rerun deps-utils.js:78
_.extend.flush deps.js:63
_.each._.forEach underscore.js:79
_.extend.flush deps.js:61
_.each._.forEach underscore.js:79
_.extend.flush deps.js:60
UPDATE 2:
I've tried the service like this (might be wrong usage), still nothing. Now it doesn't update at all on Session value's changes.
Meteor.autorun(function(){
app.factory('ssn', function(){ return{
get: function(val){
return Session.get(val);
}
}});
});
TestCtrl = [
"$scope","ssn",
function($scope, ssn){
$scope.config = ssn.get('testsConfig');
}
]
UPDATE 3: Angular has $apply() for
to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries)
At the same time Meteor has Meteor.render() for
Most of the time, though, you won't call these functions directly — you'll just use your favorite templating package, such as Handlebars or Jade. The render and renderList functions are intended for people that are implementing new templating systems.
However, it seems like I just cannot put 2 and 2 together. :(
this as an old question with old answers but I see people referring to it so here is the updated answer.
First - there is a new library for angular-meteor that handles those cases for you.
And this library gives you two possible solutions:
If you want to bind a Session variable to a scope variable, use the $meteorSession service.
What it does is that every time the scope variable will change, it will change to Session variable (and trigger an autorun if it's placed inside one).
and every time the Session variable will change, the scope variable will change as well (and change the view that it's placed upon).
If you are using the Session variable just to get a variable reactive (meaning trigger an autorun), you should use getReactively . this just returns the already existing scope variable but trigger an autorun every time it changes. a good example of this can be found it our tutorial.
Note: In anyway, when you use Tracker.autorun inside Angular, you need to connect it to a scope. this can be easily done if you replace Tracker.autorun with the $meteorUtils autorun function
Hi here is an option (might not be the best but it works I think)
app.service('Session',function($rootScope){
var self = this;
self.objects = {};
self.get = function(name){
self.objects[name] = {"value" : Session.get(name)};
Meteor.autorun(function() {
var i = Session.get(name);
if(self.objects[name].value != i){
if (!$rootScope.$$phase){
$rootScope.$apply(function(){
self.objects[name].value = i;
});
}
}
});
return self.objects[name];
}
self.set = function(name,value){
self.objects[name].value = value;
Session.set(name,value);
}
return self;
});
Call it in the $scope like this
$scope.test = Session.get("test");
In the view as {{test.value}}. Sorry for the late answer .
Happy new year!
try
angular.module('yourmod', [])
.controller('TestCtrl', ['$scope', function($scope) {
var c = Deps.autorun(function (comp) {
//... put reactive stuf on scope.....
if(!comp.firstRun) {
// only do not do aply at first run becaulse then apply is already running.
$scope.$apply()
}
});
// and to realy make it nice...
$scope.on('$destroy', function () {c.stop()});
}])

Resources