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

Commit d69d6d0

Browse files
topherfangioThomasBurleson
authored andcommitted
refactor(chips): Deprecate md-on-append in favor of others.
The usage of `md-on-append` was not well documented and it's behavior was inconsistent and confusing; many users assumed it was a simple notification of chip additions which caused issues. * renamed `md-on-append` to `md-transform-chip` and provided a new `md-on-add` method that is strictly a notification. * updated the docs and functionality of `md-transform-chip` to show expected return values and their associated behavior. > This new behavior also adds support for simlultaneously using an autocomplete to select an existing value along with the ability to create new chips. The most common case for this is a tag system which shows existing tags, but also allows you to create new ones. > Demos have been updated to show new functionality as well as to workaround a few display issues with the contact chips demo (#4450). _**Note:** This work supercedes PR #3816 which can be closed when this is merged._ BREAKING CHANGE: `md-on-append` has been renamed/deprecated in favor of `md-transform-chip` or the simple notifier `md-on-add`. We expect to remove this completely in 1.0, so please update your code to use one of the new methods. Fixes #4666. Fixes #4193. Fixes #4412. Fixes #4863. Closes #5497. Closes #3816.
1 parent cedb116 commit d69d6d0

File tree

11 files changed

+411
-25
lines changed

11 files changed

+411
-25
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<md-dialog aria-label="Autocomplete Dialog Example" ng-cloak>
2+
<md-toolbar>
3+
<div class="md-toolbar-tools">
4+
<h2>Autocomplete Dialog Example</h2>
5+
<span flex></span>
6+
<md-button class="md-icon-button" ng-click="ctrl.cancel()">
7+
<md-icon md-svg-src="img/icons/ic_close_24px.svg" aria-label="Close dialog"></md-icon>
8+
</md-button>
9+
</div>
10+
</md-toolbar>
11+
12+
<md-dialog-content>
13+
<div class="md-dialog-content">
14+
<form ng-submit="$event.preventDefault()">
15+
<p>Use <code>md-autocomplete</code> to search for matches from local or remote data sources.</p>
16+
<md-autocomplete
17+
md-selected-item="ctrl.selectedItem"
18+
md-search-text="ctrl.searchText"
19+
md-items="item in ctrl.querySearch(ctrl.searchText)"
20+
md-item-text="item.display"
21+
md-min-length="0"
22+
placeholder="What is your favorite US state?">
23+
<md-item-template>
24+
<span md-highlight-text="ctrl.searchText" md-highlight-flags="^i">{{item.display}}</span>
25+
</md-item-template>
26+
<md-not-found>
27+
No states matching "{{ctrl.searchText}}" were found.
28+
</md-not-found>
29+
</md-autocomplete>
30+
</form>
31+
</div>
32+
</md-dialog-content>
33+
34+
<div class="md-actions">
35+
<md-button aria-label="Finished" ng-click="ctrl.finish($event)">Finished</md-button>
36+
</div>
37+
</md-dialog>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div ng-controller="DemoCtrl as ctrl" layout="column" ng-cloak>
2+
<md-content class="md-padding">
3+
<p>
4+
Click the button below to open the dialog with an autocomplete.
5+
</p>
6+
7+
<md-button ng-click="ctrl.openDialog($event)" class="md-raised">Open Dialog</md-button>
8+
</md-content>
9+
</div>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
(function () {
2+
'use strict';
3+
angular
4+
.module('autocompleteDemoInsideDialog', ['ngMaterial'])
5+
.controller('DemoCtrl', DemoCtrl);
6+
7+
function DemoCtrl($mdDialog) {
8+
var self = this;
9+
10+
self.openDialog = function($event) {
11+
$mdDialog.show({
12+
controller: DialogCtrl,
13+
controllerAs: 'ctrl',
14+
templateUrl: 'dialog.tmpl.html',
15+
parent: angular.element(document.body),
16+
targetEvent: $event,
17+
clickOutsideToClose:true
18+
})
19+
}
20+
}
21+
22+
function DialogCtrl ($timeout, $q, $scope, $mdDialog) {
23+
var self = this;
24+
25+
// list of `state` value/display objects
26+
self.states = loadAll();
27+
self.querySearch = querySearch;
28+
29+
// ******************************
30+
// Template methods
31+
// ******************************
32+
33+
self.cancel = function($event) {
34+
$mdDialog.cancel();
35+
};
36+
self.finish = function($event) {
37+
$mdDialog.hide();
38+
};
39+
40+
// ******************************
41+
// Internal methods
42+
// ******************************
43+
44+
/**
45+
* Search for states... use $timeout to simulate
46+
* remote dataservice call.
47+
*/
48+
function querySearch (query) {
49+
return query ? self.states.filter( createFilterFor(query) ) : self.states;
50+
}
51+
52+
/**
53+
* Build `states` list of key/value pairs
54+
*/
55+
function loadAll() {
56+
var allStates = 'Alabama, Alaska, Arizona, Arkansas, California, Colorado, Connecticut, Delaware,\
57+
Florida, Georgia, Hawaii, Idaho, Illinois, Indiana, Iowa, Kansas, Kentucky, Louisiana,\
58+
Maine, Maryland, Massachusetts, Michigan, Minnesota, Mississippi, Missouri, Montana,\
59+
Nebraska, Nevada, New Hampshire, New Jersey, New Mexico, New York, North Carolina,\
60+
North Dakota, Ohio, Oklahoma, Oregon, Pennsylvania, Rhode Island, South Carolina,\
61+
South Dakota, Tennessee, Texas, Utah, Vermont, Virginia, Washington, West Virginia,\
62+
Wisconsin, Wyoming';
63+
64+
return allStates.split(/, +/g).map( function (state) {
65+
return {
66+
value: state.toLowerCase(),
67+
display: state
68+
};
69+
});
70+
}
71+
72+
/**
73+
* Create filter function for a query string
74+
*/
75+
function createFilterFor(query) {
76+
var lowercaseQuery = angular.lowercase(query);
77+
78+
return function filterFn(state) {
79+
return (state.value.indexOf(lowercaseQuery) === 0);
80+
};
81+
82+
}
83+
}
84+
})();

src/components/autocomplete/js/autocompleteController.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,9 +412,9 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
412412
select(ctrl.index);
413413
break;
414414
case $mdConstant.KEY_CODE.ENTER:
415+
if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
415416
event.stopPropagation();
416417
event.preventDefault();
417-
if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
418418
select(ctrl.index);
419419
break;
420420
case $mdConstant.KEY_CODE.ESCAPE:

src/components/chips/chips.spec.js

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ describe('<md-chips>', function() {
44

55
var BASIC_CHIP_TEMPLATE =
66
'<md-chips ng-model="items"></md-chips>';
7+
var CHIP_TRANSFORM_TEMPLATE =
8+
'<md-chips ng-model="items" md-transform-chip="transformChip($chip)"></md-chips>';
79
var CHIP_APPEND_TEMPLATE =
810
'<md-chips ng-model="items" md-on-append="appendChip($chip)"></md-chips>';
11+
var CHIP_ADD_TEMPLATE =
12+
'<md-chips ng-model="items" md-on-add="addChip($chip, $index)"></md-chips>';
913
var CHIP_REMOVE_TEMPLATE =
1014
'<md-chips ng-model="items" md-on-remove="removeChip($chip, $index)"></md-chips>';
1115
var CHIP_SELECT_TEMPLATE =
@@ -109,7 +113,15 @@ describe('<md-chips>', function() {
109113
expect(chips[1].innerHTML).toContain('Orange');
110114
});
111115

112-
it('should call the append method when adding a chip', function() {
116+
// TODO: Remove in 1.0 release after deprecation
117+
it('should warn of deprecation when using md-on-append', inject(function($log) {
118+
spyOn($log, 'warn');
119+
buildChips(CHIP_APPEND_TEMPLATE);
120+
expect($log.warn).toHaveBeenCalled();
121+
}));
122+
123+
// TODO: Remove in 1.0 release after deprecation
124+
it('should retain the deprecated md-on-append functionality until removed', function() {
113125
var element = buildChips(CHIP_APPEND_TEMPLATE);
114126
var ctrl = element.controller('mdChips');
115127

@@ -129,6 +141,62 @@ describe('<md-chips>', function() {
129141
expect(scope.items[3]).toBe('GrapeGrape');
130142
});
131143

144+
it('should call the transform method when adding a chip', function() {
145+
var element = buildChips(CHIP_TRANSFORM_TEMPLATE);
146+
var ctrl = element.controller('mdChips');
147+
148+
var doubleText = function(text) {
149+
return "" + text + text;
150+
};
151+
scope.transformChip = jasmine.createSpy('transformChip').and.callFake(doubleText);
152+
153+
element.scope().$apply(function() {
154+
ctrl.chipBuffer = 'Grape';
155+
simulateInputEnterKey(ctrl);
156+
});
157+
158+
expect(scope.transformChip).toHaveBeenCalled();
159+
expect(scope.transformChip.calls.mostRecent().args[0]).toBe('Grape');
160+
expect(scope.items.length).toBe(4);
161+
expect(scope.items[3]).toBe('GrapeGrape');
162+
});
163+
164+
it('should not add the chip if md-transform-chip returns null', function() {
165+
var element = buildChips(CHIP_TRANSFORM_TEMPLATE);
166+
var ctrl = element.controller('mdChips');
167+
168+
var nullChip = function(text) {
169+
return null;
170+
};
171+
scope.transformChip = jasmine.createSpy('transformChip').and.callFake(nullChip);
172+
173+
element.scope().$apply(function() {
174+
ctrl.chipBuffer = 'Grape';
175+
simulateInputEnterKey(ctrl);
176+
});
177+
178+
expect(scope.transformChip).toHaveBeenCalled();
179+
expect(scope.transformChip.calls.mostRecent().args[0]).toBe('Grape');
180+
expect(scope.items.length).toBe(3);
181+
});
182+
183+
it('should call the add method when adding a chip', function() {
184+
var element = buildChips(CHIP_ADD_TEMPLATE);
185+
var ctrl = element.controller('mdChips');
186+
187+
scope.addChip = jasmine.createSpy('addChip');
188+
189+
element.scope().$apply(function() {
190+
ctrl.chipBuffer = 'Grape';
191+
simulateInputEnterKey(ctrl);
192+
});
193+
194+
expect(scope.addChip).toHaveBeenCalled();
195+
expect(scope.addChip.calls.mostRecent().args[0]).toBe('Grape'); // Chip
196+
expect(scope.addChip.calls.mostRecent().args[1]).toBe(4); // Index
197+
});
198+
199+
132200
it('should call the remove method when removing a chip', function() {
133201
var element = buildChips(CHIP_REMOVE_TEMPLATE);
134202
var ctrl = element.controller('mdChips');
@@ -328,6 +396,80 @@ describe('<md-chips>', function() {
328396
expect(scope.items[3]).toBe('Kiwi');
329397
expect(element.find('input').val()).toBe('');
330398
}));
399+
400+
it('simultaneously allows selecting an existing chip AND adding a new one', inject(function($mdConstant) {
401+
// Setup our scope and function
402+
setupScopeForAutocomplete();
403+
scope.transformChip = jasmine.createSpy('transformChip');
404+
405+
// Modify the base template to add md-transform-chip
406+
var modifiedTemplate = AUTOCOMPLETE_CHIPS_TEMPLATE
407+
.replace('<md-chips', '<md-chips md-on-append="transformChip($chip)"');
408+
409+
var element = buildChips(modifiedTemplate);
410+
411+
var ctrl = element.controller('mdChips');
412+
$timeout.flush(); // mdAutcomplete needs a flush for its init.
413+
var autocompleteCtrl = element.find('md-autocomplete').controller('mdAutocomplete');
414+
415+
element.scope().$apply(function() {
416+
autocompleteCtrl.scope.searchText = 'K';
417+
});
418+
autocompleteCtrl.focus();
419+
$timeout.flush();
420+
421+
/*
422+
* Send a down arrow/enter to select the right fruit
423+
*/
424+
var downArrowEvent = {
425+
type: 'keydown',
426+
keyCode: $mdConstant.KEY_CODE.DOWN_ARROW,
427+
which: $mdConstant.KEY_CODE.DOWN_ARROW
428+
};
429+
var enterEvent = {
430+
type: 'keydown',
431+
keyCode: $mdConstant.KEY_CODE.ENTER,
432+
which: $mdConstant.KEY_CODE.ENTER
433+
};
434+
element.find('input').triggerHandler(downArrowEvent);
435+
element.find('input').triggerHandler(enterEvent);
436+
$timeout.flush();
437+
438+
// Check our transformChip calls
439+
expect(scope.transformChip).not.toHaveBeenCalledWith('K');
440+
expect(scope.transformChip).toHaveBeenCalledWith('Kiwi');
441+
expect(scope.transformChip.calls.count()).toBe(1);
442+
443+
// Check our output
444+
expect(scope.items.length).toBe(4);
445+
expect(scope.items[3]).toBe('Kiwi');
446+
expect(element.find('input').val()).toBe('');
447+
448+
// Reset our jasmine spy
449+
scope.transformChip.calls.reset();
450+
451+
/*
452+
* Use the "new chip" functionality
453+
*/
454+
455+
// Set the search text
456+
element.scope().$apply(function() {
457+
autocompleteCtrl.scope.searchText = 'Acai Berry';
458+
});
459+
460+
// Fire our event and flush any timeouts
461+
element.find('input').triggerHandler(enterEvent);
462+
$timeout.flush();
463+
464+
// Check our transformChip calls
465+
expect(scope.transformChip).toHaveBeenCalledWith('Acai Berry');
466+
expect(scope.transformChip.calls.count()).toBe(1);
467+
468+
// Check our output
469+
expect(scope.items.length).toBe(5);
470+
expect(scope.items[4]).toBe('Acai Berry');
471+
expect(element.find('input').val()).toBe('');
472+
}));
331473
});
332474

333475
describe('user input templates', function() {

src/components/chips/demoBasicUsage/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ <h2 class="md-title">Display an ordered set of objects as chips (with custom tem
3131
<p>Note: the variables <code>$chip</code> and <code>$index</code> are available in custom chip templates.</p>
3232

3333
<md-chips class="custom-chips" ng-model="ctrl.vegObjs" readonly="ctrl.readonly"
34-
md-on-append="ctrl.newVeg($chip)">
34+
md-transform-chip="ctrl.newVeg($chip)">
3535
<md-chip-template>
3636
<span>
3737
<strong>[{{$index}}] {{$chip.name}}</strong>

src/components/chips/demoContactChips/style.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
md-content.autocomplete {
22
min-height: 250px;
3+
4+
// NOTE: Due to a bug with the virtual repeat sizing, we must manually set the width of
5+
// the input so that the autocomplete popup will be properly sized. See issue #4450.
6+
input {
7+
min-width: 400px;
8+
}
39
}
410
.md-item-text.compact {
511
padding-top: 8px;
@@ -15,6 +21,7 @@ md-content.autocomplete {
1521
}
1622
.md-list-item-text {
1723
padding: 14px 0;
24+
max-width: 190px;
1825
h3 {
1926
margin: 0 !important;
2027
padding: 0;

src/components/chips/demoCustomInputs/index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ <h2 class="md-title">Use an <code>input</code> element to build an ordered set
1515
<br/>
1616
<h2 class="md-title">Use <code>md-autocomplete</code> to build an ordered set of chips.</h2>
1717

18-
<md-chips ng-model="ctrl.selectedVegetables" md-autocomplete-snap md-require-match="true">
18+
<md-chips ng-model="ctrl.selectedVegetables" md-autocomplete-snap
19+
md-transform-chip="ctrl.transformChip($chip)"
20+
md-require-match="ctrl.autocompleteDemoRequireMatch">
1921
<md-autocomplete
2022
md-selected-item="ctrl.selectedItem"
2123
md-search-text="ctrl.searchText"
@@ -32,6 +34,10 @@ <h2 class="md-title">Use <code>md-autocomplete</code> to build an ordered set o
3234
</md-chip-template>
3335
</md-chips>
3436

37+
<md-checkbox ng-model="ctrl.autocompleteDemoRequireMatch">
38+
Tell the autocomplete to require a match (when enabled you cannot create new chips)
39+
</md-checkbox>
40+
3541
<br />
3642
<h2 class="md-title">Vegetable Options</h2>
3743

src/components/chips/demoCustomInputs/script.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@
1616
self.numberChips = [];
1717
self.numberChips2 = [];
1818
self.numberBuffer = '';
19+
self.autocompleteDemoRequireMatch = true;
20+
self.transformChip = transformChip;
21+
22+
/**
23+
* Return the proper object when the append is called.
24+
*/
25+
function transformChip(chip) {
26+
// If it is an object, it's already a known chip
27+
if (angular.isObject(chip)) {
28+
return chip;
29+
}
30+
31+
// Otherwise, create a new one
32+
return { name: chip, type: 'new' }
33+
}
1934

2035
/**
2136
* Search for vegetables.

0 commit comments

Comments
 (0)