Autocomplete with multiple selection (MdAutocompleteTrigger customization)
- Dominant language
- TypeScript
- Stars
- 25k
- Forks
- 6.8k
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 91
Description
#### Feature proposal:
It would be nice if `MdAutocomplete` and `MdAutocompleteTrigger` supported the `multiple` attribute similar to `MdSelect`, or at least configuration options that make creating your own easier.
#### What is the expected behavior?
It should be possible to create an Autocomplete with multiple selection.
#### What is the current behavior?
`MdAutocompleteTrigger` is coded to assume single selection and is not easily customized, which makes a custom multi-autocomplete difficult to develop. Currently, you must "monkey-patch" private methods in `MdAutocompleteTrigger`.
#### Which versions of Angular, Material, OS, TypeScript, browsers are affected?
> @angular/cli: 1.0.4
> node: 6.9.1
> os: linux x64
> @angular/common: 4.2.0
> @angular/material: 2.0.0-beta.6-f89c6db
> @angular/cli: 1.0.4
#### Is there anything else we should know?
Here are my notes on the major issues I encountered in developing my own multiple Autocomplete.
In `ngAfterContentInit`, I "monkey-patch" `MdAutocompleteTrigger`. You cannot extend `MdAutocompleteTrigger` because these methods are private.
The objective here is to:
1. not deselect other `MdOption`s on selection
2. leave the Autocomplete open after option selection event.
EDIT updated for beta12
```typescript
if (this.multiple) {
const self = this;
/*
easiest to just modify this MatAutoTrigger instance to get the behavior we want.
Hopefully, material2 will support this in the future
*/
const autoTrigger: any = this.mdAutoTrigger as any;
// make a no-op so other options aren't cleared when selecting an option
autoTrigger._clearPreviousSelectedOption = () => {};
// need to override to continue getting these events
// copied from material2/src/lib/autocomplete/autocomplete-trigger.ts with CHANGEs
autoTrigger._subscribeToClosingActions = function(this: any): Subscription {
const firstStable = first.call(this._zone.onStable.asObservable());
const optionChanges = RxChain.from(this.autocomplete.options.changes)
.call(doOperator, () => this._positionStrategy.recalculateLastPosition())
// Defer emitting to the stream until the next tick, because changing
// bindings in here will cause "changed after checked" errors.
.call(delay, 0)
.result();
// When the zone is stable initially, and when the option list changes...
return RxChain.from(merge(firstStable, optionChanges))
// create a new stream of panelClosingActions, replacing any previous streams
// that were created, and flatten it so our stream only emits closing events...
.call(switchMap, () => {
this._resetActiveItem();
this.autocomplete._setVisibility();
return this.panelClosingActions;
})
// when the first closing event occurs...
// CHANGE disable first() because we want to continue getting events
// .call(first)
// set the value, close the panel, and complete.
.subscribe(event => this._setValueAndClose(event));
};
// prevent closing on select option event
autoTrigger._setValueAndClose = function(this: any, event: MatOptionSelectionChange | null): void {
if (event && event.source) {
// CHANGE don't clear selection, clear input, or change focus
// this._clearPreviousSelectedOption(event.source);
// this._setTriggerValue(event.source.value);
this._onChange(event.source.value);
// this._element.nativeElement.focus();
this.autocomplete._emitSelectEvent(event.source);
}
// CHANGE added else clause (close non-MatOptionSelectionChange event)
else {
// NOTE this is the Subscription returned from _subscribeToClosingActions
// CHANGE unsubscribe from the Subscription created in _subscribeToClosingActions
this._closingActionsSubscription.unsubscribe();
this.closePanel();
// CHANGE clear input so placeholder can show selected values
self.clearInput();
}
};
}
```
Also, while working on this, I noticed that the code I was writing was very similar to `MdSelect` and most of the logic is in `MdAutocompleteTrigger`. So maybe what's needed is a new `MdMultiAutocompleteTrigger`, which uses the same `SelectionModel` system that `MdSelect` uses.
The one other issue is that even with `ngFor trackBy` sometimes `MdOption` instances lose their selected state. So every time the options are filtered, I double-check `MdOption.selected` and select() deselect() as necessary.
Also, the way `MdSelect` sets `MdOption.multiple` seems awkward, but I basically do the same thing as the `MdSelect` code.
FYI, my solution for displaying multiple values is just to clear the input, floatPlaceholder='never' and set placeholder to the `displayWith()` of each selected value separated by commas. This works fairly well. I also added a tooltip with the same content in case the entire text is not visible.
Contributor guide
Research direction
Start by reading MdAutocompleteTrigger and the corresponding MdSelect multiple-selection logic, focusing on selection clearing, closing actions, and private methods described in the issue. Done means MdAutocomplete supports multiple selection or provides a supported customization path without monkey-patching, while preserving selected options and keeping the panel open after selection.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- angular, typescript
- Domain
- frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100