Zach Zach - 27 days ago 13
AngularJS Question

md-select showing multiple selections after updating

I created an issue on the material github, but with their focus on material2, I wanted to get some help from the gurus on here to determine if this is something I'm doing or if it's a problem with angularjs/material. So here's my issue:

enter image description here

The user can add a certification by selecting from the dropdown and clicking "ADD NEW". Those certifications are bound to an

ng-repeat
which generates the cards in yellow. Those cards have lists all bound to the same datatype. As you can see above, I'm clicking on this icon to open a dialog which presents a form for adding an item to a list which populates the
md-select
s on the page. After adding to the list, the
md-select
's selected label shows two of the same items are selected.
multiple
is not enabled on the
md-select
s and each selected id only has one value. Clicking on the
md-side-nav
, tab title, or
md-select
itself will update the selected label to show properly. Inspecting the DOM, there are no duplicate items. I have attempted to recreate this issue on codepen but so far I've been unsuccessful. This is my layout:

<md-tabs md-dynamic-height md-border-bottom>
<md-tab>
<md-tab-label>
Certifications
</md-tab-label>
<md-tab-body>
<div layout="row" layout-padding>
<div flex="50">
<md-input-container>
<label>Last Audit</label>
<md-datepicker ng-model="addEditSupplierCtrl.supplier.dateLastAudit"></md-datepicker>
</md-input-container>
</div>
<div>
<md-input-container>
<label>Next Audit</label>
<md-datepicker ng-model="addEditSupplierCtrl.supplier.dateNextAudit"></md-datepicker>
</md-input-container>
</div>
</div>
<div layout="row" layout-padding>
<md-input-container style="min-width: 200px;">
<label>Certification Type</label>
<md-select ng-model="addEditSupplierCtrl.newSupplierCertification.certificationTypeId">
<md-option ng-repeat="certificationType in addEditSupplierCtrl.certificationTypes" value="{{ certificationType.id }}">
{{ certificationType.name }}
</md-option>
</md-select>
</md-input-container>
<div>
<md-button class="md-primary md-raised" ng-click="addEditSupplierCtrl.addSupplierCertification($event)">Add New</md-button>
</div>
</div>
<div layout="row" layout-wrap>
<md-card md-theme="{{ certification.requiresAudit ? 'audit' : 'default' }}" ng-repeat="certification in addEditSupplierCtrl.supplier.supplierCertifications | orderBy:'certificationType.name'" flex="100" flex-gt-sm="40" flex-gt-md="30">
<md-card-title flex="none">
<md-card-title-text>
<div style="position: relative">
<strong>Selected Id:</strong> {{ certification.certificationTypeId | json }}<br />
<md-input-container style="min-width: 150px; max-width: 350px;">
<label>Certification Type</label>
<md-select ng-model="certification.certificationTypeId">
<md-option ng-repeat="certificationType in addEditSupplierCtrl.certificationTypes" value="{{ certificationType.id }}">
{{ certificationType.name }}
</md-option>
</md-select>
</md-input-container>
<br /><strong>Select List Data:</strong> {{ addEditSupplierCtrl.certificationTypes | json }}
<md-button class="md-icon-button md-primary" ng-click="addEditSupplierCtrl.showAddCertificationTypeDialog($event)">
<md-icon>playlist_add</md-icon>
</md-button>
<div style="position: absolute; right: 0; top: 0">
<md-button class="md-icon-button md-primary" title="Delete Certification" ng-click="addEditSupplierCtrl.deleteCertification($event, certification)">
<md-icon>cancel</md-icon>
</md-button>
</div>
</div>
</md-card-title-text>
</md-card-title>
<md-card-content>
<div class="md-media-sm card-media" flex>
<md-checkbox class="md-primary" ng-model="certification.requiresAudit">
Requires Audit
</md-checkbox>
<md-input-container class="md-block">
<label>Number</label>
<input ng-model="certification.number" />
</md-input-container>
<md-input-container>
<label>Expiration</label>
<md-datepicker ng-model="certification.expirationDate"></md-datepicker>
</md-input-container>
<md-input-container class="md-block">
<label>Notes</label>
<textarea ng-model="certification.notes"></textarea>
</md-input-container>
</div>
</md-card-content>
</md-card>
</div>
</md-tab-body>
</md-tab>
</md-tabs>


and here's my logic:

(function () {
angular.module('ASLApp').controller('AddEditSupplierController', AddEditSupplierController);

function AddEditSupplierController(addMode, $scope, $routeParams, $mdDialog, RandomService, SupplierService, CertificationTypeService) {
var vm = this;

vm.save = function (evt) {
vm.loading = true;
SupplierService.update(vm.supplier).then(function (response) {
vm.supplier = response.data;
parseDates();
}, function (response) {
if (response.data && response.data.Errors && response.data.Errors.length > 0 && response.data.Errors[0].number === 2627) {
$mdDialog.show(
$mdDialog.alert()
.clickOutsideToClose(true)
.title('Duplicate Supplier Id Entry Found')
.textContent('Another supplier entry was found with the same Id.')
.ok('Ok')
);
}
}).finally(function () {
vm.loading = false;
});
};

vm.addSupplierCertification = function (evt) {
if (!vm.supplier.supplierCertifications) {
vm.supplier.supplierCertifications = [];
}
vm.supplier.supplierCertifications.push(vm.newSupplierCertification);
vm.newSupplierCertification = {
certificationTypeId: vm.certificationTypes[0].id,
tempId: RandomService.guid()
};
};

vm.generateId = function (evt) {
SupplierService.generateId(vm.supplier.name).then(function (response) {
vm.supplier.id = response.data;
});
};

vm.showAddCertificationTypeDialog = function (evt) {
$mdDialog.show({
scope: $scope,
preserveScope: true,
templateUrl: 'app/views/AddCertificationTypeDialog.html',
parent: angular.element(document.body),
targetEvent: evt
});
};

vm.cancelDialog = function (evt) {
$mdDialog.cancel();
};

vm.addCertificationType = function () {
CertificationTypeService.add(vm.newCertificationType).then(function (response) {
vm.newCertificationType = {};
getCertificationTypes();
$mdDialog.hide();
});
};

function init() {
vm.addMode = addMode;
if (!addMode) {
getSupplier($routeParams.id);
}
getCertificationTypes();
}

function getSupplier(id) {
vm.loading = true;
SupplierService.get(id).then(function (response) {
vm.supplier = response.data;
parseDates();
}).finally(function () {
vm.loading = false;
});
}

function getCertificationTypes() {
CertificationTypeService.getAll().then(function (response) {
if (vm.certificationTypes)
delete vm.certificationTypes;

vm.certificationTypes = response.data;

vm.newSupplierCertification = {
certificationTypeId: vm.certificationTypes[0].id,
tempId: RandomService.guid()
};
});
}

function parseDates() {
if (vm.supplier.dateLastReview) {
vm.supplier.dateLastReview = new Date(vm.supplier.dateLastReview);
}

if (vm.supplier.dateNextReview) {
vm.supplier.dateNextReview = new Date(vm.supplier.dateNextReview);
}

if (vm.supplier.dateLastAudit) {
vm.supplier.dateLastAudit = new Date(vm.supplier.dateLastAudit);
}

if (vm.supplier.dateNextAudit) {
vm.supplier.dateNextAudit = new Date(vm.supplier.dateNextAudit);
}

if (vm.supplier.supplierCertifications) {
angular.forEach(vm.supplier.supplierCertifications, function (certification) {
if (certification.expirationDate) {
certification.expirationDate = new Date(certification.expirationDate);
}
});
}
}

init();
}

AddEditSupplierController.$inject = ['addMode', '$scope', '$routeParams', '$mdDialog', 'RandomService', 'SupplierService', 'CertificationTypeService'];
}());


At one point during my troubleshooting, I removed the other tabs and after adding a new item to the list, it showed the multiple selections for a half second, but then it updated to show properly. This makes me wonder if there is some sort of
debounce
happening. Being able to reproduce this on my codepen would be extremely helpful in narrowing down the issue which I suspect to be related to timing of events. Any assistance would be appreciated!

Troubleshooting update:
I tried adding a
$timeout
call to my
getCertificationTypes
method with no results, so I doubled the call to
getCertificationTypes
. It added another duplicate to the selected value label.

vm.addCertificationType = function () {
CertificationTypeService.add(vm.newCertificationType).then(function (response) {
vm.newCertificationType = {};
$timeout(getCertificationTypes, 1000);
$timeout(getCertificationTypes, 1000);
//getCertificationTypes();
$mdDialog.hide();
});
};


enter image description here

Answer

After the discussion in the comments and in the chat, the problem is with the md-select when the reference of the array that prints md-option changes the model is not updated and there is anomaly in the preview of md-select as you can see in the question. That's because ng-repeat rerenders all of the md-option and there is bug in angular material that doesn't handle this use case properly.

The solution is to add track by property in the ng-repeat so the whole list is not rerendered

<md-select ng-model="certification.certificationTypeId">
    <md-option ng-repeat="certificationType in addEditSupplierCtrl.certificationTypes track by certificationType.id" value="{{ certificationType.id }}">
           {{ certificationType.name }}
     </md-option>
</md-select>