access scope variable from directive by name, avoiding using eval - javascript

I'm new to angularJs and the currently working example(fiddle) doesn't feel right. When you try to blur the field it should increment event.times, currently I doing it by $apply on line 10, so I'm asking is there a better way to achieve this ? Maybe something like this var times = scope.$get(attrs.timesField); and then scope.$apply(function(){ times += 1; });

Whenever a directive does not use an isolate scope and you specify a scope property using an attribute, and you want to change the value of that property, use $parse:
<input custom-field times-field="event.times" ng-model="event.title" type="text">
link: function (scope, element, attrs){
var model = $parse(attrs.timesField);
element.bind('blur', function(){
model.assign(scope, model(scope) + 1);
scope.$apply();
});
}
fiddle

You're right, that's not the correct way to do something like that.
Find out the $watch() function, especially when using it in directives.

There is no need to use eval here, you can just do this:
scope.$apply(function(){
scope.event.times++;
});
However you must ensure that an event object exists on scope before using this directive and I don't think that it is good for a directive to rely on data being set elsewhere.
What are you actually trying to achieve?

Related

Angular radio ng-change works only first time

I can't understand why angular ng-change function are called only in first click. In this example
http://jsfiddle.net/ZPcSe/5/
function is called every time when i change current radio selection, but in my code something is wrong
http://jsfiddle.net/4jL3u8ko/1/
Can someone change my code to work like first example, and explain why it isn't working?
Great question.
This is not an answer but one may call this explanations to answers and any further problems that may occur. And this happens quite often, so its important to understand. The answer given by #Victor is ok and the comment of #Atias is also helpful.
Explanation -
Simple explanation : ng-repeat forms child scope.
Little detailed :
So your application has 3 scopes (or may increase depending on the number of values in $scope.radioButtons). Let me name them - parentScope(main scope), childScope1(scope of first element of ng-repeat) and childScope2(scope of second element of ng-repeat).
Parent scope variables : calledFunctions(array), radioButtons(object) and newValue(function)
ChildScope1 variables : name(assigned a literal at the start), val(assigned an object at start) and value(it has no value, or undefined)
ChildScope2 variables : name(assigned a literal at the start), val(assigned an object at start) and value(it has no value, or undefined)
What happens when you click on the radio button of childScope1(for first time):
ChildScope1.value = ChildScope1.name or ChildScope1.value= "Radio1"; the ng-change directive checks if this scope's model has been changed? Yes, as it was undefined and now it has a literal("Radio1")!! So call ChildScope1.newValue(), which obviously is not present. Therefore now look for parent- call parentScope.newValue(). This is present, so execute it.
keep in mind that ChildScope1.value= "Radio1";
After clicking on other radio buttons......
Now lets click 2nd time on ChildScope1's radio button. After clicking it- ChildScope1.value = ChildScope1.name or ChildScope1.value= "Radio1"; the ng-change directive checks if this scope's model has been changed??? No!! Because it still contains the same literal vaule as before, so do not even look for ChildScope1.newValue() or ParentScope.newValue() function.
Now same thing happens with ChildScope2.
Hope this explains why your fiddle works like a one time binding, or how #Victor or #Atias are correct.
Simply the solution is - The ng-model of ChildScope1 or ChildScope2 or any other child scopes, should point to a variable of parent scope. So you can use $parent or make an object in parent scope(main scope) with a property( eg.- ParentVal={ChildVal = ""};). And in ng-model inside ng-repeat- write ng-model=ParentVal.ChildVal.
Sorry for my poor english and please pardon my spelling mistakes if there is any.
Thanks
ng-model is tricky. You should always, always have a dot in your models, or you get this kind of problem where you are creating several models and you think you only have one.
As a quick fix, use $parent.value instead of just value in your ng-model and ng-change.
As a good fix, have a proper model object in the scope, instead of storing values directly in the scope:
$scope.model = {};
ng-model="model.radiosValue"
As an even better fix, use the controllerAs pattern and use that object as the view model.

$compile link function apparently not applying scope

I'm trying to compile some HTML on my controller like so:
$scope.online = networkService.isOnline();
var wrapper = angular.element(document.createElement("i"));
var compiledContents = $compile($scope.chapter.contents);
var linkedContents = compiledContents($scope);
wrapper.append(linkedContents);
$scope.chapter.linkedContents = wrapper.html();
Where the HTML being compiled has a few elements with a ng-if='online' there. But the compiled HTML always comes out with those elements commented, as if online was not true (which it is - I even got to the point where I added a console.log(scope.online) in Angular's $compile function and it printed true).
Am I missing anything here?
wrapper.html() gives back a string representing the inner HTML of the element. You are then assigning that string to $scope.chapter.linkedContents. Although it is not clear from your question how and where you are actually using $scope.chapter.linkedContents, one thing is certain - this string is definitely not "linked" in any way.
The actual "linkedContents" are the elements that should end up in the DOM. In your case, it is the wrapper element with its contents, but again - unclear, how, if ever, it ends up in the DOM.
But you shouldn't even be dealing with DOM in a controller. Controllers are DOM agnostic, so right there you should see a big warning sign that you are doing something wrong. Make sure that you understand the role of a controller, a directive, etc...
I think I understand what the problem you are trying to solve. You get some dynamic uncompiled HTML (or actual elements) - i.e. $scope.chapter.contents and you need to have it placed in the DOM and compiled/linked.
Typically, to bind HTML one would use ng-bind-html (assuming it's either trusted or sanitization is on):
<div ng-bind-html="chapter.contents">
</div>
But this would not be $compiled. To compile, I'd suggest writing your own directive that would work similar to ng-bind-html, but would also compile it:
<div compile-html="chapter.contents">
</div>
Then, the directive would take the content, compile/link it against some scope (say, child scope) and append it to the element hosting the directive compileHtml.
.directive("compileHTML", function($compile, $parse){
return {
scope: true,
link: function(scope, element, attrs){
// get the HTML content
var html = $parse(attrs.compileHtml)(scope);
element.empty();
// DISCLAIMER: I'm not dealing with sanitization here,
// but you should keep it in mind
$compile(html)(scope, function cloneAttachFn(prelinkContent){
element.append(prelinkContent);
});
}
};
})

Why can't I overwrite the value of a variable like this?

I'm trying to figure out why I'm having trouble overwriting a value passed to an angularJS directive via an isolate scope (#). I try to overwrite the value of vm.index with the following:
vm.index = parseInt(vm.index, 10)
However, it doesn't work for some reason.
If I change it to:
vm.newIndex = parseInt(vm.index, 10)
It works. Also, assigning the value on the $scope works.
Why doesn't the first method work?
I've created this example plunker for reference.
As you used # here which need value from an attribute with {{}} interpolation directive. And seems like directive is getting loaded first & then the vm.index value is getting evaluated. So the changes are not occurring in current digest cycle. If you want those to be reflected you need to run digest cycle in safer way using $timeout.
$timeout(function(){
vm.index = parseInt(vm.index, 10)
})
Above thing is ensuring that value is converted to decimal value. The addition will occur on the on the directive html <h2>Item {{ vm.index + 1 }}</h2>
Working Demo
The possible reason behind this
As per #dsfq & my discussion we went through the angular $compile API, & found that their is one method call initializeDirectiveBindings which gets call only when we use controllerAs in directive with an isolated scope. In this function there are switch cases for the various binding #,= and & , so as you are using # which means one way binding following switch case code gets called.
Code
case '#':
if (!optional && !hasOwnProperty.call(attrs, attrName)) {
destination[scopeName] = attrs[attrName] = void 0;
}
attrs.$observe(attrName, function(value) {
if (isString(value)) {
destination[scopeName] = value;
}
});
attrs.$$observers[attrName].$$scope = scope;
if (isString(attrs[attrName])) {
// If the attribute has been provided then we trigger an interpolation to ensure
// the value is there for use in the link fn
destination[scopeName] = $interpolate(attrs[attrName])(scope);
}
break;
In above code you can clear see that there they placed attrs.$observe which is one sort of watcher which is generally used when value is with interpolation like in our case it is the same {{index}}, this means that this $observe gets evaluated when digest cycle run, That's why you need to put $timeout while making index value as decimal.
The reason #dsfq answer works because he use = provides two way binding which code is not putting watcher directly fetching value from the isolated scope, here is the code. So without digest cycle that value is getting updated.
Apparently it has something to do with one-way binding of the scope index value. So Angular won't update scope.index (or this.index in case of bindToController: true) because scope is configured as
scope: {
index: '#'
},
If you change it to two-way binding like:
scope: {
index: '='
},
It will work:
<some-directive index="$index"></some-directive>
Demo: http://plnkr.co/edit/kq16cpk7gyw8IE7HiaQL?p=preview
UPD. #pankajparkar made a good point that updating value in the next digest fixed the issue. This approach for the problem then is closer then what I did in this answer.

scope variable inside object does not get updated

I have an issue where my $scope.searchVal variable updates, but if I assign it inside an object, it doesn't seem to update at all.
See below:
<div ng-controller="MyCtrl">
<div>{{searchVal}}</div>
<div>{{searchFilter}}</div>
<div>
<input ng-model="searchVal"/>
</div>
</div>
See the JSFiddle
If I type in the input, the first div's value updates properly for searchVal, but the searchFilter object that contains $scope.searchVal as a value is set to the original value of '' and does not update accordingly.
I've considered (and tried) updating the whole object everytime I need it by calling a function, but this seems really hacky to me. Is there a simple way to get this done? Am I missing something?
What you have to is $watch the value from the scope something like:
Online Demo
$scope.$watch('searchVal',function(newVal, oldVal){
$scope.searchFilter.search = newVal;
});
It isn't as straighforward as it seems since angular has no way of knowing whether you want the dynamic binding for the property or not.
You can achieve what you are trying to do using $watch service. Put a watch on the searchVal & use it to detect changes & update the object property accordingly.
Please put $watch . It will always watch the value change of variable
$scope.$watch('searchVal',function(newVal, oldVal){
$scope.searchFilter.search = newVal;
});
updated fiddle http://jsfiddle.net/be6s1eLo/3/

AngularJS Create new scope in template?

Using Symfony2.x, I have a twig loop going through data for some data, and I also have an ng-repeat going on for similar elements (difference being these ones get loaded in the background though), but both are to share the same functionality.
I have some odd functionality going on in the twig loop versions that are working perfectly fine in the ng-repeat versions. I have a feeling it's simply a scope issue.
I read in the docs that ng-repeat will automatically create a new scope for the repeated elements, but of course this doesn't happen with a twig loop.
How does one manually, and preferably exclusively IN the template, invoke a new scope per repeated element?
The easiest way might be to add a directive to each element. This can be done in the template. The directive can then request new scope (via scope:true or scope:{}) and each repeated element will get a new scope associated with it.
You can create a directive on an element like:
<div mydirective></div>
Then in your code, define the directive:
myApp.directive('mydirective',function(){
return {
scope: true,
link: function(scope, elem, attrs){
// do some scope / element stuff here
}
}
});

Categories

Resources