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

Commit

Permalink
refactor(chips): Deprecate md-on-append in favor of others.
Browse files Browse the repository at this point in the history
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.

After much discussion, we have renamed `md-on-append` to
`md-transform-chip` and provided a new `md-on-add` method
that is strictly a notification.

Additionally, we have 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.
  • Loading branch information
topherfangio committed Nov 5, 2015
1 parent e778cdd commit 230f176
Show file tree
Hide file tree
Showing 11 changed files with 411 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
144 changes: 143 additions & 1 deletion src/components/chips/chips.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ describe('<md-chips>', function() {

var BASIC_CHIP_TEMPLATE =
'<md-chips ng-model="items"></md-chips>';
var CHIP_TRANSFORM_TEMPLATE =
'<md-chips ng-model="items" md-transform-chip="transformChip($chip)"></md-chips>';
var CHIP_APPEND_TEMPLATE =
'<md-chips ng-model="items" md-on-append="appendChip($chip)"></md-chips>';
var CHIP_ADD_TEMPLATE =
'<md-chips ng-model="items" md-on-add="addChip($chip, $index)"></md-chips>';
var CHIP_REMOVE_TEMPLATE =
'<md-chips ng-model="items" md-on-remove="removeChip($chip, $index)"></md-chips>';
var CHIP_SELECT_TEMPLATE =
Expand Down Expand Up @@ -109,7 +113,15 @@ describe('<md-chips>', function() {
expect(chips[1].innerHTML).toContain('Orange');
});

it('should call the append method when adding a chip', function() {
// TODO: Remove in 1.0 release after deprecation
it('should warn of deprecation when using md-on-append', inject(function($log) {
spyOn($log, 'warn');
buildChips(CHIP_APPEND_TEMPLATE);
expect($log.warn).toHaveBeenCalled();
}));

// TODO: Remove in 1.0 release after deprecation
it('should retain the deprecated md-on-append functionality until removed', function() {
var element = buildChips(CHIP_APPEND_TEMPLATE);
var ctrl = element.controller('mdChips');

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

it('should call the transform method when adding a chip', function() {
var element = buildChips(CHIP_TRANSFORM_TEMPLATE);
var ctrl = element.controller('mdChips');

var doubleText = function(text) {
return "" + text + text;
};
scope.transformChip = jasmine.createSpy('transformChip').and.callFake(doubleText);

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

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

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

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

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

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

it('should call the add method when adding a chip', function() {
var element = buildChips(CHIP_ADD_TEMPLATE);
var ctrl = element.controller('mdChips');

scope.addChip = jasmine.createSpy('addChip');

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

expect(scope.addChip).toHaveBeenCalled();
expect(scope.addChip.calls.mostRecent().args[0]).toBe('Grape'); // Chip
expect(scope.addChip.calls.mostRecent().args[1]).toBe(4); // Index
});


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 +396,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.transformChip = jasmine.createSpy('transformChip');

// Modify the base template to add md-transform-chip
var modifiedTemplate = AUTOCOMPLETE_CHIPS_TEMPLATE
.replace('<md-chips', '<md-chips md-on-append="transformChip($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 transformChip calls
expect(scope.transformChip).not.toHaveBeenCalledWith('K');
expect(scope.transformChip).toHaveBeenCalledWith('Kiwi');
expect(scope.transformChip.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.transformChip.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 transformChip calls
expect(scope.transformChip).toHaveBeenCalledWith('Acai Berry');
expect(scope.transformChip.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
2 changes: 1 addition & 1 deletion src/components/chips/demoBasicUsage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ <h2 class="md-title">Display an ordered set of objects as chips (with custom tem
<p>Note: the variables <code>$chip</code> and <code>$index</code> are available in custom chip templates.</p>

<md-chips class="custom-chips" ng-model="ctrl.vegObjs" readonly="ctrl.readonly"
md-on-append="ctrl.newVeg($chip)">
md-transform-chip="ctrl.newVeg($chip)">
<md-chip-template>
<span>
<strong>[{{$index}}] {{$chip.name}}</strong>
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-transform-chip="ctrl.transformChip($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.transformChip = transformChip;

/**
* Return the proper object when the append is called.
*/
function transformChip(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 230f176

Please sign in to comment.