Components: Autocomplete with multiple selection (MdAutocompleteTrigger customization)

Created on 9 Jun 2017  路  14Comments  路  Source: angular/components

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 MdOptions on selection
  2. leave the Autocomplete open after option selection event.

EDIT updated for beta12

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.

P5 materiaautocomplete feature needs discussion

Most helpful comment

This is how i implemented https://stackblitz.com/edit/angular-xgtey4 using mat-chips, mat-autocomplete.
Using above approach there is a flicker in autocomplete panel while selecting multiple items so i have used another approach https://stackblitz.com/edit/angular-ah51ss where i used mat-select-list in autocomplete instead of mat-option

All 14 comments

@jelbourn Is someone planning to work on a multiple autocomplete? Or would you be open to me creating a new MdMultipleAutocompleteTrigger?

No plans to work on this, would need some discussion to determine if we want to add it to the library. The a11y would be problematic since an autocomplete is a role="combobox" and I'm not sure if that supports multiple values.

@jelbourn Don't know much about a11y, but MdSelect uses 'role': 'listbox', anything wrong with changing MdAutocomplete's role to listbox for multiple mode or does that cause problems?

A listbox doesn't have a text input like autocomplete does. See https://www.w3.org/TR/wai-aria/roles#combobox

Looks like combobox theoretically supports being associated with a listbox with multiple selection, but it would need some testing; screen-readers are often "best effort" on certain boundary cases in the spec

I am also in need of this function. Either a customization or a new component "multi-search-select" will work. I would imagine something close to this, https://semantic-ui.com/modules/dropdown.html#multiple-search-selection

It is good to have functionality especially in corporate application where there is always a need of this component

I want this :) I created own component, but good to see native implementation :)

@PawelOwczarekFalcon is your component open sourced? :)

@Aaron-Zhao The Semantic UI example is functionally more like chips with an autocomplete.
There is some work on combining MatChipList and MatFormField
https://material2-dev.firebaseapp.com/chips
See this issue comment https://github.com/angular/material2/issues/3273#issuecomment-327110530
They claim an Autocomplete should work, but I haven't tried it yet.
I'm not sure what the official status on this is.

However, if you want it to look more like Semantic UI you'll need to do some re-styling with CSS.

IMO, the advantage of Chips + Autocomplete is that you clearly see all selected values. However, it takes much more space.

The advantage of the Autocomplete+multiple hack I created is that it takes less space. However, it's a bit confusing to users. The fundamental problem is that the input text is your search AND the display of selected values (after blur), which is confusing. And there's also the accessibility issues that @jelbourn mentioned.

So I think going with Chips+Autocomplete is probably better.
For my use case, I'll just create more compact styling since my App isn't used on touch devices where the big chips are needed.

@jelbourn I'm tempted to close this issue unless someone has a design for multiple+Autocomplete that isn't equivalent to Chips+Autocomplete.

@arlowhite What about extending the mat-select to be searchable/filterable while multiple selection is enabled. Does that accomplish the same purpose? There's a pr open for a mat-select-header https://github.com/angular/material2/pull/7835 where you could add a search input to filter the options. The author of the pull requests is waiting for feedback because I think he want's to make sure @jelbourn's a11y concerns are addressed.

@jrood
Extending mat-select approach looks clean.

Here is how i handled it https://stackblitz.com/edit/angular-v1b716
I used two form controls. One for the autocomplete and one for multiple selection which I managed manually.

This is how i implemented https://stackblitz.com/edit/angular-xgtey4 using mat-chips, mat-autocomplete.
Using above approach there is a flicker in autocomplete panel while selecting multiple items so i have used another approach https://stackblitz.com/edit/angular-ah51ss where i used mat-select-list in autocomplete instead of mat-option

Here is how i handled it https://stackblitz.com/edit/angular-v1b716
I used two form controls. One for the autocomplete and one for multiple selection which I managed manually.

i took a reference from your solution. could you let us know how to bind the selected values as in mat select like for e.g. i tried ([value])="somearray"

Was this page helpful?
0 / 5 - 0 ratings