AngularJS : Why can't $interpolate render ng-style? - javascript

I have created a drop down input directive, where the user can pass in a template string to set what the list will actually render.
In my example I am passing in <div style="background-color: darkgrey" ng-style="{\'color\': isActive?\'red\':\'blue\'}">{{index}}</div> so it will render the items index number and then color it based on isActive. The index is show properly, and the background color is right but ng-style is being ignored.
I create the template in a setTimeout because I have to wait for ng-repeat to render the <li>.
setTimeout(function() {
var spanList = elm.find('li');
for (var index = 0; index < scope.list.length; index++) {
var listElm = angular.element(spanList[index]);
var listData = scope.list[index]
listElm.html($interpolate(scope.listTemplate)(listData));
}
}, 0);
I am iterating through the <li> elements and setting the template for each one. I am using $interpolate instead of $compile because using $compile would result in [[Object HTMLDivElement]] and I couldn't figure out why. From what I can tell $compile uses $interpolate anyways, and it works except that ng-style, ng-show, and ng-hide don't work. Why would angular's directives not work with $interpolate?
Here is the working example plunker.

$interpolate returns a function which returns a string. It is used to resolve {{bindings}}.
$compile returns a function which returns an html-element. It is used to compile raw (html) strings into html-elements and resolve angular-code inside of this string. $compile uses $interpolate to resolve any bindings inside this string.
In your case you probably want to use $compile and .replaceWith().

$interpolate is only for interpreting markup (example, "{{firstName}} {{lastName}}"):
AngularJS Documentation
$interpolate is a service used by $compile:
Compiles a string with markup into an interpolation function.
ngStyle, ngShow, etc do not rely on interpolation. Instead they rely on the $parse service to evaluate the AngularJS expression, and determine how to render the behavior.
$compile is appropriate in your scenario because it compiles the template HTML fragment by activating any directives within it, which indirectly calls $parse to evaluate expressions. Remember to append the template to the DOM before you $compile because directives may rely on parent directives higher up the DOM. I recommend the following pattern:
var e = angular.element(someTemplate);
element.append(e);
$compile(e)($scope);

Related

Accessing scope params of children directives

I'm new in AngularJS, using it for two months in a project. I've learned how to use directives and theirs scopes (false, true, obj literal), but there's some questions about it...
First of all, we have some ng-repeats and directives with some behaviors, I tried to present a equivalent scenario in this link.
I didn't figure out how to access a function (testfn - within ng-controller) inside a directive child of another directive myItemDirective. But in myStepDirective it's accessible, I tried to pass it like in the first "layer" but didn't work.
PS.1: I created a myStepDirective with a isolated scope for other examples, if you need, just uncomment to test. Both I got a way to access params/functions from parent (controller), but not inside a grandchild.
Why directive's scope params doesn't work with camel case params? I don't remember to read some hint in AngularJS docs... typewithnocase inside myItemDirective works but typeList not.
Thanks!
EDITED]
For your 1. Here is a working fiddle working with limited scope and camel to snake case conversion : https://jsfiddle.net/wu0avqau/
I spend a loooooong time not understanding why it didn't worked but you juste forgot a = in your ng click inside your second directive
ng-click"testfn()"
For your 2. I can refer you to the documentation : https://docs.angularjs.org/guide/directive
Normalization
Angular normalizes an element's tag and attribute name to determine which >elements match which directives. We typically refer to directives by their case->sensitive camelCase normalized name (e.g. ngModel). However, since HTML is case->insensitive, we refer to directives in the DOM by lower-case forms, typically >using dash-delimited attributes on DOM elements (e.g. ng-model).
The normalization process is as follows:
Strip x- and data- from the front of the element/attributes.
Convert the :, -, or _-delimited name to camelCase."
basicaly your myItemDirective would be my-item-directive inside your template but still be myItemDirective inside your js.
Good luck,
Thibaud Lamarche

$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.

How to know if $interpolate fails in AngularJS 1.2?

I'm trying to use the $interpolate service in AngularJS 1.2.16. However, I don't know how to check if Angular has properly interpolated all the vars in my string or not.
I noticed in AngularJS 1.3 they have added a new parameter AllOrNothing, which will cause the interpolate function to return an undefined if any embedded expressions fail to evaluate fully.
Any ideas how I can perform a similar check in 1.2? I would be okay looking for any embedded expressions, but Angular will strip them from the string if they are not specified in the context, so I can't even look for non-evaluated tokens in the returned string.
Use a unit test:
var exp = $interpolate('Hello {{name | uppercase}}!');
expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!');
References
AngularJS Source: interpolateSpec.js

Explanation asked about a Javascript / Angular line

In a factory I construct a HTML page. This page can contain a form, so I want to get a handle on the FormController. After some Googling I've got everything working with this line of code (html is all the html in a string in a jquery selector):
html.find("input").eq(0).controller('form');
I understand that:
find(): it is going to find all the input elements;
eq(): I suppose this will select the first found item of the find list;
controller(): this part is unclear. I find it hard to find some documentation about this. What I do know is that you can pass ngModel or form. When passing ngModel you get the FormController of the specified control, thus not the whole form. And when specifying form you get a reference to the whole form.
So, I understand the most of, but I still don't get if controller() is an Angular function or Jquery function and how/when you can use this method.
There is no concept of "controller" in jQuery: controller() is obviously an Angular function. Here is the documentation:
controller(name) - retrieves the controller of the current element or its parent. By default retrieves controller associated with the ngController directive. If name is provided as camelCase directive name, then the controller for this directive will be retrieved (e.g. 'ngModel').
controller() is a method added by Angular to the jQuery object. It returns the Angular controller associated with the element. See the docs including other extra methods here...
http://docs.angularjs.org/api/ng/function/angular.element

Categories

Resources