I'm a seasoned Java developer by now, and I love how Java uses types, signatures, methods, etc. It makes everything understandable, easy to follow and logical.
Now I'm trying to develop something in Javascript. I'm trying to learn Angular JS. However I am finding it so very confusing to understand how this code is developed.
Take a look at the code at this tutorial about ng-repeat. I'll post a sampling of the code;
app.directive('lkRepeat', function(){
return {
transclude : 'element',
compile : function(element, attr, linker){
return function($scope, $element, $attr){
var myLoop = $attr.lkRepeat,
match = myLoop.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
indexString = match[1],
collectionString = match[2],
parent = $element.parent(),
elements = [];
// $watchCollection is called everytime the collection is modified
$scope.$watchCollection(collectionString, function(collection){
var i, block, childScope;
// check if elements have already been rendered
if(elements.length > 0){
// if so remove them from DOM, and destroy their scope
for (i = 0; i < elements.length; i++) {
elements[i].el.remove();
elements[i].scope.$destroy();
};
elements = [];
}
My question is: How can you read such code? When functions are called, where are the method signatures? Also, when returning a value of function with parameters like $scope, $element, $attr, are these just place holders for the variables that will be passed? If so, then why do they have such funny names, why not drop the dollar sign?
When functions are called, where are the method signatures?
The closet JavaScript comes to having method signatures is that, when declaring a function, you can name the arguments.
function foo (named_argument, another_named_argument) {
Arguments are always optional. If you don't specify a value, it will get undefined.
You can specify as many arguments as you like and the function can access them through the arguments object.
Also, when returning a value of function with parameters like $scope, $element, $attr, are these just place holders for the variables that will be passed?
Yes
If so, then why do they have such funny names, why not drop the dollar sign?
The dollar sign is just a character that is allowed in a JavaScript identifier. Some people use it to indicate that the variable is expected to hold a jQuery object (because jQuery keys all its functionality off a variable called $).
Related
I have the following code that I am trying to use to register a callback on an array of buttons. But I cannot seem to understand how I can bind the strings that I would need in the callback. Any suggestions would be much appreciated!
for (var i = 0; i < this.car_types.length; ++i) {
this.select_car_buttons.push($("#button_select_car_" +
this.car_types[i].car_type));
this.select_car_buttons[this.select_car_buttons.length - 1]
.click(function() {
console.log(this.car_types[i].car_type);
}.bind(this));
}
Somehow the this object is the button itself and not the object under whose scope the function is called.
EDIT : It seems like the this object was indeed being passed in properly. The issue is that the variable i is not going out of scope and is being captured by reference not by value. How should I go about solving this problem?
Also there seem to lots of such issues with JavaScript as a language (at least things that can be classified as an issue considering the semantics employed by the traditional C family languages such as C and C++ to be correct), is there some article I can read that warns me against these types of issues?
ANOTHER EDIT : On trying making a closure with the value of i captured by value I tried the following code
this.select_car_buttons[this.select_car_buttons.length - 1]
.click((function(scoped_i) {
return function() {
console.log(this.car_types[scoped_i].car_type);
}.bind(this);
}(i)));
But I get the following error in Safari
TypeError: undefined is not an object (evaluating 'scoped_i')
EDIT : The same code works in Firefox and Chrome but not in Safari!
This is a scope issue. For modern browsers (that support ES6) you could just change var to let in your for loop and it would get fixed.
for (let i = 0; i < this.car_types.length; ++i)
Quoting the MDN docs
The let statement declares a block scope local variable, optionally initializing it to a value.
For more global support (non ES6 support) use an immediately invoked function to create extra scope for the variable (which you will pass as a parameter)
this.select_car_buttons[this.select_car_buttons.length - 1]
.click((function(scoped_i) { // IIF starts here, the new variable is called scoped_i for verbosity
return function() { // your original function code goes here
console.log(this.car_types[scoped_i].car_type); // use the newly scoped variable
}.bind(this);
}.bind(this)(i))); // end and execute the IIF while passing the i variable to it
Yes, this structure do make a lot of closures and make code very hard to read. Since you use jQuery, there are a much better way to solve this problem which saves the data in html:
html:
<button class="select-car" data-car-type="CarA">Select CarA</button>
<button class="select-car" data-car-type="CarB">Select CarB</button>
<!-- And a lot of buttons -->
js:
var selectCarOnClick = function() {
console.info($(this).data('car-type'));
};
$('button.select-car').click(selectCarOnClick);
Live exmaple: http://codepen.io/SCLeo/pen/VaQYjW
If you have a lot of other information to store and you want to use a object to store them instead of DOM, you can save car-name or car-id instead of car-type.
Here is the document about $.data: https://api.jquery.com/jquery.data/
Starting to get into angular.js, I've seen a common pattern relating to arguments of callbacks. An example is the ui-router documentation:
var myApp = angular.module('myApp', ['ui.router']);
myApp.config(function($stateProvider, $urlRouterProvider) {
// Do stuff with ui-router providers
}):
But, from want I've seen, the myApp.config callback's arguments could be different and it would still behave as expected:
myApp.config(function(OtherProvider) {
// Do stuff with provider from another service
});
How does myApp.config know the difference? Is there done weird magic introspection going on, or is there some basic JS concept that allows for this? E.g. something like:
myApp.config = function(callback) {
var providerNames = callback.argumentNames;
var providers = this.findProviders(providerNames);
};
Perhaps I'm too used to python, where this sort of feature would only be possible through some very scary hacks.
This is basically part of javascript. Javascript is very very flexible regarding functional arguments when it is invoking.
For example you declare a function:
function abc (first, second) {
}
But when you call it, you can pass any number of arguments even without zero argument. If you don't pass an argument but it has been defined when declaring the function, you will get value 'undefined' for that particular argument but it does not interrupt code execution.
Javascript provide a special local array (Basically everything is object in JS) named 'arguments' inside the function. I am calling special because it an Array with only one Array properties 'length' other Array methods or properties are unavailable
abc(firs,second,third)
Inside abc, we can check provided arguments
function abc (first, second) {
var args_total = arguments.length;
var no_of_defined_args = Function.length;
}
Yes Function.length is another properties by which you can check how many arguments are really defined by this function
This is a "scary hack" used by Angular for one method of dependency injection - where the service names are automatically resolved from the argument names. There is no intrinsic support for such an operation at the language level.
The argument values (but not the argument names) can be accessed via the arguments object within the function. However, the argument names themselves can only be accessed by applying [[ToString]] to the function object and parsing the result1, which is a "scary hack".
In the explicit forms the injected parameters must appear in the same order as the list of supplied dependencies - in this case the names of the parameters don't matter because the service names have been supplied separately.
An excerpt of the forms, from the linked documentation, slightly modified:
// service names supplied separately
// - only Order of parameters matters
someModule.controller('MyController',
['$scope', 'greeter', function(I_am_a_scope, greeter) { ..
// service names supplied separately
// - only Order of parameters matters
var MyController = function($scope, GREETER) { ..
MyController.$inject = ['$scope', 'greeter'];
// "scary hack" magic of Angular JS, as service names not specified separately
// - the Name of the parameters is used as service name
// arguably Too Much Magic (TM), and a source of pain for minification
someModule.controller('MyController', function(greeter, $scope) { ..
The first two forms use Function.prototype.apply, which is similar to apply() in Python. The third asks for, and parses, the textual representation of the function - a "scary hack".
1To see how the magical form can be implemented, consider the following:
f = function (a,b) { return a+b }
f.toString() // e.g. -> "function (a, b) { return a + b; }"
ECMAScript 5 says Function.prototype.toString returns:
An implementation-dependent representation of the function is returned. This representation has the syntax of a FunctionDeclaration. Note in particular that the use and placement of white space, line terminators, and semicolons within the representation String is implementation-dependent.
(I'm not aware of any modern browsers that don't return a 'usable' result; the behavior of returning a representation is not optional is ES5.)
I am trying to accomplish something like in : eval() with variables from an object in the scope
The correct answers suggests using the "with" keyword, but I can't find any examples of someone actually using "with". Can someone explain how to pass multiple variables using "with" into an "eval" expression like in the link above ?
i wouldn't recommend using with or eval except as a learning exercise because either one slows code down, and using them both at once is especially bad and frowned upon by the larger js community.
but it does work:
function evalWithVariables(code) {
var scopes=[].slice.call(arguments,1), // an array of all object passed as variables
A=[], // and array of formal parameter names for our temp function
block=scopes.map(function(a,i){ // loop through the passed scope objects
var k="_"+i; // make a formal parameter name with the current position
A[i]=k; // add the formal parameter to the array of formal params
return "with("+k+"){" // return a string that call with on the new formal parameter
}).join(""), // turn all the with statements into one block to sit in front of _code_
bonus=/\breturn/.test(code) ? "": "return "; // if no return, prepend one in
// make a new function with the formal parameter list, the bonus, the orig code, and some with closers
// then apply the dynamic function to the passed data and return the result
return Function(A, block+bonus+code+Array(scopes.length+1).join("}")).apply(this, scopes);
}
evalWithVariables("a+b", {a:7}, {b:5}); // 12
evalWithVariables("(a+b*c) +' :: '+ title ", {a:7}, {b:5}, {c:10}, document);
// == "57 :: javascript - How to pass multiple variables into an "eval" expression using "with"? - Stack Overflow"
edited to use any number of scope sources, watch out for property name conflicts.
again, i don't normally use with, but this was kinda fun...
Updated with three methods that work, and the original one that does not
I have made an angular js directive, and I am trying to access the ctrl.$modelValue. It does not work in the main flow.
I have three potential solutions, all of which have drawbacks.
Method 1 does not work as I would hope, and I can't find any other property available in the directive directly accessible in this way.
Method 2 works because it waits until the current flow is complete, and then executes in the next moment. This happens to be after the angular js lifecycle is complete, and the controller seems to be hooked up to the model at this point. This does not seem ideal to me, as it is waiting for all execution to finish. If it is possible, I would prefer to run my code as soon as the controller is linked to the model, and not after all the code in current flow completes.
Method 3 works well, accessing the model from the $scope, and determining what the model is from the string representation accessed on the attrs object. The drawback is that this method uses eval in order to get hold of the addressed value - and as we all know, eval is evil.
Method 4 works, but it seems like an overly complex way to access a simple property. I can't believe that there is not a simpler way than the string manipulation, and while loop. I am not confident that the function for accessing properties is 100% robust. At least I might like to change it to use a for loop.
Which method should I use, or is there a 5th method that has no drawbacks?
DEMO: http://jsfiddle.net/billymoon/VE9dX/9/
HTML:
<div ng-app="myApp">
<div ng-controller="inControl">
I like to drink {{drink.type}}<br>
<input my-dir ng-model="drink.type"></input>
</div>
</div>
Javascript:
var app = angular.module('myApp', []);
app.controller('inControl', function($scope) {
$scope.drink = {type:'water'};
});
app.directive('myDir', function(){
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, element, attrs, ctrl) {
// Method 1
// logs NaN
console.log({"method-1": ctrl.$modelValue});
// Method 2
// on next tick it is ok
setTimeout(function(){
console.log({"method-2": ctrl.$modelValue});
},0);
// Method 3
// using eval to access model on scope is ok
// eval is necessary in case model is chained
// like `drink.type`
console.log({"method-3": eval("$scope."+attrs.ngModel)});
// Method 4
// using complex loop to access model on scope
// which does same thing as eval method, without eval
var getProperty = function(obj, prop) {
var parts = prop.split('.'),
last = parts.pop(),
l = parts.length,
i = 1,
current = parts[0];
while((obj = obj[current]) && i < l) {
current = parts[i];
i++;
}
if(obj) {
return obj[last];
}
}
console.log({"method-4": getProperty($scope,attrs.ngModel)});
}
};
});
There are quite a few alternatives, some better than others depending on your requirements, e.g. should you be notified if the view value changes, or the model value, or are you happy with the initial value.
Just to know the initial value you can use either of the following:
console.log('$eval ' + $scope.$eval(attrs.ngModel));
console.log('$parse ' + $parse(attrs.ngModel)($scope));
Both $eval and $parse end result is the same, however $eval sits off $scope, where $parse is an Angular service which converts an expression into a function. The returned $parse function can then be invoked and passed a context (usually scope) in order to retrieve the expression's value. In addition, if the expression is assignable the returned $parse function will have an assign property. The assign property is a function that can be used to change the expression's value on the given context. See $parse docs.
If you need to be informed when the model value changes, you could use $watch, however there are better ways when dealing with ngModel. If you need to track changes to the the model value when the change occurs on the model itself, i.e. inside your code, you can use modelCtrl.$formatters:
ctrl.$formatters.push(function(value){
console.log('Formatter ' + value);
});
Note that $formatters are only called when the model value changes from within your code, and NOT when the model changes from user input. You can also use $formatters to alter the model view value, e.g. convert the display text to uppercase without changing the underlying model value.
When you need to be informed of model value changes that occurs from user input, you can use either modelCtrl.$parsers or modelCtrl.$viewChangeListeners. They are called whenever user input changes the underlying model value:
ctrl.$viewChangeListeners.push(function(){
console.log('$viewChangeListener ' + ctrl.$modelValue, arguments);
});
ctrl.$parsers.push(function(value){
console.log('$parsers ' + value, arguments);
return value;
});
$parsers allows you to change the value from user input to model if you need to, where $viewChangeListeners just lets you know when the input value changed.
To sum up, if you only need the initial value, use either $eval or $parse, if you need to know when the value changes you need a combination of $formatters and $parsers/$viewChangeListeners.
The following fiddle shows all these and more options, based on your original fiddle:
http://jsfiddle.net/VE9dX/6/
Instead of using the native eval use the $eval function on the $scope object:
console.log($scope.$eval(attrs.ngModel))
See this fiddle: http://jsfiddle.net/VE9dX/7/
New to angular. Trying to understand the proper way to do things
I am trying to create a directive that will run custom logic when a user clicks on a link before moving to the next page. However, before I can even get to the custom logic I'm having difficulty setting up the directive. I want to be able to:
A) Have certain attributes on the directive be optional (set to different defaults in certain cases)
B) Have a user be able to pass a string literal OR a variable from the controller's scope into the directive's attributes to be used on the directive's scope.
My Problems:
1.) If I use string literals in the directive's attributes, the corresponding $scope property is undefined. I have to access it through the $attrs. This isn't the worst thing but seems like a wrong practice to have to check the $scope.Prop and if it is undefined check $attrs.Prop.
2.) Also, this answer seems to say I need to use single quote when using string literals in the attributes but that behavior does not work in my example.
3.) When I have certain attributes on the directive not set, the default value I set on the $scope in the 'controller' function on the directive is not reflected when rendered. I cannot figure out why.
Code in Plnkr
Any help would be appreciated!
Here is a version where all three cases work:
The literals work without anything special
The scope values work using the usual {{ expression }} syntax
Defaults are provided in the directive's template using this syntax href='{{strHref || 'default href'}}' which I found in this thread.
Here is the full directive definition:
return {
restrict: 'E',
replace: true,
scope: {
strHref: '#link',
strText: '#displayText',
},
template: '<div><div>variables: {{strHref || \'default href\'}}, {{strText || \'default text\'}}</div> result: {{strText || \'default text\'}}<span ng-show="blnIsBackButton"> (Return)</span></div>',
};
Link : http://plnkr.co/edit/RB6shHI0xYa1FkP4eXDy
Scope & can be used for literals /expression. Scope = for setting up 2 way binding and scope = for method call.
You can set default value for property in controller if you want
I think directive name has to be lower case. Can someone confirm this? If i use pageLink it does not work but works for pagelink. (note 'L'). Same for property.
There are some nice videos about scope in http://www.egghead.io/.
I created this in Plunker which uses all 3 scope isolation types