Skip to content
This repository was archived by the owner on Oct 2, 2019. It is now read-only.

[ngRepeat:dupes] Duplicates in repeater are not allowed. Broken after angular/ui/select upgrade #366

Open
davisford opened this issue Nov 3, 2014 · 10 comments

Comments

@davisford
Copy link

I have what should amount to a fairly simple use of ui-select, in fact I've been running it in production for months now with a pinned version of angular-ui and angular b/c things have been a bit dicey trying to get angular-ui working with angular 1.3.2. Well, today, I'm trying to update the dependencies and work through some of these issues, and this ui-select broke in the process. Here's output from bower on the versions:

├── angular#1.3.2-build.3514+sha.d906ed3
   /* removed other angular modules, e.g. angular-route, etc. */
├─┬ angular-ui-bootstrap-bower#0.11.2
├─┬ angular-ui-select#0.8.3

This is the error being thrown after the upgrade:

Error: [ngRepeat:dupes] Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: $item in $select.selected, Duplicate key: string:u, Duplicate value: "u"
http://errors.angularjs.org/1.3.2-build.3514+sha.d906ed3/ngRepeat/dupes?p0=%24item%20in%20%24select.selected&p1=string%3Au&p2=%22u%22
    at http://localhost:3000/angular/angular.js:80:12
    at ngRepeatAction (http://localhost:3000/angular/angular.js:24073:21)
    at Object.$watchCollectionAction [as fn] (http://localhost:3000/angular/angular.js:13819:13)
    at Scope.$digest (http://localhost:3000/angular/angular.js:13952:29)
    at Scope.$apply (http://localhost:3000/angular/angular.js:14214:24)
    at done (http://localhost:3000/angular/angular.js:9474:47)
    at completeRequest (http://localhost:3000/angular/angular.js:9659:7)
    at XMLHttpRequest.requestLoaded (http://localhost:3000/angular/angular.js:9602:9) angular.js:11339(anonymous function) angular.js:11339(anonymous function) angular.js:8415Scope.$digest angular.js:13970Scope.$apply angular.js:14214done angular.js:9474completeRequest angular.js:9659requestLoaded

Obviously, this is a well-known error, and nicely documented, to boot...the problem is track by doesn't seem to resolve the issue, and besides, I'm binding to an array of strings in the repeat, which shouldn't need track by AFAIK, because it ought to just use the array index (I thought).

Here's what the code looks like:

<ui-select multiple ng-model='user.roles' theme='bootstrap'>
  <ui-select-match placeholder='Select a role...'> {{$item}} </ui-select-match>
  <ui-select-choices repeat='role in roles track by $index'> {{role}} </ui-select-choices>
</ui-select>

I added the track by $index but it seems to make no difference.

The controller:

// Roles and Accounts are ngResource
angular.module('app').controller('MyCtrl', ['$scope', 'Roles', 'Accounts', function ($scope, Roles, Accounts) {

  // ui-select requires these to be bindable even before ngResource is resolved
  $scope.user = { roles: [ ] };
  $scope.roles = [ ];

  Roles.query().$promise.then(function (res) {
    $scope.roles = res.roles;
    // i.e. [ 'user', 'admin', 'superuser' ]
  }, handleError);

  Accounts.get({id: id}).$promise.then(function(user) {
    $scope.user = user;
  }, handleError);

}]);

The interesting thing is if I breakpoint the error in Angular, it seems to be doing ng-repeat on each character of each string in the array, as opposed to each individual word in the array. This is why it throws the error, because the word 'superuser' contains two u characters - which it considers a duplicate.

screen shot 2014-11-03 at 1 15 57 pm

I haven't the foggiest why it is behaving this way...any ideas are more than welcome.

This is the DOM HTML for this element, copied out of Chrome DevTools (after the error is thrown):

<div class="ui-select-multiple ui-select-bootstrap dropdown form-control ng-valid" ng-class="{open: $select.open}" multiple="multiple" ng-model="user.roles" theme="bootstrap">
  <div>
    <span class="ui-select-match" placeholder="Select a role...">
      <!-- ngRepeat: $item in $select.selected -->
    </span>
    <input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" class="ui-select-search input-xs ng-pristine ng-valid ng-touched" placeholder="Select a role..." ng-disabled="$select.disabled" ng-hide="$select.disabled" ng-click="$select.activate()" ng-model="$select.search">
  </div>
  <ul class="ui-select-choices ui-select-choices-content dropdown-menu ng-scope" role="menu" aria-labelledby="dLabel" ng-show="$select.items.length > 0" repeat="role in roles track by $index">
    <li class="ui-select-choices-group">
      <div class="divider ng-hide" ng-show="$select.isGrouped &amp;&amp; $index > 0"></div>
      <div ng-show="$select.isGrouped" class="ui-select-choices-group-label dropdown-header ng-binding ng-hide"></div>
      <!-- ngRepeat: role in $select.items track by $index -->
      <div class="ui-select-choices-row ng-scope" ng-class="{active: $select.isActive(this), disabled: $select.isDisabled(this)}" ng-repeat="role in $select.items track by $index" ng-mouseenter="$select.setActiveItem(role)" ng-click="$select.select(role)"><a href="javascript:void(0)" class="ui-select-choices-row-inner" uis-transclude-append=""><span class="ng-binding ng-scope">user</span></a></div>
      <!-- end ngRepeat: role in $select.items track by $index -->
      <div class="ui-select-choices-row ng-scope" ng-class="{active: $select.isActive(this), disabled: $select.isDisabled(this)}" ng-repeat="role in $select.items track by $index" ng-mouseenter="$select.setActiveItem(role)" ng-click="$select.select(role)"><a href="javascript:void(0)" class="ui-select-choices-row-inner" uis-transclude-append=""><span class="ng-binding ng-scope">admin</span></a></div>
      <!-- end ngRepeat: role in $select.items track by $index -->
      <div class="ui-select-choices-row ng-scope" ng-class="{active: $select.isActive(this), disabled: $select.isDisabled(this)}" ng-repeat="role in $select.items track by $index" ng-mouseenter="$select.setActiveItem(role)" ng-click="$select.select(role)"><a href="javascript:void(0)" class="ui-select-choices-row-inner" uis-transclude-append=""><span class="ng-binding ng-scope">superuser</span></a></div>
      <!-- end ngRepeat: role in $select.items track by $index -->
    </li>
  </ul>
  <input ng-disabled="$select.disabled" class="ui-select-focusser ui-select-offscreen ng-scope" type="text" aria-haspopup="true" role="button">
</div>
@davisford
Copy link
Author

I've spent some time digging into this. I've been able to get past the ngRepeat:dupes by making the following changes:

<ui-select multiple='true' ng-model='user.roles' theme='bootstrap'>
  <ui-select-match placeholder='Select a role...'> {{$item}} </ui-select-match>
  <ui-select-choices repeat='role in roles track by $index'> {{role}} </ui-select-choices>
</ui-select>

The big change here was from the simple attribute, multiple to multiple="true". The logic for determining if multiple is in play seems wrong (despite being woefully hard to read...sorry)?

select.js line 615

$select.multiple = (angular.isDefined(attrs.multiple)) ? (attrs.multiple === '') ? true : (attrs.multiple.toLowerCase() === 'true') : false;

In the demo and docs, the attribute multiple is simply defined, but in this case, if you also don't assign the string value of "true" to the attribute, the code takes some weird branches and all kinds of difficult to track down problems ensue:

  • ngRepeat:dupes error was popping up
  • sometimes the widget seems like it might work, but if you selected something, it would split the string up into individual characters. for example the selection "admin" would get split up and a button with an X would be placed in the input for each letter:, a, d, m, i, n.
  • after solving the above issue with track by $index inserted into the templates at the bottom of select.js, and solving the second issue with binding to {{roles[$index]}} there were other problems where the delete would throw an exception in the code, and so on...

It came down to that one small change. It might be good to either:

a) update the docs / examples to make it explicit that multiple="true" is required or
b) change the line above to be more reasonable...if the attribute multiple is defined and doesn't equal "false" for example, then $select.multiple is true.

$select.multiple = false;
if (angular.isDefined(attrs.multiple)) && attrs.multiple.toLowerCase() !== 'false') {
  $select.multiple = true;
} 

@RDGthree
Copy link

Just ran into this exact problem, and the same thing - setting multiple to true - fixed it. For me, it was working fine on dev, but the bugs discussed here all showed up on production. Not 100% sure why - I'm minifiying all of my scripts so potentially something to do with that. The fix you listed should solve it regardless.

Thanks for writing this out in so much detail!

@icfantv
Copy link

icfantv commented Apr 6, 2015

I'm getting this on a single-select, i.e., 'multiple' has not been specified. Anyone have any thoughts on getting around it? Thanks.

@abeninskibede
Copy link

Replacing ngModel.$formatters.unshift... in uiSelectMultiple directive
with the following code fixed the problem for me:

                // From model --> view
                ngModel.$formatters.unshift(function (inputValue) {
                    var data = $select.parserResult.source(scope, { $select: { search: '' } }), //Overwrite $search
                        locals = {},
                        result;
                    if (!data) return inputValue;
                    var resultMultiple = [];

                    var alreadyExistsInResultsFn = function (candidate) {
                        var trackBy = $select.parserResult.trackByExp;

                        for (var i = 0; i < resultMultiple.length; i++) {
                            var current = resultMultiple[i];

                            if (trackBy) {
                                var matches = /\.(.+)/.exec($select.parserResult.trackByExp);
                                if (matches && matches.length > 0 && current[matches[1]] != undefined && current[matches[1]] == candidate[matches[1]]) {
                                    return true;
                                }
                            }
                            else {
                                if (angular.equals(current, candidate)) {
                                    return true;
                                }
                            }
                        }

                        return false;
                    }

                    var addToResultsSafeFn = function (candidate) {

                        if (!alreadyExistsInResultsFn(candidate)) {
                            resultMultiple.unshift(candidate);
                            return true;
                        }

                        return false;
                    }

                    var checkFnMultiple = function (list, value) {
                        if (!list || !list.length) return;
                        for (var p = list.length - 1; p >= 0; p--) {
                            locals[$select.parserResult.itemName] = list[p];
                            result = $select.parserResult.modelMapper(scope, locals);
                            if ($select.parserResult.trackByExp) {
                                var matches = /\.(.+)/.exec($select.parserResult.trackByExp);
                                if (matches && matches.length > 0 && result[matches[1]] != undefined && result[matches[1]] === value[matches[1]]) {
                                    if (addToResultsSafeFn(list[p])) {
                                        return true;
                                    }
                                }
                            }
                            if (angular.equals(result, value)) {
                                if (addToResultsSafeFn(list[p])) {
                                    return true;
                                }
                            }
                        }
                        return false;
                    };
                    if (!inputValue) return resultMultiple; //If ngModel was undefined
                    for (var k = inputValue.length - 1; k >= 0; k--) {
                        //Check model array of currently selected items 
                        if (!checkFnMultiple($select.selected, inputValue[k])) {
                            //Check model array of all items available
                            if (!checkFnMultiple(data, inputValue[k])) {
                                //If not found on previous lists, just add it directly to resultMultiple
                                addToResultsSafeFn(inputValue[k]);
                            }
                        }
                    }
                    return resultMultiple;
                });

@silentHoo
Copy link

+1

@user378230
Copy link
Contributor

@silentHoo this original issue is over two years old and the last update ~12 months. You're +1 here is not very helpful...

Could you instead:

  1. Verify against the latest version of the library
  2. Include details of your angular version
  3. Post a reproduction plunkr

That'd be way more helpful to anyone trying to fix 😃

Thank you! 👍

@eMerzh
Copy link

eMerzh commented May 12, 2016

still have the issue here with ui.select-0.17
and angular-1.5.3

a multiple with refresh, after adding some tags (often many) i ran into a duplicate

@LeonardoGentile
Copy link

LeonardoGentile commented May 26, 2016

I can also confirm: still happening on angular 1.5 and ui-select 0.17.1
The same application is running fine on Chrome but throwing this error on Firefox 45.0.2

@FishTheOriginal
Copy link

FishTheOriginal commented Sep 1, 2016

I'm having the same issue with ui-select (version 2.1.3). The selected items are loaded via ajax, the correct selection will show up but when clicked to select another option, the already selected options are within the possible options and when selected again a dupe error is firing.

I decided not to dig in the sources, instead to force refresh the directive from the script.

This is the fix:
HTML

<ui-select multiple="true" ng-model="x.itemsSelected" theme="bootstrap">
    <ui-select-match placeholder="Select Eligibility Item...">{{$item.name + ' #' + $item.id}}</ui-select-match>
    <ui-select-choices repeat="item.id as item in x.itemsAvailable | filter: $select.search track by $index" refresh="uiSelectRefreshFix($select)" refresh-delay="1000">
        <div ng-bind-html="item.name + ' #' + item.id | highlight: $select.search"></div>
    </ui-select-choices>
</ui-select>

Controller

// ui-select bugfix.
$scope.uiSelectRefreshFix = function () {

    if ($scope.uiSelectFix) return; 
    $scope.uiSelectFix = true;

    var fix = $scope.x.itemsSelected;
    $scope.x.itemsSelected = [];

    setTimeout(function() {
        $scope.$apply(function(){
            $scope.x.itemsSelected = fix;
        });
    }, 100)
    console.log([model, query]);
}

@hamdoune2011
Copy link

Use clone object
example:
var obj={
"name": "Algeria",
"capital": "Algeria",
"language": "Arabic"
}
let newObj= Object.assign({}, obj);
objList.push(newObj);

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests