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

Commit

Permalink
feat(chips): Support multiple return values for md-on-append.
Browse files Browse the repository at this point in the history
The usage of `md-on-append` is not well documented and the
behavior is not consistent.

Fix by updating documentation to set expectations of return
values and updating code to conform to their associated behavior.

Additionally, this adds support for simlultaneously using an
autocomplete selection along with the ability to create new chips.

Previously, the chips directive would not allow for a scenario
which used the autocomplete to provide a list of options, but
also provided a method of inputting new options. The most common
case for this was a tag system which showed existing tags, but
allowed you to create new ones.

Update autocomplete and chips to provide both scenarios and
document how this can be achieved.

Lastly, workaround a few display issues with contact chips demo
(#4450).

_**Note:** This work supercedes PR #3816 which can be closed when
this is merged._

Fixes #4666. Fixes #4193. Fixes #4412. Fixes #4863.
  • Loading branch information
topherfangio committed Nov 3, 2015
1 parent fcd199e commit 9f815a2
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 25 deletions.
37 changes: 37 additions & 0 deletions src/components/autocomplete/demoInsideDialog/dialog.tmpl.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<md-dialog aria-label="Autocomplete Dialog Example" ng-cloak>
<md-toolbar>
<div class="md-toolbar-tools">
<h2>Autocomplete Dialog Example</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="ctrl.cancel()">
<md-icon md-svg-src="img/icons/ic_close_24px.svg" aria-label="Close dialog"></md-icon>
</md-button>
</div>
</md-toolbar>

<md-dialog-content>
<div class="md-dialog-content">
<form ng-submit="$event.preventDefault()">
<p>Use <code>md-autocomplete</code> to search for matches from local or remote data sources.</p>
<md-autocomplete
md-selected-item="ctrl.selectedItem"
md-search-text="ctrl.searchText"
md-items="item in ctrl.querySearch(ctrl.searchText)"
md-item-text="item.display"
md-min-length="0"
placeholder="What is your favorite US state?">
<md-item-template>
<span md-highlight-text="ctrl.searchText" md-highlight-flags="^i">{{item.display}}</span>
</md-item-template>
<md-not-found>
No states matching "{{ctrl.searchText}}" were found.
</md-not-found>
</md-autocomplete>
</form>
</div>
</md-dialog-content>

<div class="md-actions">
<md-button aria-label="Finished" ng-click="ctrl.finish($event)">Finished</md-button>
</div>
</md-dialog>
9 changes: 9 additions & 0 deletions src/components/autocomplete/demoInsideDialog/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div ng-controller="DemoCtrl as ctrl" layout="column" ng-cloak>
<md-content class="md-padding">
<p>
Click the button below to open the dialog with an autocomplete.
</p>

<md-button ng-click="ctrl.openDialog($event)" class="md-raised">Open Dialog</md-button>
</md-content>
</div>
84 changes: 84 additions & 0 deletions src/components/autocomplete/demoInsideDialog/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
(function () {
'use strict';
angular
.module('autocompleteDemoInsideDialog', ['ngMaterial'])
.controller('DemoCtrl', DemoCtrl);

function DemoCtrl($mdDialog) {
var self = this;

self.openDialog = function($event) {
$mdDialog.show({
controller: DialogCtrl,
controllerAs: 'ctrl',
templateUrl: 'dialog.tmpl.html',
parent: angular.element(document.body),
targetEvent: $event,
clickOutsideToClose:true
})
}
}

function DialogCtrl ($timeout, $q, $scope, $mdDialog) {
var self = this;

// list of `state` value/display objects
self.states = loadAll();
self.querySearch = querySearch;

// ******************************
// Template methods
// ******************************

self.cancel = function($event) {
$mdDialog.cancel();
};
self.finish = function($event) {
$mdDialog.hide();
};

// ******************************
// Internal methods
// ******************************

/**
* Search for states... use $timeout to simulate
* remote dataservice call.
*/
function querySearch (query) {
return query ? self.states.filter( createFilterFor(query) ) : self.states;
}

/**
* Build `states` list of key/value pairs
*/
function loadAll() {
var allStates = 'Alabama, Alaska, Arizona, Arkansas, California, Colorado, Connecticut, Delaware,\
Florida, Georgia, Hawaii, Idaho, Illinois, Indiana, Iowa, Kansas, Kentucky, Louisiana,\
Maine, Maryland, Massachusetts, Michigan, Minnesota, Mississippi, Missouri, Montana,\
Nebraska, Nevada, New Hampshire, New Jersey, New Mexico, New York, North Carolina,\
North Dakota, Ohio, Oklahoma, Oregon, Pennsylvania, Rhode Island, South Carolina,\
South Dakota, Tennessee, Texas, Utah, Vermont, Virginia, Washington, West Virginia,\
Wisconsin, Wyoming';

return allStates.split(/, +/g).map( function (state) {
return {
value: state.toLowerCase(),
display: state
};
});
}

/**
* Create filter function for a query string
*/
function createFilterFor(query) {
var lowercaseQuery = angular.lowercase(query);

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

}
}
})();
2 changes: 1 addition & 1 deletion src/components/autocomplete/js/autocompleteController.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,9 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
select(ctrl.index);
break;
case $mdConstant.KEY_CODE.ENTER:
if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
event.stopPropagation();
event.preventDefault();
if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
select(ctrl.index);
break;
case $mdConstant.KEY_CODE.ESCAPE:
Expand Down
112 changes: 112 additions & 0 deletions src/components/chips/chips.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,44 @@ describe('<md-chips>', function() {
expect(scope.items[3]).toBe('GrapeGrape');
});

it('should add the chip if md-on-append is used only as a notifier (i.e. it returns nothing)', function() {
var element = buildChips(CHIP_APPEND_TEMPLATE);
var ctrl = element.controller('mdChips');

var noReturn = function(text) {
};
scope.appendChip = jasmine.createSpy('appendChip').and.callFake(noReturn);

element.scope().$apply(function() {
ctrl.chipBuffer = 'Grape';
simulateInputEnterKey(ctrl);
});

expect(scope.appendChip).toHaveBeenCalled();
expect(scope.appendChip.calls.mostRecent().args[0]).toBe('Grape');
expect(scope.items.length).toBe(4);
expect(scope.items[3]).toBe('Grape');
});

it('should not add the chip if md-on-append returns null', function() {
var element = buildChips(CHIP_APPEND_TEMPLATE);
var ctrl = element.controller('mdChips');

var nullChip = function(text) {
return null;
};
scope.appendChip = jasmine.createSpy('appendChip').and.callFake(nullChip);

element.scope().$apply(function() {
ctrl.chipBuffer = 'Grape';
simulateInputEnterKey(ctrl);
});

expect(scope.appendChip).toHaveBeenCalled();
expect(scope.appendChip.calls.mostRecent().args[0]).toBe('Grape');
expect(scope.items.length).toBe(3);
});

it('should call the remove method when removing a chip', function() {
var element = buildChips(CHIP_REMOVE_TEMPLATE);
var ctrl = element.controller('mdChips');
Expand Down Expand Up @@ -328,6 +366,80 @@ describe('<md-chips>', function() {
expect(scope.items[3]).toBe('Kiwi');
expect(element.find('input').val()).toBe('');
}));

it('simultaneously allows selecting an existing chip AND adding a new one', inject(function($mdConstant) {
// Setup our scope and function
setupScopeForAutocomplete();
scope.onAppend = jasmine.createSpy('onAppend');

// Modify the base template to add md-on-append
var modifiedTemplate = AUTOCOMPLETE_CHIPS_TEMPLATE
.replace('<md-chips', '<md-chips md-on-append="onAppend($chip)"');

var element = buildChips(modifiedTemplate);

var ctrl = element.controller('mdChips');
$timeout.flush(); // mdAutcomplete needs a flush for its init.
var autocompleteCtrl = element.find('md-autocomplete').controller('mdAutocomplete');

element.scope().$apply(function() {
autocompleteCtrl.scope.searchText = 'K';
});
autocompleteCtrl.focus();
$timeout.flush();

/*
* Send a down arrow/enter to select the right fruit
*/
var downArrowEvent = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.DOWN_ARROW,
which: $mdConstant.KEY_CODE.DOWN_ARROW
};
var enterEvent = {
type: 'keydown',
keyCode: $mdConstant.KEY_CODE.ENTER,
which: $mdConstant.KEY_CODE.ENTER
};
element.find('input').triggerHandler(downArrowEvent);
element.find('input').triggerHandler(enterEvent);
$timeout.flush();

// Check our onAppend calls
expect(scope.onAppend).not.toHaveBeenCalledWith('K');
expect(scope.onAppend).toHaveBeenCalledWith('Kiwi');
expect(scope.onAppend.calls.count()).toBe(1);

// Check our output
expect(scope.items.length).toBe(4);
expect(scope.items[3]).toBe('Kiwi');
expect(element.find('input').val()).toBe('');

// Reset our jasmine spy
scope.onAppend.calls.reset();

/*
* Use the "new chip" functionality
*/

// Set the search text
element.scope().$apply(function() {
autocompleteCtrl.scope.searchText = 'Acai Berry';
});

// Fire our event and flush any timeouts
element.find('input').triggerHandler(enterEvent);
$timeout.flush();

// Check our onAppend calls
expect(scope.onAppend).toHaveBeenCalledWith('Acai Berry');
expect(scope.onAppend.calls.count()).toBe(1);

// Check our output
expect(scope.items.length).toBe(5);
expect(scope.items[4]).toBe('Acai Berry');
expect(element.find('input').val()).toBe('');
}));
});

describe('user input templates', function() {
Expand Down
7 changes: 7 additions & 0 deletions src/components/chips/demoContactChips/style.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
md-content.autocomplete {
min-height: 250px;

// NOTE: Due to a bug with the virtual repeat sizing, we must manually set the width of
// the input so that the autocomplete popup will be properly sized. See issue #4450.
input {
min-width: 400px;
}
}
.md-item-text.compact {
padding-top: 8px;
Expand All @@ -15,6 +21,7 @@ md-content.autocomplete {
}
.md-list-item-text {
padding: 14px 0;
max-width: 190px;
h3 {
margin: 0 !important;
padding: 0;
Expand Down
8 changes: 7 additions & 1 deletion src/components/chips/demoCustomInputs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ <h2 class="md-title">Use an <code>input</code> element to build an ordered set
<br/>
<h2 class="md-title">Use <code>md-autocomplete</code> to build an ordered set of chips.</h2>

<md-chips ng-model="ctrl.selectedVegetables" md-autocomplete-snap md-require-match="true">
<md-chips ng-model="ctrl.selectedVegetables" md-autocomplete-snap
md-on-append="ctrl.onAppend($chip)"
md-require-match="ctrl.autocompleteDemoRequireMatch">
<md-autocomplete
md-selected-item="ctrl.selectedItem"
md-search-text="ctrl.searchText"
Expand All @@ -32,6 +34,10 @@ <h2 class="md-title">Use <code>md-autocomplete</code> to build an ordered set o
</md-chip-template>
</md-chips>

<md-checkbox ng-model="ctrl.autocompleteDemoRequireMatch">
Tell the autocomplete to require a match (when enabled you cannot create new chips)
</md-checkbox>

<br />
<h2 class="md-title">Vegetable Options</h2>

Expand Down
15 changes: 15 additions & 0 deletions src/components/chips/demoCustomInputs/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@
self.numberChips = [];
self.numberChips2 = [];
self.numberBuffer = '';
self.autocompleteDemoRequireMatch = true;
self.onAppend = onAppend;

/**
* Return the proper object when the append is called.
*/
function onAppend(chip) {
// If it is an object, it's already a known chip
if (angular.isObject(chip)) {
return chip;
}

// Otherwise, create a new one
return { name: chip, type: 'new' }
}

/**
* Search for vegetables.
Expand Down
Loading

0 comments on commit 9f815a2

Please sign in to comment.