Skip to content

Autocomplete with multiple selection (MdAutocompleteTrigger customization) #5053

Open
@arlowhite

Description

@arlowhite

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3An issue that is relevant to core functions, but does not impede progress. Important, but not urgentarea: material/autocompletefeatureThis issue represents a new feature or feature request rather than a bug or bug fix

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions