angularjs - ngRepeat with ngInit - ngRepeat doesn't refresh rendered value - angularjs

I have array which is displayed using ngRepeater but with this directive I'm using ngInit directive which execute function which should return object to be displayed. Everything works perfectly but when I added "New" button where I add new value to array then function "SetPreview" is executed only once I think function should be executed depending from the amount of array value. How can I do that?
UI:
<body ng-app>
<ul ng-controller="PhoneListCtrl">
<button ng-click="add()">Add</button>
<li ng-repeat="phone in phones" ng-init="displayedQuestion=SetPreview(phone);">
{{displayedQuestion.name}}
<p>{{displayedQuestion.snippet}}</p>
</li>
</ul>
</body>
Controller:
function PhoneListCtrl($scope) {
$scope.phones = [
{"name": "Nexus S",
"snippet": "Fast just got faster with Nexus S."},
{"name": "Motorola XOOM™ with Wi-Fi",
"snippet": "The Next, Next Generation tablet."},
{"name": "MOTOROLA XOOM™",
"snippet": "The Next, Next Generation tablet."}
];
$scope.add = function(){
this.phones.push({name:'1', snippet:'n'});
};
$scope.SetPreview = function(phone)
{
//here logic which can take object from diffrent location
console.log(phone);
return phone;
};
}
Sample is here - jsfiddle
Edit:
Here is more complicated sample: -> Now collection of Phones is empty, when you click Add button new item is added and is set as editable(you can change value in text field) and when ngRender is executed SetPreview function returns editable object(it’s work like preview). Now try click Add button again and as you can see the editable value of first item is still presented to user, but I want to refresh entire ng-repeater.

You are running into an Angular performance feature.
Essentially Angular can see that the element in the array ('A' for example) is the same object reference, so it doesn't call ng-init again. This is efficient. Even if you concatenated an old list into a new list, Angular would see that it it the same reference.
If instead you create a new object with the same values as the old object, it has a different reference and Angular re-inits it:
Bad example that does what you are looking for: http://jsfiddle.net/fqnKt/37/
$scope.add = function(item) {
var newItems = [];
angular.forEach($scope.items, function(obj){
this.push({val:obj.val});
},newItems)
newItems.push({val:item})
$scope.items = newItems;
};
I don't recommend the approach taken in the fiddle, but rather you should find a different method than ng-init to trigger your code.

I've found out that you can just replace ng-init with angular expression {{}} in a hidden block:
<body ng-app>
<ul ng-controller="PhoneListCtrl">
<button ng-click="add()">Add</button>
<li ng-repeat="phone in phones">
<span style="display: none">{{displayedQuestion=SetPreview(phone)}}</span>
{{displayedQuestion.name}}
<p>{{displayedQuestion.snippet}}</p>
</li>
</ul>
</body>

If you have an ngInit in an element, and an ngRepeat in a descendant, the ngInit will only run once.
To fix that, add an ngRepeat to the element with the ngInit, with an expression that repeats once and depends upon the same array (and other dependencies).
For example:
ng-repeat="_ in [ products ]" ng-init="total = 0"

Related

Delimiting some sibling nodes without additional element

What is the equivalent angularjs template for the following handlebar template? Is there any way to achieve same result without wrapping the if block with another tag?
(foo is false)
<ul>
<li>a</li>
{{if foo}}
<li>b</li>
…
<li>c</li>
{{/if}}
<li>d</li>
</ul>
The rendered template should be exactly:
<ul>
<li>a</li>
<li>d</li>
</ul>
ng-if with one time binding(if you are in version 1.3.x else resort to some other libraries like bindonce to avoid any unnecessary watches) might be more appropriate for you. But ideally it is clearly unclear because you can solve this with many ways in angular. It does not even has to get to the view, you could just filter it out from the controller itself while setting up the view model which is used to repeat (ng-repeat) the lis. ng-show can also be used if you are trying to show and hide them. Difference between ng-if and ng-show/ng-hide is that ng-if removes the element completely from dom (and it cannot be animated with nganimate). ng-show just sets the css property display:none if condition set is false.
<ul>
<li>a</li>
<li ng-if="::foo">b</li><!-- Using :: for one time binding V1.3.x so no more watchers -->
<li ng-if="::foo">c</li>
<li>d</li>
</ul>
Update based on the comment that OP is looking for "a block statement to show/hide a bunch of elements together without adding a container tag".
Angular is not just a templating library like handlebars. So first thing before providing any specific answer is to recommend to learn how angular works. It is much more than a templating engine, it binds data to DOM that is already rendered and view is more of a reflection of the view model/model built from the controller. So in your case, as i explained earlier you just have to filter out the data based on a specific condition. Take a look at ng-repeat, event DOM filters that can be used with ng-repeat. So in short looking for a a block statement to show/hide a bunch of elements together without adding a container tag in angular (just what you would in handlebars) is thinking in wrong direction in my opinion. A possible solution for you can as well just be to identify when foo becomes true do not event provide those items (to be filtered out) to be rendered to view (or works case use filters in the view). And adding a block statement can just result in an invalid html in your case and browser will just strip it off before even angular has a chance to process it (unlike handlerbars where you transform your template to html before even rendering).
Here is one possible, better way (Using view filters are bad if filtering is one time, if it is just one time do the filtering in the controller) to do this in my opinion.
angular.module('app', [])
.controller('ctrl', function($scope) {
$scope.items = [{
name: 'a',
hideWhenFoo: false
}, {
name: 'b',
hideWhenFoo: false
}, {
name: 'c',
hideWhenFoo: true
}, {
name: 'd',
hideWhenFoo: true
}, {
name: 'e',
hideWhenFoo: true
}, {
name: 'f',
hideWhenFoo: false
}, {
name: 'g',
hideWhenFoo: false
}];
$scope.foo = true; // due to some condition
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<ul>
<li ng-repeat="item in items | filter:{hideWhenFoo:!foo}">{{item.name}}</li>
</ul>
</div>
The following works, similar to ng-repeat-start and ng-repeat-end. However, I did not find it on the docs.
<ul>
<li>a</li>
<li ng-if-start='foo'>b</li>
…
<li ng-if-end>c</li>
<li>d</li>
</ul>

Conditionally apply hasDropdown directive on ng-repeated element

I'm working on a project where I use both angularJS and foundation, so I'm making use of the Angular Foundation project to get all the javascript parts of foundation working. I just upgraded from 0.2.2 to 0.3.1, causing a problem in the top bar directive.
Before, I could use a class has-dropdown to indicate a "top-bar" menu item that has a dropdown in it. Since the menu items are taken from a list and only some have an actual dropdown, I would use the following code:
<li ng-repeat="item in ctrl.items" class="{{item.subItems.length > 0 ? 'has-dropdown' : ''}}">
However, the latest version requires an attribute of has-dropdown instead of the class. I tried several solutions to include this attribute conditionally, but none seem to work:
<li ng-repeat="item in ctrl.items" has-dropdown="{{item.subItems.length > 0}}">
This gives me a true or false value, but in both cases the directive is actually active. Same goes for using ng-attr-has-dropdown.
this answer uses a method of conditionally applying one or the other element, one with and one without the directive attribute. That doesn't work if the same element is the one holding the ng-repeat so i can't think of any way to make that work for my code example.
this answer I do not understand. Is this applicable to me? If so, roughly how would this work? Due to the setup of the project I've written a couple of controllers and services so far but I have hardly any experience with custom directives so far.
So in short, is this possible, and how?
As per this answer, from Angular>=1.3 you can use ngAttr to achieve this (docs):
If any expression in the interpolated string results in undefined, the
attribute is removed and not added to the element.
So, for example:
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
angular.module('app', []).controller('testCtrl', ['$scope',
function ($scope) {
$scope.ctrl = {
items: [{
subItems: [1,2,3,4], name: 'Item 1'
},{
subItems: [], name: 'Item 2'
},{
subItems: [1,2,3,4], name: 'Item 3'
}]
};
}
]);
<div ng-app="app">
<ul ng-controller="testCtrl">
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
{{item.name}}
</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
Ok, I made a directive. All <li> will need an initial attr of:
is-drop-down="{{item.subItems.length > 0}}"
Then the directive checks that value and for somereason its returning true as a string. Perhaps some onc can shed some light on that
app.directive('isDropDown', function () {
return {
link: function (scope, el, attrs) {
if (attrs.isDropDown == 'true')
{
return el.attr('has-dropdown', true); //true or whatever this value needs to be
}
}
};
});
http://jsfiddle.net/1qyxrcd3/
If you inspect test2 you will see it has a has-dropdown attribute. There is probably a cleaner solution, but this is all I know. I'm still new to angular.
edit I noticed a couple extra commas in my example json data..take note, still works, but they shouldn't be there.

$scope var not propagating in child

In a list of items, clicking an item opens up an input field using ng-show="showInput=true".
<div ng-app="myApp" ng-controller="Ctrl">
<li ng-click="showInput=true" ng-repeat="label in labels">{{label}} - ---> show input = {{showInput}}
<form ng-show="showInput" >
<input type=text value={{label}}><button ng-click="saveDate()">save</button>
</form>
</li>
</div>
However, when clicking on save, setting showInput=false the form is not hiding:
angular.module('myApp', [])
.controller('Ctrl', ['$scope', function($scope) {
$scope.labels=["click a", "click b", "click c", "click d", "click e"];
$scope.showInput = false;
$scope.saveData = function(){
$scope.showInput = false;
}
}]);
I suspect this is a parent / child scope issue. Can anyone point out how to make this work?
Fiddle: http://jsfiddle.net/supercobra/PUZzZ/
You have a few bugs here.
In your HTML you should write saveData() (not saveDate()).
When you click any element inside your li (including your button), it will set your showInput at true.
You are dealing with a pure JavaScript object within the scope. There is a question specificlly asking what to do with this at an AngularJS Meetup you can see here. The best solution seems to use an object so the child and the parent use the same referenced object. Here is how I've done it (using a key system instead of the label would be safer tho)
Look at this fiddle for my solution.
<div ng-app="myApp" ng-controller="Ctrl">
<li ng-repeat="label in labels">
<span ng-click="showInput[label] = true">{{label}}</span> - ---> show input = {{showInput}}
<form ng-show="showInput[label]" >
<input type=text value={{label}}><button ng-click="saveData(label)">save</button>
</form>
</li>
</div>
angular.module('myApp', [])
.controller('Ctrl', ['$scope', function($scope) {
$scope.labels=["click a", "click b", "click c", "click d", "click e"];
$scope.showInput = {};
$scope.saveData = function(label){
$scope.showInput[label] = false;
}
}]);
This work perfectly. The problem is if you use a $scope variable inside a child, the parent will not be able to access it when you save.
The problem is indeed that ng-repeat creates its own scope and that you override your showInput.
What I usually do in this situation is to keep track of those forms which are currently shown and implement a toggle like method, like shown in this fiddle. This keeps track of the opened form within the controller and not the $scope object, which only provides methods (to all child scopes, such as that of ng-repeat) to access the private information.
You also have a typo in the call to saveData, but that is not the problem.
Yo have given <button ng-click="saveDate()"> in your view and in your controller you call the function as $scope.saveData. Typo error. change $scope.saveData to $scope.saveDate
why dont you try something like:
ng-click="showInput = false">
sometime using ng-click inside form doesnot work the way you want. u can try with input type='submit'
also that makes your work much easier.

ng-repeat's element not updated on array modification

When I trying to update my array on ng-click in order to update my ng-repeat's directive items:
<li ng-repeat="itemL1 in mainMenuL1Arr"
ng-click="selectL2menu(itemL1.name)">
<i ng-class="getClass(itemL1.icon)"></i>
</li>
selectL2menu() defined on controller as:
$scope.selectL2menu = function selectL2menu(itemL1name){
$scope.$apply(function () {
$scope.mainMenuL2CurArr = $scope.mainMenuL2Obj[itemL1name];
});
}
Right after click on 1st level menu item it should reveal 2nd level menu block with correspondent elements (by updating the array with correspondent values, see below).
Here is 2nd level menu block I need to update on click:
<li ng-repeat="itemL2 in mainMenuL2CurArr"
class="subMenuElemBlock"
ng-class="{active: itemL2.selected}">
<a href="#">
<i ng-class="getClass(itemL2.icon)"></i>
<span>{{itemL2.name}}</span>
</a>
</li>
While click event triggered - my array successfully updated. Nevertheless ng-repeat directive not updated my 2nd level menu (base on updating array). I've tried to with (and without) using $apply function - same result (though using $apply error message appearing Error: $apply already in progress).
So, why my array being successfully updated on click not revealed on ng-repeat directive menu element?
I've read related posts (link, link, link) but did't find any working decision.
Without seeing more of your controller code, it is difficult to determine what the problem might be. Here is a simplified working fiddle. I suggest you compare it to what you have.
Note that you don't need to call $scope.$apply() because the ng-click directive will do that for us automatically.
HTML:
<ul>
<li ng-repeat="itemL1 in mainMenuL1Arr" ng-click="selectL2menu(itemL1.name)">
{{itemL1.name}}</li>
</ul>
<ul style="margin-left: 20px">
<li ng-repeat="itemL2 in mainMenuL2CurArr"><a href="#">
<span>{{itemL2.name}}</span>
</a>
</li>
</ul>
JavaScript:
function MyCtrl($scope) {
$scope.mainMenuL1Arr = [ {name: 'one'}, {name: 'two'} ];
$scope.mainMenuL2Obj = {
one: [ {name: '1.1'}, {name: '1.2'} ],
two: [ {name: '2.1'}, {name: '2.2'} ] };
$scope.mainMenuL2CurArr = $scope.mainMenuL2Obj['one'];
$scope.selectL2menu = function (itemL1name) {
console.log(itemL1name);
$scope.mainMenuL2CurArr = $scope.mainMenuL2Obj[itemL1name];
};
}
Can you try to make the $scope.$apply() after you add item in array like
$scope.array.push({id: 1, name: 'test'});
$scope.$apply();
Works fine for me, probably you have another function of a plugin or something like this, that blocking the scope apply, in my case I have a select2 and when select in field the apply is not fired
Have you tried changing the selectL2Menu function to:
$scope.selectL2menu = function (itemL1name){
$scope.mainMenuL2CurArr = $scope.mainMenuL2Obj[itemL1name];
}

How directives in Angularjs works?

I am struggle with one question and can not figure out why it happens.
I have a module (portfolioModule), service (Menu) and a directive (niMenu). The directive should render a menu item with its subitems.
HTML:
<div ng-app="portfolioModule">
<script id="menuItem" type="text/ng-template">
<li>
<a>{{item.name}}</a>
<ul>
<li ng-repeat="item in menu.getKids(item.id)" ng-include="'menuItem'"></li>
</ul>
</li>
</script>
<div>
<div ni-menu="test">test1</div>
</div>
</div>
JavaScript:
var portfolioModule = angular.module('portfolioModule', []);
portfolioModule.factory('Menu', function() {
var items = [
{
"parent_id": null,
"id": 1,
"name": "foo"
}, {
"parent_id": 1,
"id": 2,
"name": "foo-bar"
}
];
this.getKids = function(parent_id) {
var result = [];
parent_id = parent_id || null;
console.log('getKids', parent_id);
angular.forEach(items, function(value) {
if (value.parent_id === parent_id) {
result.push(value);
}
});
return result;
};
return this;
}
);
portfolioModule.directive('niMenu', function(Menu) {
var niMenu;
return niMenu = {
template: '<ul ng-include="\'menuItem\'" ng-repeat="item in menu.getKids()"></ul>',
restirt: 'A',
link: function(scope, element, attrs) {
console.log('link');
scope.menu = Menu;
}
};
}
);
Working demo: http://jsfiddle.net/VanSanblch/Ng7ef.
In html I call niMenu by ni-menu. The directive has a template and a link function that put Menu-service into a scope of module. In directive's template I use Menu.getKids() and get all top level items. Later, in template that used by ng-include I call Menu.getKids(item.id) and get all children of particular item.
Everything works excellent except one small detail. If you open console then you can observe that there are much more calls of getKids than I am expected. For example, for array of two elements the number of getKids calls is nine.
Could someone explain why on earth that happens?
Ok, so the reason it's executing more than once is because that's how the digest cycle works: it continuously executes all view expressions (like the one you pass to ngRepeat) until it expresses no change. See this answer for more information, but the takeaway is this: it will always execute at least once, but oftentimes more.
When using ngRepeat, you generally want to avoid fetching the data from a function because it negatively impacts performance; why call the same function more than once when the data never changed? A better approach is to have your controller (or in this case directive) execute your function and store the results on the scope, so your ngRepeat looks like ng-repeat="item in items" instead of ng-repeat="item in getItems()".
Unfortunately, that means you have to restructure the way you have your directive working. This turns out to be a good idea anyway, because your directive can be rewritten to be a little bit simpler - you don't need any ngInclude, for example.
What you want to do is create two directives because you have two templates: one to represent the overall menu and one to represent a child item (which, in turn, can have child items). The menu directive should repeat over the top-level menu items, drawing a menuItem for each one. The menuItem directive should check for children and repeat over those, if necessary, with more menuItems. Etc. Here's an example created by Andy Joslin of how you can accomplish a recursive directory: http://plnkr.co/edit/T0BgQR.
To implement something like this moves beyond the scope of this question, but take a stab at it and post a new question if you need help.
Good luck!

Resources