ng-repeat's element not updated on array modification - angularjs

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

Related

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.

AngularFire - Remove Single Item

Here is the relevant code in my view:
p(ng-repeat="t in todos")
input(
type="checkbox",
ng-model="t.done",
ng-click="clearItem($event)"
)
{{t.text}} done? {{t.done}}
When the checkbox is clicked, I want the appropriate object in the todos array to be removed from the database.
My clearItem function is as follows:
$scope.clearItem = function(event) {
todoRef.remove($scope.t);
}
However, this removes all the entries in my database. I want it to remove only the specific object in question. Is there anyway for me to do this?
Ok, figured it out.
When looping using ng-repeat, use (id, t) in todos. This allows you to send id as the parameter to the ng-click function, and $scope.todos.$remove(id) works just fine.
To provide a more complete example for anyone else that lands here, according to Firebase's documentation for AngularFire this would be the preferred way, and I believe the easiest way to remove an object:
// Create an app. Synch a Firebase array inside a controller
var myApp = angular.module("myApp", ["firebase"]);
// inject $firebaseArray
myApp.controller("TodoCtrl", ["$scope", "$firebaseArray", function($scope, $firebaseArray) {
// bind $scope.todos to Firebase database
$scope.todos = $firebaseArray(myFirebaseRef.child("todo"));
// create a destroy function
$scope.removeTodo = function(todo) {
$scope.todos.$remove(todo);
};
}]);
In your view, you could do something like below. Note that you could bind the removeTodo function to a checkbox as the question specifies, or a regular old <a href> element:
// In your view
<div ng-controller="TodoCtrl">
<ul>
<li ng-repeat="todo in todos">
{{ todo.text }} : <a href ng-click="removeTodo(todo)">X</a>
</li>
</ul>
</div>
Hope that helps!
A better solution would be to have $scope.clearItem() take the object t as an argument, instead of $event.
HTML - <p ng-repeat="t in todos"><input... ng-click="clearItem(t)">
JS - $scope.clearItem = function(obj) {todoRef.$remove(obj)};
The only way I'm able to remove the item is using a loop on the array we get from firebase.
var ref= new Firebase('https://Yourapp.firebaseio.com/YourObjectName');
var arr_ref=$firebaseArray(ref);
for(var i=0;i<arr_ref.length;i++){
if(key==arr_ref[i].$id){
console.log(arr_ref[i]);
arr_ref.$remove(i);
}
}
The easiest way to remove the object would be
scope.clearItem = function(event) {
todoRef.$loaded().then(function(){
todoRef.$remove($scope.t)
});
The asynchronous nature of the beast has gotten me a few times.

Angularjs binding array splice

I have an issue with splicing an array:
I m displaying my array like this :
Email:<input type="text" ng-model="newcontact"/>
<button ng-click="add(newcontact)">Add</button>
<h2>Contacts</h2>
<ul id="todo-list">
<li ng-repeat="contact in contacts">
{{contact.libelle}}
del
</li>
</ul>
My controller is :
angular.module('angulartestApp')
.controller('MainCtrl', function ($scope,$log) {
var $scope.contacts = [{libelle:'test'},{libelle:'test2'}];
$scope.add = function(newcontact) {
$scope.contacts.push({libelle:newcontact});
$scope.newcontact = '';
};
$scope.del = function (idx){
$scope.contacts.splice(idx, 1);
for (var i=0;i<$scope.contacts.length;i++)
{
$log.info($scope.contacts[i].libelle+',');
}
};
});
If I add two items ('a' and 'b'), my view displays the good items in my list ('test', 'test2', 'a','b').
If I delete the 'a' item, my view displays only the two initial items of my list ('test', 'test2').
But in the console everything is good, 'test', 'test2' and 'b' gets well displayed.
I don't understand why. If someone can show me the good way to do it...
Thank you.
Is the page being reloaded when you click the del link? Try using a button instead of to verify. This may also help:
href causes unintended page reload with Angularjs and Twitter Bootstrap

AngularJS: Setting a variable in a ng-repeat generated scope

How do I access the set of scopes generated by an ng-repeat?
I suppose at the heart of this is the fact that I don't understand quite how the relationship works between a) the collection of objects that I pass into the ng-repeat directive and b) the collection of scopes that it generates. I can play around with (a), which the ng-repeat scope watches and picks up, but how do I set variables on the scope itself (b)?
My use case is that I have a set of elements repeating using ng-repeat, each of which has an edit view that gets toggled using ng-show/ng-hide; the state for each element is held in a variable in the local scope. I want to be able to trigger an ng-show on a particular element, but I want the trigger to be called from outside the ng-repeat, so I need to be able to access the local scope variable.
Can anyone point me in the right direction (or tell me if I'm barking up the wrong tree)?
Thanks
Update: Link below was very helpful thankful. In the end I created a directive for each of the repeating elements, and used the directive's link function to add its scope to a collection on the root scope.
Here is a pretty simple way to do what I think you are trying to do (I figured this out when I needed to do something similar):
We know that each repeated item has it's own scope created for it. If we could pass this scope to a method defined on the parent scope, then we'd be able to do what we want with it in terms of manipulating or adding properties. It turn out this can be done by passing this as an argument:
Example
// collection on controller scope
$scope.myCollection = [
{ name: 'John', age: 25 },
{ name: 'Barry', age: 43 },
{ name: 'Kim', age: 26 },
{ name: 'Susan', age: 51 },
{ name: 'Fritz', age: 19 }
];
// template view
<ul>
<li ng-repeat="person in myCollection">
<span ng-class="{ bold : isBold }">{{ person.name }} is aged {{ person.age }} </span>
<button class="btn btn-default btn-xs" ng-click="toggleBold(this)">toggle bold</button>
</li>
</ul>
So when we press the "toggle bold" button, we are calling the $scope.toggleBold() method that we need to define on the controller's $scope. Notice that we pass this as the argument, which is actually the current ng-repeat scope object.
Therefore we can manipulate it like this
$scope.toggleBold = function(repeatScope) {
if (repeatScope.isBold) {
repeatScope.isBold = false;
} else {
repeatScope.isBold = true;
}
};
Here is a working example: http://plnkr.co/edit/Vg9ipoEP6wxG8M1kpiW3?p=preview
Ben Nadel has given a pretty clean solution to the "how do I assign to an ngRepeat's $scope" problem, which I just implemented in my own project. Essentially, you can add an ngController directive alongside your ngRepeat, and manipulate ngRepeat's $scope inside the controller.
Below is my own contrived example, which demonstrates assigning to ngRepeat's $scope in a controller. Yes, there are better ways to do this exact thing. See Ben Nadel's post for a better example.
<div ng-controller="ListCtrl">
<h1>ngRepeat + ngController</h1>
<ul>
<li ng-repeat="item in items" ng-controller="ItemCtrl" ng-show="isVisible">
{{item.name}}
<button ng-click="hide()">hide me!</button>
</li>
</ul>
</div>
<script type="text/javascript">
var app = angular.module("myApp", []);
app.controller("ListCtrl", function($scope) {
$scope.items = [
{name: "Item 1"},
{name: "Item 2"},
{name: "Item 3"}
];
});
app.controller("ItemCtrl", function($scope) {
$scope.isVisible = true;
$scope.hide = function() {
$scope.isVisible = false;
};
});
</script>
EDIT: Having re-read your question, seeing that you need to manipulate a bunch of child scopes in a parent scope, I think that your directive(s) approach is the way to go. I still think this answer may be useful to some, as I came across your question while looking for this answer.
When working within a hierarchy of scopes I find very useful to dispatch events with $emit and $broadcast.
$emit dispatches an event upwards so your child scopes can notify parent scopes of a particular event.
$broadcast is the other way round.
Alternatively, as child scopes have access to parent scope properties you could trigger changes by using $watch on a particular property in the parent scope.
UPDATE: As for accessing the child scopes, this may turn useful for you : Get to get all child scopes in Angularjs given the parent scope

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

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"

Resources