AngularJs ng-repeat track by $index issue with angular-bootstrap-switch [duplicate] - javascript

This question already has an answer here:
Problems with `track by $index` with Angular UI Carousel
(1 answer)
Closed 4 years ago.
I am having an issue with AngularJs ng-repeat and angular-bootstrap-switch
I am using:
ng-repeat="item in items track by $index"
When I added a new item into the first index of array by:
newItem = {};
items.splice(0, 0, newItem);
The DOM contain a bs-switch:
When a new item added it reuse the first element in array base on $index, so it doesn't re-render (that's what I get from this docs). The problem I have is the previous element has switch-on.
The DOM effect issue I have is the class "switch-on" doesn't refresh on new item and it keep on.
Expected: In this case I want to switch is off instead of on like the image. Cause it's an empty object
P/s: Cause of the business so
I cannot add default value for that Switch. It need to be an empty object
I also cannot use any identifier of the item object to track replace to $index
If I use default track by $id it will cause some business issue also
TEMP SOLUTION FOR WHO WORKING ON Angular 1.5 or Upper:
With angular 1.5 and you can use angular-bootstrap-switch 0.5.1
It will fixed the issue, after frapontillo release a changed: "Make "switch-change" trigger when model changes"
Thank you so much for supporting.

What I perceive from your question is that you want to add a new item to main array and when It renders, you want it's switch to be off by default.
Well there are couple of things you need to keep in mind.
ng-repeat only behaves as a loop in our DOM.
If you want to have the switch off by default, you must need to have a model for that in every item of the main array. It'll keep track of every switch's state in DOM.
Here's what you should try
From your controller:
newItem = {switchState: 0};
items.splice(0, 0, newItem);
From your HTML:
<div ng-repeat="item in items track by $index">
<!-- your Name and Decription input fields.. -->
<input
bs-switch
ng-model="item.switchState" />
So I think you're ignoring ng-model of angular-bootstrap-switch which causes you the problem.

Related

Changes to selected element in scope won't be persisted in array of available elements

I'm using angularJS 1.5.6 and try to update a value by using a combo box within an object that had already being selected through another combo box. But after switching the source object these changes are lost.
For a better understanding try this JSFiddle.
Within the above example the first combo box just selects an item out of a list of availables:
<select data-ng-model="currentElement"
data-ng-options="element.name for element in elements track by element.id"
data-ng-change="updateDeeperOption()">
</select>
The second combo box works on a second list of available options:
<select data-ng-model="deeperOption"
data-ng-options="option.name for option in availableOptions track by option.value"
data-ng-change="writeDeeperOptionToCurrentElement()">
</select>
The two method, which are being called whenever a change happens are simply updating the data in both directions:
$scope.updateDeeperOption = function() {
$scope.deeperOption = $scope.availableOptions.filter(function (option) {
return option.value === $scope.currentElement.going.deeper.to.myOption; })[0];
};
$scope.writeDeeperOptionToCurrentElement = function() {
$scope.currentElement.going.deeper.to.myOption = $scope.deeperOption.value;
};
The problem comes from writeDeeperOptionToCurrentElement(). It updates the current element (you can see it within the fiddle), but if you switch to another element and back again, the changes are lost.
Any idea, what I made wrong? I think, I'm still missing some fundamental understanding about when objects are copied or just referenced within AngularJS. So any explanation or links would be helpful.
Update:
I isolated the actual problem here and found that if you remove the track by from your first select then the currentElement object gets updated by reference. See: https://jsfiddle.net/tbzggyg1/4/
So instead of this:
<select data-ng-model="currentElement"
data-ng-options="element.name for element in elements track by element.id"
data-ng-change="updateDeeperOption()">
Try this:
<select data-ng-model="currentElement"
data-ng-options="element.name for element in elements"
data-ng-change="updateDeeperOption()">
And here is why: https://docs.angularjs.org/api/ng/directive/ngOptions
... is to use a track by clause, because then ngOptions will track the identity of the item not by reference, but by the result of the track by expression. For example, if your collection items have an id property, you would track by item.id.
Old answer:
It looks like your '$scope.currentElement' object is actually just getting a copy reference from the array, so when you make modifications it doesnt really update the original array (which is what your dropdowns bind to). I tried this and it worked:
$scope.writeDeeperOptionToCurrentElement = function() {
// $scope.currentElement.going.deeper.to.myOption = $scope.deeperOption.value; // old code
var elementRef = $scope.elements.filter(isMatchingElement)[0];
elementRef.going.deeper.to.myOption = "" + $scope.deeperOption.value;
};
function isMatchingElement(e) {
return e.id === $scope.currentElement.id;
}
Also, add this to your template so you can see the entire picture of whats happening:
<tt>{{currentElement}}</tt>
<hr>
<tt>{{elements}}</tt>
Link to fiddle forked: https://jsfiddle.net/t2dvm0a2/
In ng-options each option item is identified by the value you provide in track by.
Had you given your deep value in track by, things would have worked.
<select data-ng-model="currentElement"
data-ng-options="element.name for element in elements track by element.going.deeper.to.myOption"
data-ng-change="updateDeeperOption()">
</select>
Please check the updated fiddle
https://jsfiddle.net/tbzggyg1/3/

Ractive.js index doesn't appear to be reactive

Please see this jsfiddle (adapted forked from one by Rich Harris):
http://jsfiddle.net/upgu2tq3/29/
I'm trying to remove an item by index from the 'Posts' keypath. I'm able to do this using the built in splice method. It behaves just like a normal Array.splice. splice(array,startIndex,numberOfItemsToRemove)
{{#each Posts:i}}
<button on-click="splice('Posts',i,1)">Remove {{i}}</button>
{{/each}}
This does the job of removing the item from the keypath. However, upon removal, the value of {{i}} (next to 'Remove') doesn't appear to update.
I've labelled the posts 'Post 0', 'Post 1' etc.. to match up with the index.
It looks like its being iterated by numeric index (from 0 to length-1)? Just the template doesn't seem to update the index when the array is altered. I've checked the length after an item has been removed - its reduced...
I'm just a little confused by this.
It's working as expected.
The index will change after splicing. You can see the button label change after clicking the remove button.
You are getting confused because you have hard-coded the content to "Post 0" etc. The content will not change because the index changed.
If you want it to change then change {{Text}} to Post {{i}}

Adding item to model and refresh a second ng-repeat

While working on the phone tutorial i wondered how to implement this use case
browser renders a list of phones
user clicks on add phone for one of these phones
the function addItem is fired and adds this phone into the array phonesInCart
the list with phones in cart $scope.phonesInCart = []; is updated
My codepen demo has the same logic as this code
<body ng-controller="PhoneListCtrl">
<h3>Phones we sell</h3>
<ul class="phones">
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
<b>{{phone.name}} </b>
<button ng-click="addItem(phone)"
ng-disabled="{{phone.orderReadonly}}">add phone</button>
</li>
</ul>
<h3>Phones in your cart</h3>
<ul>
<li ng-repeat="phoneInCart in phonesInCart">
{{phoneInCart.name}}
</li>
</ul>
</body>
and relevant javascript
$scope.phonesInCart = [];
$scope.addItem = function(phone) {
// these lines have no effect on the ui
phonesInCart.push(phone);
$scope.$apply();
}
Current status
The list is rendered and the 'addItem' function is fired.
Currently the list of the phones in cart are not updated / not rendered.
My question
Could you explain
what has to be done to fill a second array phonesInCart[] and refresh another ng-repeat and
do i have to create / use more than one controller phonecatApp.controller('PhoneListCtrl' ... to be able to have a secon ng-repeat?
In your code, instead of phonesInCart.push(phone), you need to do:
$scope.phonesInCart.push(phone)
Since that is the variable you loop over in your html.
EDIT Since the addItem call happens as a part of an ng-click directive, you can get rid of $scope.$apply. Actually, you should be getting an error saying 'digest loop already in progress' with your current code.
I edited your codepen code slightly.
First off, as others already pointed out, you missed a $scope when pushing into the array of phones.
Secondly, if you want to be able to add multiple of the same phones to the list, you need to add track by $index to your ng-repeat, so it accepts "duplicates".
You also had an unnecessary $scope.apply in your addPhone function - you don't need to update it manually; when you push to the scope variable array, it triggers the digest cycle itself.
Lastly, you don't need to $watch the phonesInCart array due to the same reason as above.
Here's an updated pen to which all these changes have been made. If you have anything else to ask, please do :)

AngularJS: Custom directives not working with dirPagination

I am using AngularJS to create a page which contains a list of products that shows information such as a name, price, region etc. This is displayed kind of like an accordion with the name in the header and extra information in the body.
Since there could be a large amount of these items displayed I am using dirPagination (https://github.com/michaelbromley/angularUtils/tree/master/src/directives/pagination) to paginate these items. My markup at the moment looks like this:
<div class="custom-list" dir-paginate="asset in assets | itemsPerPage: 10 track by $index">
<div class="custom-list-item" ng-class="{'tag-hover': isHoveredTag(asset)}">
<div class="custom-list-item-heading" ng-click="toggleShowAssetDetails(asset)">
<p class="col-lg-5">{{ asset.name }}</p>
<div class="col-lg-offset-2 col-lg-3 rating">
<rating value="asset.rating" />
</div>
<button ng-click="addAsset(asset)"><span class="glyphicon glyphicon-plus"></span></button>
<div class="clearfix"></div>
</div>
<div class="custom-list-item-content" style="display: none" animate="shouldShowAssetDetails(asset)">
...
</div>
</div>
</div>
As you can see I'm using paginate in a pretty standard way just looping through the items in an array and displaying 10 per page. I also have a directive called rating which looks at a value called rating in the item. This is a number from 1 - 5 which is used to display a star rating system next to the name. The directive looks like this:
var rating = function ($compile) {
return {
restrict: "E",
scope: {
value: "="
},
link: function (scope, element, attrs) {
scope.$watch(attrs.rating, function () {
for (var i = 1; i <= 5; i++) {
if (i <= scope.value) {
var starElement = angular.element("<span class=\"icon icon-crown\"></span>");
$compile(starElement)(scope);
element.append(starElement);
} else {
var emptyStarElement = angular.element("<span class=\"icon-empty icon-crown\"></span>");
$compile(emptyStarElement)(scope);
element.append(emptyStarElement);
}
}
})
}
}
}
This looks at the value and inserts the icons based on the value of rating (e.g if the rating was 2 the directive would insert two icon-crown icon spans and 3 icon-empty icon-crown icon spans.
This was working perfectly fine before I included the pagination. However now it will only work for the first 10 items. When you change the page, the rating will not change and just keep the same icons from the previous page, even if the values are different.
I understand this is because the directive sets everything at the beginning and it will not run when you change page because the items aren't reloading they are just being shown and hidden again. But the directive is manipulating the DOM so it doesn't update when the page changes.
The problem is I don't know how to resolve this. I thought about changing the directive to look for the pagination current page instead but then I don't have access to the current list item.
I'd appreciate any help on getting the directive to update the icons when the page is changed.
Update
Here's a link to a Plunker project showing the problem I'm having: http://plnkr.co/edit/VSQ20eWCwVpaCoS7SeQq?p=preview
This is a very stripped down version of the section on my app that I'm having an issue with. There's no styling included although I have kept the CSS class structure. I've also changed the icons to use bootstrap ones just to simplify the Plunker project.
The functionality is the same however. If you go from page 1 to page 2 notice how the stars remain the same despite that fact that the asset rating values are different. However if you go to page 3 and back to page 2 or 1 they will change. The reason this happens is because there are less items on page 3 and therefore when you go back to page 1 or 2 the remaining items will be called again to retrieve the rating values.
You simply need to remove or replace track by $index.
Tracking by $index will give you the following behavior:
There is an array of max 10 length that represents the items to show. The first item will have index 0.
You go to the next page and the items in the array are replaced.
The first item in the array will still have index 0. Since you are tracking by index and the index has not changed, AngularJS will not recreate the DOM node representing the item. Since the DOM node isn't recreated, the directive will not execute this time and the rating will not update.
If you go from page 3 to page 2, the directive will execute for the 7 last elements on page 2, since they didn't exist on page 3, so they are considered new this time.
If you really need to use track by you should use a property that is actually related to the object (and unique), for example:
dir-paginate="asset in assets | itemsPerPage: 10 track by asset.name"
Demo: http://plnkr.co/edit/A80tSEliUkG5idBmGe3B?p=preview

Remove blank option ng-repeat and ng-options after filter

There are several questions very similar to this one yet I have been unable to come up with a solution.
I have a select list using angularJS. I need to use the title attribute so I have an ng-repeat to create the options, there is always a blank option even if I use ng-selected to always select the first option.
Even if I make a selection and the blank option goes away, if I then filter out that selected value the blank will reappear.
I have included a select list using ng-option (which does not include my needed tittle attribute) and a default value to show that the blank will appear after filter.
The behavior I desire would be to never have a blank option (always selecting first option would be fine) and to possibly have a directive per option for special handling of click events.
Thanks in Advance!
JS Fiddle: http://jsfiddle.net/32DFM/3/
<select size="3" ng-model="person.current">
<option ng-repeat="p in people | filter:person.SearchTerm"
ng-selected="$first"
value="{{p}}"
title="{{p.name}}">
{{p.name}}
</option>
</select>
I forked your fiddle (if I may be so blunt): http://jsfiddle.net/XsFe8/2/
This fixes it somewhat. Although I haven't gotten it to work properly together with the filter.
Anyway, what I do here, is to use the person.id as the value on each option.
<select ng-model="person.current">
<option ng-repeat="p in people | filter:person.SearchTerm" ng-selected="$first" value="{{p.id}}" title="{{p.name}}">
{{p.name}}
</option>
</select>
And set the initial calue on the person.current model:
$scope.person.current = $scope.people[1].id;
But it's still not 100% though. I'm a bit stumped to why the blank spaces appear when you filter the select....
An alternative that might or might not work, would be to use something like ng-repeat="p in filterPeople() and filter your array in a filterPeople function. But I'm not sure if this will change anything.
UPDATE: I tested out my suggestion above, here: http://jsfiddle.net/XsFe8/2/
If you set the selected object to be the first object in the filtered array, it works. I do this each time a new filtered array is created:
$scope.filterPeople = function () {
var array = filterFilter($scope.people, $scope.person.SearchTerm);
$scope.person.current = array[0].id;
return array;
};
It looks like things get hairy when another object than what is visible in the select is actually selected. This is kind of understandable :)
Your actual problem is the value in ngModel is referencing a value which doesn't exist in the select anymore.
A solution is to whenever you alter the select options, you also check the person.current to ensure that it points to a valid entry.
This also implies that you might want to move your filter into the controller, and set the options in the scope (you can use the $filter service in your code to get same behaviour there, https://docs.angularjs.org/api/ng/filter/filter). This way you can have a function in your controller checking if person.current is valid, and if not, set it to desired options (e.g. the first one).
the hairyness cited above is due to an empty array when all items are filtered out and is fixed by:
if(array.length>0)
$scope.person.current = array[0].id;
http://jsfiddle.net/b0z6vpr8/
Hope this helps

Categories