Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit 74d2445

Browse files
Splaktarjelbourn
authored andcommitted
fix(chips): regression where chips model gets out of sync with view (#11310)
manually call `ngChange` as it won't be triggered by `ngModel` for chips add `ng-change` test to contact-chips refinements to chips and contact-chips docs remove references to `filter-selected` on contact-chips it was disabled 2 years ago remove use of `angular.lowercase` which is removed in AngularJS 1.7 Fixes #11304. Fixes #11301.
1 parent a85f038 commit 74d2445

File tree

13 files changed

+136
-64
lines changed

13 files changed

+136
-64
lines changed

src/components/autocomplete/demoBasicUsage/script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
* Create filter function for a query string
7575
*/
7676
function createFilterFor(query) {
77-
var lowercaseQuery = angular.lowercase(query);
77+
var lowercaseQuery = query.toLowerCase();
7878

7979
return function filterFn(state) {
8080
return (state.value.indexOf(lowercaseQuery) === 0);

src/components/autocomplete/demoCustomTemplate/script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
* Create filter function for a query string
9696
*/
9797
function createFilterFor(query) {
98-
var lowercaseQuery = angular.lowercase(query);
98+
var lowercaseQuery = query.toLowerCase();
9999

100100
return function filterFn(item) {
101101
return (item.value.indexOf(lowercaseQuery) === 0);

src/components/autocomplete/demoFloatingLabel/script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
* Create filter function for a query string
5353
*/
5454
function createFilterFor(query) {
55-
var lowercaseQuery = angular.lowercase(query);
55+
var lowercaseQuery = query.toLowerCase();
5656

5757
return function filterFn(state) {
5858
return (state.value.indexOf(lowercaseQuery) === 0);

src/components/autocomplete/demoInsideDialog/script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
* Create filter function for a query string
7474
*/
7575
function createFilterFor(query) {
76-
var lowercaseQuery = angular.lowercase(query);
76+
var lowercaseQuery = query.toLowerCase();
7777

7878
return function filterFn(state) {
7979
return (state.value.indexOf(lowercaseQuery) === 0);

src/components/chips/chips.spec.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('<md-chips>', function() {
3939
describe('with no overrides', function() {
4040
beforeEach(module('material.components.chips', 'material.components.autocomplete'));
4141
beforeEach(inject(function($rootScope, _$exceptionHandler_, _$timeout_) {
42-
scope = $rootScope.$new();
42+
scope = $rootScope.$new(false);
4343
scope.items = ['Apple', 'Banana', 'Orange'];
4444
$exceptionHandler = _$exceptionHandler_;
4545
$timeout = _$timeout_;
@@ -177,6 +177,30 @@ describe('<md-chips>', function() {
177177
expect(scope.addChip.calls.mostRecent().args[1]).toBe(3); // Index
178178
});
179179

180+
it('should update the view if the add method changes or removes the chip', function() {
181+
var element = buildChips(CHIP_ADD_TEMPLATE);
182+
var ctrl = element.controller('mdChips');
183+
184+
scope.addChip = function ($chip, $index) {
185+
if ($chip === 'Grape') {
186+
var grape = scope.items.pop();
187+
grape += '[' + $index + ']';
188+
scope.items.push(grape);
189+
}
190+
if ($chip === 'Broccoli') {
191+
scope.items.pop();
192+
}
193+
};
194+
195+
element.scope().$apply(function() {
196+
ctrl.chipBuffer = 'Broccoli';
197+
simulateInputEnterKey(ctrl);
198+
ctrl.chipBuffer = 'Grape';
199+
simulateInputEnterKey(ctrl);
200+
});
201+
202+
expect(scope.items[3]).toBe('Grape[3]');
203+
});
180204

181205
it('should call the remove method when removing a chip', function() {
182206
var element = buildChips(CHIP_REMOVE_TEMPLATE);
@@ -217,16 +241,17 @@ describe('<md-chips>', function() {
217241
simulateInputEnterKey(ctrl);
218242
});
219243
expect(scope.onModelChange).toHaveBeenCalled();
244+
expect(scope.onModelChange.calls.count()).toBe(1);
220245
expect(scope.onModelChange.calls.mostRecent().args[0].length).toBe(4);
221246

222247
element.scope().$apply(function() {
223248
ctrl.removeChip(0);
224249
});
225250
expect(scope.onModelChange).toHaveBeenCalled();
251+
expect(scope.onModelChange.calls.count()).toBe(2);
226252
expect(scope.onModelChange.calls.mostRecent().args[0].length).toBe(3);
227253
});
228254

229-
230255
it('should call the select method when selecting a chip', function() {
231256
var element = buildChips(CHIP_SELECT_TEMPLATE);
232257
var ctrl = element.controller('mdChips');
@@ -694,7 +719,8 @@ describe('<md-chips>', function() {
694719
input.val(' Test ');
695720

696721
// We have to trigger the `change` event, because IE11 does not support
697-
// the `input` event to update the ngModel. An alternative for `input` is to use the `change` event.
722+
// the `input` event to update the ngModel. An alternative for `input` is to use the
723+
// `change` event.
698724
input.triggerHandler('change');
699725

700726
expect(ctrl.chipBuffer).toBeTruthy();

src/components/chips/contact-chips.spec.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ describe('<md-contact-chips>', function() {
1010
md-highlight-flags="i"\
1111
md-min-length="1"\
1212
md-chip-append-delay="2000"\
13+
ng-change="onModelChange(contacts)"\
1314
placeholder="To">\
1415
</md-contact-chips>';
1516

1617
beforeEach(module('material.components.chips'));
1718

1819
beforeEach(inject(function($rootScope) {
19-
scope = $rootScope.$new();
20+
scope = $rootScope.$new(false);
2021
var img = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
2122
scope.allContacts = [
2223
{
@@ -35,7 +36,7 @@ describe('<md-contact-chips>', function() {
3536
];
3637
scope.contacts = [];
3738

38-
scope.highlightFlags = "i";
39+
scope.highlightFlags = 'i';
3940
}));
4041

4142
var attachedElements = [];
@@ -65,6 +66,31 @@ describe('<md-contact-chips>', function() {
6566
expect(ctrl.highlightFlags).toEqual('i');
6667
});
6768

69+
it('should trigger ng-change on chip addition/removal', function() {
70+
var element = buildChips(CONTACT_CHIPS_TEMPLATE);
71+
var ctrl = element.controller('mdContactChips');
72+
var chipsElement = element.find('md-chips');
73+
var chipsCtrl = chipsElement.controller('mdChips');
74+
75+
scope.onModelChange = jasmine.createSpy('onModelChange');
76+
77+
element.scope().$apply(function() {
78+
chipsCtrl.appendChip(scope.allContacts[0]);
79+
});
80+
expect(scope.onModelChange).toHaveBeenCalled();
81+
expect(scope.onModelChange.calls.count()).toBe(1);
82+
expect(scope.onModelChange.calls.mostRecent().args[0].length).toBe(1);
83+
expect(scope.contacts.length).toBe(1);
84+
85+
element.scope().$apply(function() {
86+
chipsCtrl.removeChip(0);
87+
});
88+
expect(scope.onModelChange).toHaveBeenCalled();
89+
expect(scope.onModelChange.calls.count()).toBe(2);
90+
expect(scope.onModelChange.calls.mostRecent().args[0].length).toBe(0);
91+
expect(scope.contacts.length).toBe(0);
92+
});
93+
6894
it('forwards the md-chips-append-delay attribute to the md-chips', function() {
6995
var element = buildChips(CONTACT_CHIPS_TEMPLATE);
7096
var chipsCtrl = element.find('md-chips').controller('mdChips');
@@ -167,4 +193,12 @@ describe('<md-contact-chips>', function() {
167193
return container;
168194
}
169195

196+
function simulateInputEnterKey(ctrl) {
197+
var event = {};
198+
event.preventDefault = jasmine.createSpy('preventDefault');
199+
inject(function($mdConstant) {
200+
event.keyCode = $mdConstant.KEY_CODE.ENTER;
201+
});
202+
ctrl.inputKeydown(event);
203+
}
170204
});

src/components/chips/demoContactChips/index.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
md-contact-email="email"
1111
md-require-match="true"
1212
md-highlight-flags="i"
13-
filter-selected="ctrl.filterSelected"
1413
placeholder="To">
1514
</md-contact-chips>
1615

@@ -43,7 +42,6 @@ <h2 class="md-title">Searching asynchronously.</h2>
4342
md-contact-email="email"
4443
md-require-match="true"
4544
md-highlight-flags="i"
46-
filter-selected="ctrl.filterSelected"
4745
placeholder="To">
4846
</md-contact-chips>
4947
</md-content>

src/components/chips/demoContactChips/script.js

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
'use strict';
33

44
// If we do not have CryptoJS defined; import it
5-
if (typeof CryptoJS == 'undefined') {
5+
if (typeof CryptoJS === 'undefined') {
66
var cryptoSrc = 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/md5.js';
77
var scriptTag = document.createElement('script');
88
scriptTag.setAttribute('src', cryptoSrc);
@@ -13,15 +13,14 @@
1313
.module('contactChipsDemo', ['ngMaterial'])
1414
.controller('ContactChipDemoCtrl', DemoCtrl);
1515

16-
function DemoCtrl ($q, $timeout) {
16+
function DemoCtrl ($q, $timeout, $log) {
1717
var self = this;
1818
var pendingSearch, cancelSearch = angular.noop;
1919
var lastSearch;
2020

2121
self.allContacts = loadContacts();
2222
self.contacts = [self.allContacts[0]];
2323
self.asyncContacts = [];
24-
self.filterSelected = true;
2524

2625
self.querySearch = querySearch;
2726
self.delayedQuerySearch = delayedQuerySearch;
@@ -39,7 +38,7 @@
3938
* Also debounce the queries; since the md-contact-chips does not support this
4039
*/
4140
function delayedQuerySearch(criteria) {
42-
if ( !pendingSearch || !debounceSearch() ) {
41+
if (!pendingSearch || !debounceSearch()) {
4342
cancelSearch();
4443

4544
return pendingSearch = $q(function(resolve, reject) {
@@ -77,16 +76,16 @@
7776
* Create filter function for a query string
7877
*/
7978
function createFilterFor(query) {
80-
var lowercaseQuery = angular.lowercase(query);
79+
var lowercaseQuery = query.toLowerCase();
8180

8281
return function filterFn(contact) {
83-
return (contact._lowername.indexOf(lowercaseQuery) != -1);
82+
return (contact._lowername.indexOf(lowercaseQuery) !== -1);
8483
};
8584

8685
}
8786

88-
function onModelChange(model) {
89-
alert('The model has changed');
87+
function onModelChange(newModel) {
88+
$log.log('The model has changed to ' + JSON.stringify(newModel) + '.');
9089
}
9190

9291
function loadContacts() {
@@ -117,6 +116,4 @@
117116
});
118117
}
119118
}
120-
121-
122119
})();

src/components/chips/demoCustomInputs/script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
* Create filter function for a query string
4545
*/
4646
function createFilterFor(query) {
47-
var lowercaseQuery = angular.lowercase(query);
47+
var lowercaseQuery = query.toLowerCase();
4848

4949
return function filterFn(vegetable) {
5050
return (vegetable._lowername.indexOf(lowercaseQuery) === 0) ||

src/components/chips/js/chipsController.js

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ angular
2121
* @param $element
2222
* @param $timeout
2323
* @param $mdUtil
24+
* @param $exceptionHandler
2425
* @constructor
2526
*/
26-
function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $mdUtil) {
27+
function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $mdUtil,
28+
$exceptionHandler) {
2729
/** @type {$timeout} **/
2830
this.$timeout = $timeout;
2931

@@ -42,6 +44,9 @@ function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $md
4244
/** @type {$log} */
4345
this.$log = $log;
4446

47+
/** @type {$exceptionHandler} */
48+
this.$exceptionHandler = $exceptionHandler;
49+
4550
/** @type {$element} */
4651
this.$element = $element;
4752

@@ -142,10 +147,10 @@ function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $md
142147
this.contentIds = [];
143148

144149
/**
145-
* The index of the chip that should have it's tabindex property set to 0 so it is selectable
150+
* The index of the chip that should have it's `tabindex` property set to `0` so it is selectable
146151
* via the keyboard.
147152
*
148-
* @type {int}
153+
* @type {number}
149154
*/
150155
this.ariaTabIndex = null;
151156

@@ -326,9 +331,9 @@ MdChipsCtrl.prototype.getCursorPosition = function(element) {
326331
* @param chipContents
327332
*/
328333
MdChipsCtrl.prototype.updateChipContents = function(chipIndex, chipContents){
329-
if(chipIndex >= 0 && chipIndex < this.items.length) {
334+
if (chipIndex >= 0 && chipIndex < this.items.length) {
330335
this.items[chipIndex] = chipContents;
331-
this.updateNgModel();
336+
this.updateNgModel(true);
332337
}
333338
};
334339

@@ -454,8 +459,7 @@ MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) {
454459
/**
455460
* Append the contents of the buffer to the chip list. This method will first
456461
* call out to the md-transform-chip method, if provided.
457-
*
458-
* @param newChip
462+
* @param {string} newChip chip buffer contents that will be used to create the new chip
459463
*/
460464
MdChipsCtrl.prototype.appendChip = function(newChip) {
461465
this.shouldFocusLastChip = true;
@@ -486,7 +490,7 @@ MdChipsCtrl.prototype.appendChip = function(newChip) {
486490

487491
this.updateNgModel();
488492

489-
// If they provide the md-on-add attribute, notify them of the chip addition
493+
// If the md-on-add attribute is specified, send a chip addition event
490494
if (this.useOnAdd && this.onAdd) {
491495
this.onAdd({ '$chip': newChip, '$index': index });
492496
}
@@ -549,7 +553,8 @@ MdChipsCtrl.prototype.getChipBuffer = function() {
549553
this.userInputNgModelCtrl ? this.userInputNgModelCtrl.$viewValue :
550554
this.userInputElement[0].value;
551555

552-
// Ensure that the chip buffer is always a string. For example, the input element buffer might be falsy.
556+
// Ensure that the chip buffer is always a string. For example, the input element buffer
557+
// might be falsy.
553558
return angular.isString(chipBuffer) ? chipBuffer : '';
554559
};
555560

@@ -577,23 +582,38 @@ MdChipsCtrl.prototype.hasMaxChipsReached = function() {
577582

578583
/**
579584
* Updates the validity properties for the ngModel.
585+
*
586+
* TODO add the md-max-chips validator to this.ngModelCtrl.validators so that the validation will
587+
* be performed automatically.
580588
*/
581589
MdChipsCtrl.prototype.validateModel = function() {
582590
this.ngModelCtrl.$setValidity('md-max-chips', !this.hasMaxChipsReached());
583591
this.ngModelCtrl.$validate(); // rerun any registered validators
584592
};
585593

586-
MdChipsCtrl.prototype.updateNgModel = function() {
587-
this.ngModelCtrl.$setViewValue(this.items.slice());
588-
// TODO add the md-max-chips validator to this.ngModelCtrl.validators so that
589-
// the validation will be performed automatically on $viewValue change
590-
this.validateModel();
594+
/**
595+
* Function to handle updating the model, validation, and change notification when a chip
596+
* is added, removed, or changed.
597+
* @param {boolean=} skipValidation true to skip calling validateModel()
598+
*/
599+
MdChipsCtrl.prototype.updateNgModel = function(skipValidation) {
600+
if (!skipValidation) {
601+
this.validateModel();
602+
}
603+
// This will trigger ng-change to fire, even in cases where $setViewValue() would not.
604+
angular.forEach(this.ngModelCtrl.$viewChangeListeners, function(listener) {
605+
try {
606+
listener();
607+
} catch (e) {
608+
this.$exceptionHandler(e);
609+
}
610+
});
591611
};
592612

593613
/**
594614
* Removes the chip at the given index.
595-
* @param {number} index
596-
* @param {Event=} event
615+
* @param {number} index of chip to remove
616+
* @param {Event=} event optionally passed to the onRemove callback
597617
*/
598618
MdChipsCtrl.prototype.removeChip = function(index, event) {
599619
var removed = this.items.splice(index, 1);
@@ -705,7 +725,7 @@ MdChipsCtrl.prototype.focusChip = function(index) {
705725

706726
/**
707727
* Configures the required interactions with the ngModel Controller.
708-
* Specifically, set {@code this.items} to the {@code NgModelCtrl#$viewVale}.
728+
* Specifically, set {@code this.items} to the {@code NgModelCtrl#$viewValue}.
709729
* @param ngModelCtrl
710730
*/
711731
MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) {

0 commit comments

Comments
 (0)