AngularJS Create new scope in template? - javascript

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
}
}
});

Related

AngularJS Directives: Using return in the `pre` function

I've been trying to track down a bug in ngGridv2.0.14 where the "gridID" class is not being set on my ngGrid classed element, even though the dynamic stylesheet is being appended to my document header, and I found something kind of wierd...
This bug causes the columns to appear "on top of" each other...
In ng-grid-2.0.14.debug.js there is a directive created called ngGrid (of course!). The directive uses the suggested "directive definition object" form. They define an inherited scope by defining scope:true and they declare a compile function which returns an object structure containing ONLY the function pre and DOES NOT declare the function post.
What is weird is that there is a return statement within this pre function, and I cannot find documentation about return statements within a pre function. The function ends up in Angular's "processQueue", which I believe could be Deffered.$$state.pending (I am tracing the call stack to find this, so I am not sure). During a $digest loop, angular tests these array indexes as functions and invokes them.
The ngGrid function that is being called, which can be found at ng-grid-2.0.14.debug.js line 3318, does a lot, and most of these things are working, such as $destroy cleanup logic, $complie($scope) of the gridTemplate, event listener establishing, and, most importantly, the addition of two very specific classes to our directive's iElement which appears to still be "in scope." This is the part that is failing.
They are adding a class to the iElement by:
iElement.addClass("ngGrid").addClass(grid.gridId.toString());
Sometimes this works, sometimes this doesn't...
Sometimes the element picks up the classes "ng-scope ngGrid ng[0-9]{13}" (e.g: ng-scope ngGrid ng1453913625179), and sometimes it does not, even when I have stepped through the function and watched the line of code that adds the classes get executed.
Further Investigation!: If I place an ng-if="true" on the parent node of my "ngGrid element" (the element that instanciates the ngGrid directive by attribute), then eveything works!
My Question: Does appending classes to an element within a $digest loop have issues or any reason why this would work sometimes and not all the time? Also, can you find documentation on setting return values for pre? I couldn't find anything about the return values here. Finally, why does setting ng-if="true" on the parent of iElement fix the issue of the class names not being appended to iElement's class list? Thanks!

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);
});
}
};
})

AngularJS Directive - re-run link function on scope parameter change

I have a directive that builds a set of nested <ul> elements representing a folder structure. I used the link function to create the new DOM elements and append them to the directive instance element:
function link(scope, iElement, iAttr) {
var rootElement = buildChildElement(scope.tree);
iElement.append(rootElement);
}
Elements within the <ul> tree are wired with jQueryUI's drag/drop interactions that call a function on the Controller housing the directive to update the scope parameter based on the drag & drop events.
I would like the <ul> tree to automatically update when there is a change to the scope parameter. I have tried a watch function within my link function:
scope.$watch('tree', function(newTree, oldTree) {
var newRoot = buildChildElement(newTree);
iElement.contents().remove();
iElement.append(newRoot);
}
This works to a certain extent, but the call to remove() fires off the $watch() method a second time which ends up reverting my Controller changes. If I comment out the remove(), I can see that a new <ul> tree is written that properly reflects the changes to the parameter made in the Controller.
The double firing $watch() makes me think I'm going about this wrong. Without it, my parameter is properly updating but my <ul> doesn't update (the dropped element stays where it was dropped).
What's the correct way to make sure your directive is refreshed on a change in one of the scope parameters?
Should I be using the compile function and building the <ul> tree based on the attributes array instead of using the link function?
Your approach is very jQuery-style. I think you'll find that you're working against Angular in this case. sh0ber is right with his/her question; you should post a demo or something, or at least some sample code so you can have an effective answer.
I think you want to make a recursive tree directive. Check out this SO answer for some interesting approaches to this. The main idea is that watch is unnecessary. Simply change the object and Angular will take care of the rest. The most efficient thing is to change the specific node objects directly rather than replacing the whole object, but that will work too.
scope.$watch('tree', function(newTree, oldTree) {
var newRoot = buildChildElement(newTree);
iElement.contents().remove();
iElement.append(newRoot);
},**true**)
I think you can have a try and reference the watch API for more information
Here is another artical
http://www.bennadel.com/blog/2566-scope-watch-vs-watchcollection-in-angularjs.htm

Can I avoid the object variable name in ng-repeat loop?

When defining an ng-repeat directive to iterate over an array, the syntax specifies ng-repeat="friend in friends", and then within the template you use the interoplation operator like so {{friend.name}}.
Is it possible to have the properties assigned to the current item scope, rather than a variable in it? So I could call just {{name}} instead of {{friend.name}}?
The reason being is that my directive is being used in the scope of two different templates - for example I might have a directive "userActions" that is used in both a repeater, and also inside an unrelated template where {{friend.name}} doesn't make sense. I would like to avoid artificially manufacturing the friend object where it doesn't have a semantic meaning.
My use case is something like this:
I have a grid that renders blocks of various types. Some psuedo code:
<div ng-repeat="block in blocks">
< cat block />
< friend block >
<userActions directive />
</ friend block >
< guitar block />
.... more blocks
</div>
I also have a friend page, that contains the exact same user actions:
..fragment of friend page..
<element ng-control="friend">
<userActions directive />
</element>
Now, if I want to user a property of the block inside the repeater, the syntax is {{block.name}}. So the template for userActions contains this.
However, once I use this template in the friend page, I must create {{block.name}} inside the scope of the friend controller. This does not make sense though, because the block only exists in the context of the block grid. I shouldn't have to create this block.
What I want to be able to do, is just to call {{name}} from within the userActions directive template, since both the block scope and the controller contain it. I don't want to create a block object, then artificially set block.name in each scope where I want to use the userActions directive.
Here's a jsFiddle to illustrate the cause
I've decided to combine the informative answers of Mathew Berg and ganaraj with my newfound knowledge to create a helpful answer to this.
The short answer is You really don't want to do that.
The longer answer is this:
When using ng-repeat="block in blocks" , a new scope is created for each block element, and the properties of every block object are create in scope.block of each block. This is a good thing, because this means all properties can be accessed by reference, updated or $watched.
If ng-repeat wouldn't have done that, and all properties would just be slapped unto the block's scope, then all primitives in block (strings, ints, etc) would just be copied from the block object to the block scope object. A change in one will not reflect on the other, and that's bad. More info on that here.
Ok so now that we've decided that's a good thing rather than a bad thing, how do we overcome the semantic issue? I've decided to use the friendData object container as the object on the scope of my directive, and so the directive expects the friend-data attribute to hold the relevant properties
angular.module('myApp',[])
.directive("lookActions", function(){
return {
restrict: 'E',
template: "<input value='Kill -{{ friendData.name }}-' type='button'>",
scope : {
friendData : '='
}
}
});
This way I can assign this object regardless of which context I'm calling my directive template.
Given these controller contexts:
function gridCtrl($scope) {
$scope.blocks = [{ type: "cat", name: "mitzi"},{ type: "friend", name: "dave"},{ type: "guitar", name: "parker"}];
}
function friendCtrl($scope) {
$scope.data={
name: "dave"
}
}
How to call the directive -
Within an ng-repeat:
<div class="block" ng-repeat="block in blocks" >
<look-actions friend-data="block" />
</div>
Or in a difference context:
<div ng-controller="friendCtrl">
<look-actions friend-data="data" />
</div>
Here's the solution Fiddle
Thanks for all the help!
It all depends on how you structure your directive. It's hard to tell without a fiddle/plunkr what your code looks like so I'm taking a stab in the dark here. Right now I think what you're trying to say is that in the context of where you're using your directive friend.name does not make sense. Perhaps something more generic like person.name might be more appropriate. In that case you can do the following so that you pass in to the directive what you want the person to be associated with:
Html
<div data-ng-repeat="friend in friends">
{{ friend.name }}
<div class="myDirective" data-person="friend"></div>
</div>
javascript
.directive("myDirective", function(){
return {
restrict: 'C',
scope: {
person: "=person"
},
template: "<div>{{ person.name }}</div>"
}
});
jsfiddle: http://jsfiddle.net/5aVLf/1/

Categories

Resources