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

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/

Related

What makes an Angular.JS directive require curly braces around it?

I've been doing a codeschool tutorial on Angular.JS and one section in particular confused me. Here's the code snippet I'm referring to:
<section class="tab" ng-controller="TabController as tabs">
<ul class="nav nav-pills">
<li ng-class="{active:tabs.isSet(1)}">
<a href ng-click="tabs.setTab(1)">Description</a></li>
<li ng-class="{active:tabs.isSet(2)}">
<a href ng-click="tabs.setTab(2)">Specs</a></li>
<li ng-class="{active:tabs.isSet(3)}">
<a href ng-click="tabs.setTab(3)">Reviews</a></li>
</ul>
</section>
This line in particular needs curly braces in the directive to be correct by CodeSchool standards:
<li ng-class="{active:tabs.isSet(1)}">
My question is, how come this other line's directive comes through correctly without the need of curly braces:
<a href ng-click="tabs.setTab(1)">Description</a></li>
To me, it would seem that since I'm accessing the function tabs.setTab() I would need to wrap it in the same way as before. Can anyone explain why this is the case?
Here's the TabController JS code by the way for reference:
app.controller('TabController', function(){
this.tab = 1;
this.setTab = function(newValue){
this.tab = newValue;
};
this.isSet = function(tabName){
return this.tab === tabName;
};
});
First of all, you should understand that these conventions are completely arbitrary. They were decided by the angular authors. They could have easily used some other character, like an exclamation point for example...
ng-class="!active: true, otherClass: false!"
These values are not code. They are just strings (plain text). There is some javascript in angular that parses these strings and makes sense of them. It loops over every character and looks out for certain key characters, like curly braces.
That being said, there is a reason they chose the characters they did. They are trying to mimic javascript in html. The values passed to ng-click represent a function call in javascript. Not coincidentally, this is how you call a function in javascript...
tabs.setTab(1)
The value passed to ng-class, however, is not mimicking a function call. It is mimicking an object. This is how you declare an object in javascript...
{ active: tabs.isSet(1) }
This represents an object with a key of active and a value of tabs.isSet(1), which evaluates to a boolean telling angular whether that class should be applied
It depends on the directive you're using. If you're just trying to set a boolean, a function call or a single boolean controller parameter might suffice. Other directives might be more complicated and have different parameter requirements. You'll find out when you start making your own directives.
In the case of ngClass, it is because you can set multiple classes at once, conditionally. There are several ways of doing this, but the syntax is fairly clear (documentation).
This is an example from the docs:
<p ng-class="{strike: deleted, bold: important, 'has-error': error}">Map Syntax Example</p>
Here's the related plunkr: https://plnkr.co/edit/?p=preview

How to use Angular structural directive with multiple inputs

I want to implement something similar with angular-permisssion. And with requirement to control the element's existance, I need to use angular structural directive.
At the beginning, i think such syntax would work:
<h2 *permissionIf [permissionIfExcept]="'Read'">Except</h2>
However, it doesn't work that way.
Moreover, the offical guide only teach you how to write custom structural directive with single input.
With multi-inputs, some third-party tutorials involve a bit. But that's using the angular template micro-syntax to achieve data binding. Then one problem occurs:
template syntax doesn't support pure key-value inputs:
<h2 *permissionIf="except: map.except;only: 'test'">Except</h2>
It expands into this(which is illegal):
<h2 template="permissionIf except: map.except;only: 'test'">Except</h2>
A stupid temporary solution is add a useless variable declaration.
<h2 *permissionIf="let i;except: map.except;only: 'test'">Except</h2>
Another inconvenient way is to use template element to wrap the code.
<template permissionIf [permissionIfExcept]="'Read'">
<h2>Except</h2>
</template>
The above all are not accepetable enough. But I can't find a bette way to resolve it.
Hope some guys can give some suggestion:).
The input names need all to be prefixed with the selector of the directive, followed by the input name capitalized (i.e. permissionIfExcept). Example:
#Directive({
selector: '[permissionIf]'
})
export class PermissionIfDirective implements AfterContentInit {
private _permissionIf:string[];
#Input()
set permissionIf(value: string[]) {
this._permissionIf=value;
console.log('permissionIf: ', value);
}
private _except:string;
#Input()
set permissionIfExcept(value: string) {
this._except = value;
console.log('except: ', value);
}
}
To use them with the '*' syntax:
<div *permissionIf="permissions;except:'Read'"></div>
Note here you're using the name following the prefix uncapitalized (i.e. except). Also note the : in the assignment.
The explicit syntax (using template) would look like this:
<template [permissionIf]="permissions" [permissionIfExcept]="'Read'">
</div></div>
</template>
but with <ng-container> it could look like
<ng-container *permissionIf="permissions;except:'Read'">
<div></div>
</ng-container>
Plunker example
See also the source of NgFor as an example.
#Günter Zöchbauer answer is almost correct.
Actually right now to make his answer working you need to explicitly rename the secondary #Input name.
So it should be:
#Input("permissionIfExcept")
set permissionIfExcept(value: string) {
this._except = value;
console.log('except: ', value);
}
Then one problem occurs: template syntax doesn't support pure key-value inputs:
True
A stupid temporary solution is add a useless variable declaration.
I think you are using this in a way it was not meant to be.
From the docs:
The microsyntax parser title-cases all directives and prefixes them with the directive's attribute name, such as ngFor. For example, the ngFor input properties, of and trackBy, become ngForOf and ngForTrackBy, respectively. That's how the directive learns that the list is heroes and the track-by function is trackById.
https://angular.io/guide/structural-directives#microsyntax-examples
Bottom line is in the context of your question, the microsyntax accepts "expression", followed by optional "keyed expression"s and I'm afraid those are your only options.
One could of course pass an object as the first expression—similar to ngIf—, the difference being you can teach your directive how to evaluate the expression:
*permissionIf="{ only: 'whatever', except: ['things', 'stuff'] }"

`ng-model` of AngularStrap `bs-typeahead` not available in scope?

I am running into an issue with the value of ng-model, applied to an element using AngularStrap's bs-typeahead, is not accessible within scope. It is however viable from a {{ var }} within the HTML.
I have the following HTML:
<input type="text" placeholder="add a destination" ng-options="item as item for item in modelTypeahead" ng-model="selectedDestination" bs-typeahead data-template="templates/SrcDstTypeaheadTemplate.html">
I initialize the variable in my controller:
$scope.selectedDestination = "";
Placing a {{ selectedDestination }} elsewhere within the HTML works as expected.
However, when I do a console.log($scope.selectedDestination); within my controller it comes out as an empty string.
If I update my initialization to be something, for example:
$scope.selectedDestination = "abc123";
... both the <input> and the {{ selectedDestination }} update appropriately. My console.log will also spit out the set value. However, if I update the typeahead the {{ selectDestination }} will update but my console.log will spit out 'abc123' still.
Is there a scope issue that I am missing? I don't understand how {{ selectedDestination }} is putting out the correct string but the console.log is putting out something different. It would almost seem my binding is one-way, but AngularStrap's bs-typeahead should be two-way (per all the examples).
Where are you doing console.log? You would have to make sure that the value has changed before you do that for the value to show up, you could do:
$scope.watch('selectedDestination', function() {
console.log($scope.selectedDestination);
});
I'm not sure if you're still running into any issues with this, but I found a solution to the exact same problem when I ran into it just recently. I'm almost positive that its a scope issue or apply failure within the AngularStrap stuff, but I wouldn't know where to start looking.
Really, I'm not educated enough to give you the exact reasons that this works, but this is what you do:
(1) You change the variable to an object.
when you put the model and watch on an object instead of a top layer variable, it works better through layers of directives. Don't ask me why....
(2) Use a deep watch on the object you just created.
When you change it to an object, you need to use a deep watch on the variable or the $apply and $digest won't pick up any changes. This is because by default the value will be checked for "reference" equality instead of "value" equality. This breaks because the object's "reference" doesn't change, only its values. But be careful using this deep comparison because the extra effort can cause a lot of overhead.
Here's an example in use with AngularStrap's typeahead:
$scope.selectedDestination = {};
~~~
<input type="text" placeholder="add a destination" ng-options="item as item for item in modelTypeahead" ng-model="selectedDestination.destination" bs-typeahead data-template="templates/SrcDstTypeaheadTemplate.html">
~~~
$scope.$watch('selectedDestination', function(value) {
console.log('selectedDestination', $scope.subComponent);
}, true); //here we need to tell the watch to do a deep watch
EDIT I've traced my issues back to a few things, but part of it was the $render function. I'll keep looking into it. Good luck!

How do I get the current child-scope of a ngRepeated element in AngularJS?

This seems like a simple question, but I've been googling around for a while and can't seem to find it.
In my JS I have something called parseTags(book) that takes a JSON comma-separated list of tags (book.tags) and parses it into an array:
$scope.parseTags = function(book){
book.tags = book.tags.split(',');
};
In my HTML I have something like this:
<div ng-repeat="book in books" ng-init="parseTags(book)">{{book.title}}</div>
Is there a way just to get the child scope from within the $scope.parseTags function? Instead of passing in book each time?
Something like:
$scope.parseTags = function($childScope){
$childScope.tags = $childScope.tags.split(',');
}
<div ng-repeat="book in books" ng-init="parseTags()">{{book.title}}</div>
parseTags function is executed in context of the current child scope. So parseTags can also be written as:
$scope.parseTags = function() {
this.book.tags = this.book.tags.split(',');
};
Demo: http://plnkr.co/edit/DUkDVOMjjj0khh5KCYo7?p=preview
you should only use ng-init for special cases when you need to use a property of the ng-repeat, I think you will better doing this kind of functionality in the controller. Unless there is a really specific reason you can't do that. I haven't seen your use of tags in the html, but looks like the kind of functionality a filter would do.
From angularjs docs:
The only appropriate use of ngInit is for aliasing special properties
of ngRepeat, as seen in the demo below. Besides this case, you should
use controllers rather than ngInit to initialize values on a scope.
source: angularjs docs

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