Something like "with variable" in AngularJS? - angularjs

Sample controller data:
$scope.items = [{ id: 1, name: 'First'}, { id: 2, name: 'Second'}];
Is there something in angular to make the following code work like "with variable"?
<ul>
<li data-ng-repeat="items">{{id}} {{name}}</li>
</ul>
Instead of:
<ul>
<li data-ng-repeat="i in items">{{i.id}} {{i.name}}</li>
</ul>
Please feel free to make a more understandable title/question.

Referring to Angular ngRepeat document, currently only followings expressions are supported.
variable in expression
(key, value) in expression
variable in expression track by tracking_expression
variable in expression as alias_expression
This means, you can't simply use ng-repeat="items" to iterate the collection.
BTW, ng-repeat will create a separate scope for each element and bind variable or (key, value) to the newly created scope. So "with variable" you refer to is not Angular built-in. You need to create a customized directive for this functionality.

My preferred answer would be "don't do this" but failing that, and because it's interesting, here's a proof of concept, assisted by this question and mostly adapted from this blog post:
app.directive('myRepeat', function(){
return {
transclude : 'element',
compile : function(element, attrs, linker){
return function($scope, $element, $attr){
var collectionExpr = attrs.myRepeat;
var parent = $element.parent();
var elements = [];
// $watchCollection is called everytime the collection is modified
$scope.$watchCollection(collectionExpr, 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 = [];
}
for (i = 0; i < collection.length; i++) {
// create a new scope for every element in the collection.
childScope = $scope.$new();
// ***
// This is the bit that makes it behave like a `with`
// statement -- we assign the item's attributes to the
// child scope one by one, rather than simply adding
// the item itself.
angular.forEach(collection[i], function(v, k) {
childScope[k] = v;
});
// ***
linker(childScope, function(clone){
// clone the transcluded element, passing in the new scope.
parent.append(clone); // add to DOM
block = {};
block.el = clone;
block.scope = childScope;
elements.push(block);
});
};
});
}
}
}
});
And then this will do what you want:
app.controller("myController", function($scope, $http) {
$scope.items = [
{a: 123, b: 234},
{a: 321, b: 432}
];
});
With the HTML structure you want:
<div ng-controller="myController">
<ul>
<li my-repeat="items">
{{ a }} {{ b }}
</li>
</ul>
</div>
Notice that given the attributes are copied into the child scopes, rather than referenced, if changes are made to the view, they won't affect the model (ie. the parent items list), severely limiting the usefulness of this directive. You could hack around this with an extra scope.$watch but it'd almost certainly be less fuss to use ng-repeat as it's normally used.

I can't see why other users are telling you that what you want has to be done via a new directive. This is a working snippet.
angular.module("Snippet",[]).controller("List",["$scope",function($scope){
$scope.items = [{ id: 1, name: 'First'}, { id: 2, name: 'Second'}];
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="Snippet" ng-controller="List as list">
<ul>
<!-- Iterating the array -->
<li ng-repeat="item in items">
<!-- Iterating each object of the array -->
<span ng-repeat="(key,value) in item">{{value}} </span>
</li>
</ul>
</body>
Simply, you need to iterate the elements of the array via ng-repeat, then you can do what you want with the object item retrieved. If you want to show its values, for example, as it seems for your question, then a new ng-repeat gets the job done.

Related

ng-repeat is not refreshed when ng-click method called from directive

I am working on creating reusable directive which will be showing composite hierarchical data .
On first page load, Categories like "Server" / "Software"/ "Motherboard" (items array bound to ng-repeat) would be displayed . If user clicks on "Server" then it would show available servers like "Ser1"/"Ser2"/"Ser3".
html :
<div ng-app="myApp" ng-controller="myCtrl" ng-init="init()">
<ul>
<li ng-repeat="item in items">
<div my-dir paramitem="item"></div>
</li>
</ul>
</div>
Now first time Items are loading, but clicking on any item is not refreshing ng-repeat. I have checked ng-click, "subItemClick" in below controller, method and it is being fired. However the items collection is not getting refreshed.
http://plnkr.co/edit/rZk9cbEJU90oupVgcSQt
Controller:
var myApp = angular.module('myApp', []);
myApp.controller('myCtrl', ['$scope', function($scope) {
$scope.init = function() {
$scope.items = [{iname: 'server',subItems: ['ser1', 'ser2','ser3']}
];
};
$scope.subItemClick = function(sb) {
if (sb.subItems.length > 0) {
var zdupitems = [];
for (var i = 0; i < sb.subItems.length; i++) {
zdupitems.push({
iname: sb.subItems[i],
subItems: []
});
}
$scope.items = zdupitems;
}
};
}])
.directive('myDir', function() {
return {
controller: 'myCtrl',
template: "<div><a href=# ng-click='subItemClick(paramitem)'>{{paramitem.iname}}</a></div>",
scope: {
paramitem: '='
}
}
});
I am expecting items like ser1/ser2 to be bound to ng-repeat on clicking "Server" but it is not happening .
Any help?
I think that onClick is screwing up the method's definition of $scope. In that context, the $scope that renders the ngRepeat is actually $scope.$parent (do not use $scope.$parent), and you're creating a new items array on the wrong $scope.
I realize the jsfiddle is probably a dumbed down example of what you're dealing with, but it's the wrong approach either way. If you need to use a global value, you should be getting it from an injected Service so that if one component resets a value that new value is reflected everywhere. Or you could just not put that onClick element in a separate Directive. What's the value in that?

angularjs move element/directive from one list to another without reinitialization

Lets image that we have two lists of elements, where each element itself is a directive.
List 1:
- A
- B
- C
List 2:
- X
- Y
- Z
I want to move element "Y" from list 2 to list 1 without reinitializing the directive. Hence, I want to simply move the contents/directive from list 2 to list 1.
I made a simple example to show what I mean: http://jsfiddle.net/HB7LU/24171/
As you can see from the example, the console prints 'init: Y' again, after moving the element. But I don't want that. I just want to have it moved.
How can I do this without reinitializing the directive?
template.html
<div ng-controller="MyCtrl">
<h2>List 1:</h2>
<ul>
<li ng-repeat="entry in list1">
<span my-directive="entry"></span>
</li>
</ul>
<h2>List 2:</h2>
<ul>
<li ng-repeat="entry in list2">
<span my-directive="entry"></span>
</li>
</ul>
<button ng-click="move()">
Move 'Y' to List 1
</button>
</div>
app.js
var myApp = angular.module('myApp',[]);
myApp.directive('myDirective', function() {
return {
restrict: 'A',
scope: {
myDirective: '='
},
template: '{{myDirective}}',
controller: function($scope) {
console.log('init: ' + $scope.myDirective);
}
};
});
function MyCtrl($scope) {
$scope.list1 = ['A', 'B', 'C'];
$scope.list2 = ['X', 'Y', 'Z'];
$scope.move = function() {
var entries = $scope.list2.splice(1,1);
$scope.list1.push(entries[0]);
};
}
Since your directive is used inside and ng-repeat, every item addition will create a new scope and thus directive controller will be initialized every time. You can however do it the other way around, i.e. bind your list to the directive and have ng-repeat within the template. See the Updated Fiddle. Let me know if that works for you.

How do I control the execution flow so the ng-repeat completes before the parent directive?

I'd like extend-item directive to run after ng-repeat is rendered in DOM.
In the example below the ^ is added only to the static ul elements, since the dynamic ul elements are not yet generated by ng-repeat yet. See code in Plunker
How do I control the execution flow so the ng-repeat completes before the parent directive?
<ul id="nav" ng-controller="AppController as vm" extend-item>
<li ng-repeat="group in vm.groups">
<span>{{group.name}}</span>
<ul>
<li ng-repeat="item in vm.items">
<span>{{item.name}}</span>
</li>
</ul>
</li>
<!-- Static -->
<li>
<span>Static Group 10</span>
<ul>
<li><span>Static 11</span>
</li>
</ul>
</li>
</ul>
(function() {
'use strict';
angular.module('app')
.directive('extendItem', extendItem);
extendItem.$inject = ['$compile'];
/* #ngInject */
function extendItem($compile) {
var directive = {
restrict: 'A',
link: link
};
return directive;
function link(scope, element, attrs, ctrls) {
var $lists = element.find('ul').parent('li');
$lists.append('<i>^</i>');
console.log('extendItem - $lists length: ' + $lists.length);
}
}
})();
(function() {
'use strict';
angular.module('app')
.controller('AppController', AppController);
AppController.$inject = ['$scope'];
/* #ngInject */
function AppController($scope) {
var vm = this;
vm.groups = [{
id: 1,
name: 'Group 1'
}, {
id: 2,
name: 'Group 2'
}];
vm.items = [{
id: 1,
name: 'Item 1'
}, {
id: 2,
name: 'Item 2'
}];
activate();
///////////////////////////////
function activate() {
console.log('AppController');
}
}
})();
The easiest way to do that is use $timeout before executing your code: this will wait for the current digest cycle to be done and the DOM to be refreshed.
http://plnkr.co/edit/KVlzZHHwa3ECxteJBvD8?p=preview
function link(scope, element, attrs, ctrls) {
$timeout(function() {
var $lists = element.find('ul').parent('li');
$lists.append('<i>^</i>');
console.log('extendItem - $lists length: ' + $lists.length);
});
}
The problem with $timeout is that it will only work the first time your DOM is evaluated and relies on your model never changing. If your model changes (i.e, you add groups), the new groups will not have the ^ appended.
Here is a plunk that demonstrates this problem.
I would instead use css (if you are using a modern browser)
<style>
ul[extend-item] > li::after {
content: '^'
}
</style>
In general, any time you find yourself wanting to modify the DOM after ng-repeat "completes" or "is rendered", you are thinking about it "wrong" in the Angular sense. ng-repeat never "completes". It is always watching the model and can potentially add or remove elements based on changes in the model. Using $timeout does not guarantee that the DOM will not be modified at some time in the future.
You might want to also investigate using ng-repeat-start and ng-repeat-end for this particular use case, especially if the intent is to add more than just simple content after each list element.
From the docs:
To repeat a series of elements instead of just one parent element,
ngRepeat (as well as other ng directives) supports extending the range
of the repeater by defining explicit start and end points by using
ng-repeat-start and ng-repeat-end respectively. The ng-repeat-start
directive works the same as ng-repeat, but will repeat all the HTML
code (including the tag it's defined on) up to and including the
ending HTML tag where ng-repeat-end is placed.

Read existing data from DOM into model, ng-model for static data?

I'm currently playing around with rewriting the functionality of an existing page using Angular. The gist of it is that I have a plain HTML page with a list of stuff, like this:
<ul>
<li class="item">
<h1>Foo</h1>
<ul class="categories">
<li class="category">Bar</li>
</ul>
</li>
...
</ul>
This is augmented by some Javascript which parses this data once and adds a dynamic category filter menu to the page. I.e. it extracts all li.category elements and displays a menu with them, and clicking on one of these categories filters the item list to display only items with the chosen category.
I've replicated the basics of that in Angular with a lot less code than I had before. However, I'm still doing a lot of jQuery traversing of the .item elements to build that initial list of categories:
myApp.controller('MyController', function ($scope) {
$scope.categories = [];
angular.element('.item').each(function () {
angular.element(this).find('.categories .category').each(function () {
var category = this.textContent;
for (var i = 0, length = $scope.categories.length; i < length; i++) {
if ($scope.categories[i].name == category) {
$scope.categories[i].count++;
$scope.categories[i].items.push(this);
return;
}
}
$scope.categories.push({ name : category, count : 1, items : [this] });
});
});
});
This does not seem to be in the spirit of Angular, and I'd like to replace it with something like:
<ul>
<li class="item" ng-item>
<h1>Foo</h1>
<ul class="categories">
<li class="category" ng-category>Bar</li>
</ul>
</li>
...
</ul>
A directive should then be able to parse all ng-item/ng-category elements and add them to the model/scope once. Something like ng-model, but for static data.
I have virtually no experience with Angular, how can I accomplish this; or shouldn't I want to do something entirely different in the first place?
For creating your own ng-item and ng-category directives, I suggest that you can go through Creating Custom Directives part in Angular Offical Develop Guide:
http://docs.angularjs.org/guide/directive
It will tell you how to begin creating your directive from add directive to module like this:
.directive('myCustomer', function() {
return {
template: 'Name: {{customer.name}} Address: {{customer.address}}'
};
});
Edit:
This two is also useful tutorial:
http://www.ng-newsletter.com/posts/directives.html
http://www.befundoo.com/university/tutorials/angularjs-directives-tutorial/
Edit2:
To answer your comment:
Do you have a concrete sample of how I'd write a directive that reads data from its element and modifies the controller's scope?
I thought that it has clear explanation in Angular Official Guide:
.directive('myCustomer', function() {
return {
restrict: 'E',
scope: {
customerInfo: '=info'
},
templateUrl: 'my-customer-iso.html'
};
});
In this example:
restrict: 'E' : directive name match element name
So directive look like this:
<my-customer></my-customer>
scope: { customerInfo: '=info'}
<my-customer info="myInfo"></my-customer>
this will bind myInfo to scope just like this expression:
$scope.customerInfo = myInfo;
this is a concrete sample of how to read data from its element and modify the controller's scope.

editable with ngrepeat: automatically editing the latest added item

I need to add new items to a collection, that gets rendered with ngrepeat and using xeditable make it automatically editable.
BTW, I'm using the "manual trigger" method for xeditable.
Here it is the HTML
<h4>Angular-xeditable demo</h4>
<div ng-app="app" ng-controller="Ctrl" style="margin: 50px">
<div class="btn btn-default" ng-click="addNew()">+</div>
<ul>
<li ng-repeat="item in array | orderBy:'-value'">
{{ item.field }}
<i ng-show="!itemForm.$visible" ng-click="itemForm.$show()">edit</i>
</li>
</ul>
</div>
and here the controller:
var app = angular.module("app", ["xeditable"]);
app.run(function(editableOptions) {
editableOptions.theme = 'bs3';
});
app.controller('Ctrl', function($scope, $filter) {
$scope.array = [
{value: 1, field: 'status1'},
{value: 2, field: 'status2'},
{value: 3, field: 'status3'},
{value: 4, field: 'status4'}
];
$scope.addNew = function(){
$scope.array.push({value:$scope.array.length+1, field: 'enter text here'});
//MAKE IT EDITABLE????????
}
});
Take a look to the issue in this fiddle: http://jsfiddle.net/dpamio/hD5Kh/1/
Here is a updated fiddle that works. Because of how the directive was written, and how ng-repeat works, it required an extremely hacky solution...
app.controller('Ctrl', function($scope, $filter, $timeout) {
$scope.itemForms = {};
$scope.addNew = function(){
$scope.array.push({value:$scope.array.length+1, field: 'enter text here'});
// Use timeout to force evaluation after the element has rendered
// ensuring that our assignment expression has run
$timeout(function() {
$scope.itemForms[0].$show(); // last index since we sort descending, so the 0 index is always the newest
})
}
Background on how ng-repeat works: ng-repeat will create a new child scope for each element that is repeated. The directive assigns a variable on that scope using the string passed into e-form for its name (in this case itemForm). If it was smarter, it'd allow for expression evaluation for assignment. (Then we could assign it to the parent scope, and access it in the controller, but that's a different matter).
Since we don't have any way to access this child scope outside of the directive, we do something very bad. We use the mustache expression in a span of display none to assign the itemForm variable to the parent scope so that we can use it later. Then inside our controller we use the look up value to call the itemForm.$show() method that we expect.
Abstracting that bit of nastyness into an angular directive, we could write the following:
.directive('assignFromChild', function($parse) {
return {
restrict: 'A',
link: function(scope, el, attrs) {
scope.$watch(function() { return $parse(attrs.assignFromChild)(scope); }, function(val) {
$parse('$parent.' + attrs.toParent).assign(scope, val);
})
}
};
});
Allowing our HTML to go back down to:
<ul>
<li ng-repeat="item in array | orderBy:'-value'" assign-from-child="itemForm" to-parent="itemForms[{{$index}}]">
{{ item.field }}
<i ng-show="!itemForm.$visible" ng-click="itemForm.$show()">edit</i>
</li>
</ul>
Here is a fiddle with my final solution
I found a very simple solution using ng-init="itemForm.$show()", that will activate the xeditable form when the new item is inserted.
Here's the updated jsFiddle answering the question: http://jsfiddle.net/hD5Kh/15/

Resources