Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Autocomplete] Restrict selection to given set of items #3334

Closed
julianobrasil opened this issue Feb 28, 2017 · 81 comments · Fixed by #27423
Closed

[Autocomplete] Restrict selection to given set of items #3334

julianobrasil opened this issue Feb 28, 2017 · 81 comments · Fixed by #27423
Assignees
Labels
area: material/autocomplete feature This issue represents a new feature or feature request rather than a bug or bug fix P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent

Comments

@julianobrasil
Copy link
Contributor

julianobrasil commented Feb 28, 2017

Bug, feature request, or proposal:

Request

What is the expected behavior?

md-autocomplete could have an option to force selection. I think it's not in the design doc, but near 50% of the autocompletes I've used must had to force selection.

EDITED: When the requested "force selection" option is set to true, it could clear the input (and any bound property) if the user types something in and moves the focus to other component without selecting one of the options shown on the opened panel (if there are no sugestions, the input is also cleared on the blur event).

What is the current behavior?

EDITED: The feature is not achievable direclty from the component. In one single project I'm working on, I have about 15 md-autompletes, and 11 of them must force selection. Currently I got this feature by two steps:

1. checking (in the intput's valueChanges observable) wether the value is an object - that I save in a private result property - or a regular string - that is ignored, by clearing up the same private result property (basically the input value is an object just when I select one option from the opened panel otherwise it is just a regular string that must be ignored)

2. in the blur event I verify wether the private result property is cleared or has a value (if it's cleared, I also clear the input).

Another way to do that is comparing what was typed to what came from the async server search - but I'm not sure if either of these aproaches is the best solution not wether it's suitable to the case of a search made directly in an in-memory array instead of bringing results fom a remote server. There are too many confusing workarounds to make it do what you want. I'm worried about the future, when I eventualy have to change anything in this code - it will be very time-consuming to remember all of this. There would be much less pain if, in a year from now I could just look at the component and see something like forceSelection="true".

@crisbeto
Copy link
Member

Could you give an example of how it would work? The menu wouldn't close unless you select something?

@julianobrasil
Copy link
Contributor Author

julianobrasil commented Feb 28, 2017

@crisbeto, If the user began to type and left the input without selecting one of the options shown in the opened panel, it would clear the input (and any bound property).

@crisbeto
Copy link
Member

Thanks, that makes a bit more sense.

@kara kara added the feature This issue represents a new feature or feature request rather than a bug or bug fix label Feb 28, 2017
@rosslavery
Copy link

rosslavery commented Feb 28, 2017

Basically a hybrid between a selectbox and an autocomplete. Can be thought of as:

  • A select with a search/filter feature or..
  • An autocomplete that only allows values from the suggestion box.

@fxck
Copy link
Contributor

fxck commented Mar 10, 2017

I've hacked around this by checking whether the model value is an object (given that my items are actually objects) on blur, if it's a string, it means autocomplete was blurred without selecting an option (the value will be a string). But it's nowhere near ideal, it requires setTimeouts, as the value immediately after blur will still be a string even if you actually click on an item, as it needs some time to propagate.

@julianobrasil
Copy link
Contributor Author

julianobrasil commented Mar 11, 2017

@fxck I had forgotten to mention the setTimouts. I had to use them too in the blur event handler in some cases (where there was a group of dependent mdSelects that should keep their values if they were related to the new option selected from the autocomplete's list, but should also be cleared if the user didn't choose an option or if they were not related to the chosen option selected).

@badre429
Copy link

badre429 commented Mar 12, 2017

i agree with @rosslavery in real world app md-select has avg > 50 elements therfore its more productive to use autocomplete that only allows values from the suggestion box.

it heard breaking to see your users scrool al the way down to select element

@fxck
Copy link
Contributor

fxck commented Mar 12, 2017

@badre429 @rosslavery there's a different feature for that #3211

here specifically you can see select filtering with async results fxck@f0dd2ec

@badre429
Copy link

badre429 commented Mar 12, 2017

@fxck its a just a pull request
for me md-select with md-select-header to enable search plus clean buttun [x] it will be perfect for my apps

@gedclack
Copy link

Excuse me, how do you guys make a selection (from hundreds of options) without the discussed md-select-header ?
by using autocomplete? <-- but this is not built for doing that, right?

@julianobrasil
Copy link
Contributor Author

julianobrasil commented Apr 12, 2017

@gedclack, this is an example of a email search input. As the user types an email it goes to the database, grab the suggestions and shows them in the popup panel. If the user leaves the component without selecting one of them, it clears out the typed characters.

style.css (top level file - nothing to do with template style file), not necessary since 2.0.0-beta.3 cesium-cephalopod

.mat-autocomplete-panel {
    max-width: none !important;
}

Template:

<md-input-container class="full-width">
   <!-- this the actual input control -->
   <input mdInput [formControl]="acpEmailControl" [mdAutocomplete]="acpEmail" 
      type="text" placeholder="email" autocomplete="off"
      (blur)="checkEmailControl()" autofocus required>
   <!--this produces an animation while searching for the typed characters-->
   <span mdPrefix>
      <i [style.visibility]="showEmailSearchSpinner" class="fa fa-refresh fa-spin" 
        style="font-size:14px;color:black;margin-right:5px"></i>
   </span>
</md-input-container>

<md-autocomplete #acpEmail="mdAutocomplete" [displayWith]="displayEmailFn">
   <md-option *ngFor="let user of usersFromDatabase" [value]="user" 
     [style.font-size]="'0.7rem'">
      <div>
         {{ user?.userName }}<div style="float: right">{{user?.email}}</div>
      </div>
   </md-option>
</md-autocomplete>

Typescript code:

public acpEmailControl: FormControl = new FormControl();
private emailSearchSubscription: Subscription;
public showEmailSearchSpinner = 'hidden';
public usersFromDatabase: User[] = [];
public chosenUser: User;

ngOnInit() {
   // unsubscribe in ngOnDestroy
   this.emailSearchSubscription = this.acpEmailControl.valueChanges
      .startWith(null)
      .map((user: User | any) => {
        if (user && (typeof user === 'object')) {
          this.chosenUser = user;
        } else {
          this.chosenUser = new User();
        }

        return user && typeof user === 'object' ? user.userName : user;
      })
      .debounceTime(300)
      .switchMap<string, User[]>((typedChars: string) => {
        if (typedChars !== null && typedChars.length > 1) {
          this.showEmailSearchSpinner = 'visible';
          //  Observable that goes to the database and grabs the
         //  users with email or name based on typedChars
          return this.userService.findLikeNameOrEmail(typedChars);
        } else {
          this.showEmailSearchSpinner = 'hidden';
          return Observable.of<User[]>([]);
        }
      })
      .subscribe((value: User[]) => {
        this.showEmailSearchSpinner = 'hidden';
        if (value.length > 0) {
          this.usersFromDatabase = value;
        } else {
          this.usersFromDatabase = [];
        }
      },
      err => console.log('error'));
}

// clears out the input if the user left it (blur event) without
// actually choose one of the options presented in the list
// The setTimeout is because the blur event propagates
// faster then input changes detection 
// (see @fxck's comment above)
checkEmailControl() {
  setTimeout(() => {
    if (this.chosenUser.email === '') {
      this.acpEmailControl.setValue(null);
    } 
  }, 300);
}

displayEmailFn(user: User) {
  return (user && user.email && (user.email !== '')) ? user.email : '';
}

@gedclack
Copy link

@julianobrasil thanks for showing me the codes 👍
I will try it now in my project.
*to reduce data transfer, is it a good idea if I load all the options in ngOnInit() and put it in a variable, then just filter that array with every valueChanges?
*which is better, to use FormControl and subscribe valueChanges like you did above, or just put (ngModelChange)="InputChanged()" in the template?

Thanks in advance, I am new to Angular and Angular Material.

@julianobrasil
Copy link
Contributor Author

julianobrasil commented Apr 14, 2017

@gedclack, yes, you can choose the approach of saving the user's bandwith by putting all in memory (as it is done in the material.angular.io). It depends on what type of machines you expect your clients to use and how much information is available to be placed and how often you expect the end users to make use of the feature. In my environment the most common type are PC's with low memory and Microsoft office applications running with lot's of small documents. They usually have a bad UI experience dispite of all efforts to make it better. I thought saving some memory was a good path to follow. By doing it I also have the benefit of delegating to the data server the work of filtering the array for me. And of course, data transfer rates is not a significant problem for the majority of my end users.

You can change the code to use ngModelChange, but if you're going to contact the server over a network as I am, you'd have to add some extra code to do equivalent things as .debounce(300), which is available right out of the box for the valueChanges observable. Not mentioning you'll end up using another observable inside ngModelChange's function to get to the server side (as I do in the switchMap in the example code) - not sure if you're trying to run away from observables, but if so, forget it and get used to them (personally I think it's a hard thing to do in the beginning, but like anything else, it'll get easier with the practice, and they're one of the most important veins in Angular's heart).

Both of your questions may be good for larger discussions. Try asking them on stackoverflow.com to get more help (I've already heard a lot of "here is not the right place for this kind of question" in many posts. Let's avoid get lectured by moderators).

@gedclack
Copy link

@julianobrasil yep, better not to do long discussions here :) , your approach makes sense for me, but I choose to use ngModelChange and filter the array of options I put in a variable as the page load for the first time because my end users will access this Web App via Android Devices from remote locations with weak signal coverage, so I need it to send or receive data as small as possible everytime they submit something without the need to reload the entire page. Nice chat 👍

@willshowell
Copy link
Contributor

willshowell commented Jul 31, 2017

Here is a solution that doesn't require setTimeout:

this.trigger.panelClosingActions
  .subscribe(e => {
    if (!(e && e.source)) {
      this.stateCtrl.setValue(null)
      this.trigger.closePanel()
    }
  })

https://plnkr.co/edit/VWcGei7HxHYnncpzyYfW?p=preview

EDIT: note that this does not include escaping out of the autocomplete, tracking #6209

@julianobrasil
Copy link
Contributor Author

julianobrasil commented Jul 31, 2017

Much... much better... setTimeout always smelled like a fragile workaround to me.

@kleber-swf
Copy link

kleber-swf commented Jul 31, 2017

I've created a Validator to give an error if no option was selected. I know this is not the solution for this feature request, but it can be used as an alternative.

@kleber-swf
Copy link

But I have to admit, the @willshowell solution is much better 👍

@willshowell
Copy link
Contributor

@jelbourn what are you thoughts api-wise for something like this?

  • forceSelection vs. requireMatch?
  • Does it clear the input or set a validation error if blurred without a selection?
  • Should the attribute belong on the trigger or the autocomplete?
  • Is there an accessibility story?

@julianobrasil
Copy link
Contributor Author

julianobrasil commented Sep 20, 2017

@willshowell, is there a difference between forceSelection and requireMatch? Or is it just a matter of names?

Edited: BTW, I suggested forceSelection in this feature request just because it was the one I used when I was working with jquery ui... or JSF Primefaces - can't remember exactly and also not sure if it was part of their api. Anyway, it was not an important reason. IMO, both of the names seems to describe well what it is supposed to do. As English isn't my native language, any of them are fine to me (at least both of them sound equally good in their "Portuguese translation").

@leblancmeneses
Copy link

leblancmeneses commented Mar 5, 2019

Thanks @mustafarian and @vlio20, this version gets rid of the subscription management and timeout.

import { Directive, Input, Host, Self, AfterViewInit, OnDestroy } from '@angular/core';
import { untilDestroyed } from 'ngx-take-until-destroy';
import {MatAutocompleteTrigger, MatAutocomplete} from '@angular/material/autocomplete';
import {NgControl} from '@angular/forms';

@Directive({
  selector: '[appExtMatAutocompleteTriggerEnforceSelection]'
})
export class ExtMatAutocompleteTriggerEnforceSelectionDirective implements AfterViewInit, OnDestroy {

  @Input()
  matAutocomplete: MatAutocomplete;

  constructor(@Host() @Self() private readonly autoCompleteTrigger: MatAutocompleteTrigger,
      private readonly ngControl: NgControl) {  }

  ngAfterViewInit() {
    this.autoCompleteTrigger.panelClosingActions.pipe(
      untilDestroyed(this)
    ).subscribe((e) => {
      if (!e || !e.source) {
        const selected = this.matAutocomplete.options
            .map(option => option.value)
            .find(option => option === this.ngControl.value);

        if (selected == null) {
          this.ngControl.reset();
        }
      }
    });
  }

  ngOnDestroy() { }
}

View


    <mat-form-field>
      <input matInput type="text" placeholder="Vendor Selection" name="vendor"
          appExtMatAutocompleteTriggerEnforceSelection [(ngModel)]="vendor" [matAutocomplete]="auto1" [disabled]="!!isBusy.length" required />
    </mat-form-field>
    <mat-autocomplete #auto1="matAutocomplete" class="vendorAutocompletePanel" [displayWith]="displayVendorAs.bind(this)">
      <mat-option *ngFor="let option of vendors$ | async" [value]="option">
        {{option.DisplayName}}
      </mat-option>
    </mat-autocomplete>

@AndonyEmmanuelVelazquez
Copy link

Thanks @mustafarian and @vlio20, thanks, this directive solved my problem.
Also, why the status is still open?

@billbarni
Copy link

Thanks @mustafarian and @vlio20, thanks, this directive solved my problem.
Also, why the status is still open?

Because a directive should not be required, specially considering that such feature of force selection existed previsously and in the new angular/material versions is not present anymore.

@bfwg
Copy link

bfwg commented Nov 29, 2019

Is there an official recommended method?

@AndonyEmmanuelVelazquez

@bfwg My work around was validate the object binded to the autocompelte, so in the submit if the object is not valid i show an error.

@newmanw
Copy link

newmanw commented Feb 18, 2020

@leblancmeneses I am running into a couple of issues with your directive.

  1. When adding a filter, the filter is not reset when an option is not selected. This means that when going back into the field to select another option, 'filteredOptions' is filtering out most if not all options.

From: https://material.angular.io/components/autocomplete/overview#adding-a-custom-filter

<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
  {{option}}
</mat-option> 
  ngOnInit() {
    this.filteredOptions = this.myControl.valueChanges
      .pipe(
        startWith(''),
        map(value => this._filter(value))
      );
  }
  1. It would be nice to default back to the previous model value if the user types an invalid option.

@mmalerba mmalerba added needs: discussion Further discussion with the team is needed before proceeding and removed discussion labels Mar 3, 2020
@jelbourn jelbourn changed the title Autocomplete force selection [Autocomplete] Restrict selection to given set of items Apr 28, 2020
@jelbourn jelbourn added the P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent label Apr 28, 2020
@JayAhn2
Copy link

JayAhn2 commented Jul 5, 2020

This feature is deadly needed...

@gitalvininfo
Copy link

Here is a solution that doesn't require setTimeout:

this.trigger.panelClosingActions
  .subscribe(e => {
    if (!(e && e.source)) {
      this.stateCtrl.setValue(null)
      this.trigger.closePanel()
    }
  })

https://plnkr.co/edit/VWcGei7HxHYnncpzyYfW?p=preview

EDIT: note that this does not include escaping out of the autocomplete, tracking #6209

Thanks for this. This is probably I'm looking for. In my case instead of returning the value of input field to null,I just return the value the previous selected option in mat autocomplete.

@essana3
Copy link

essana3 commented Mar 24, 2021

In the React Material-UI implementation of MatAutoComplete, if you type some value that does not exist in the list, the input is either reset or rebound to the last chosen value.
Why is this not the case here?

@willherr
Copy link

willherr commented Mar 5, 2022

What's the discussion that's needed here? It sounds like this is a sought after feature (although not necessary to have to implement).

Is the discussion regarding what to do when a user does not choose an option? Why not use the same behavior as a select when a user does not select an option (do nothing a.k.a. keep the state the same).

Surely the discussion is not to or to not implement it? Someone said 50% of the time you need to force a selection. From my experience it is 95% of the time.

@alixroyere
Copy link

alixroyere commented Jun 15, 2023

I monkey patched "_handleInput" and "_onChange" methods of MatAutocompleteTrigger to get the desired result. I do not like using this intrusive approach, but I didn't see any other way for it to work the way I wanted it to. I do not clear the text on blur if no option is selected, but the underlying form control value will be null. Also, the underlying value will never be a string at any point in time, which was the case with a couple of solutions above.

Here is demo: https://stackblitz.com/edit/angular-material2-issue-wbrzr1

This solution seemed the best to have only valid data in the formControl, but it is only working in one way (view to model). In fact, when reseting the control or patching the value, displayed options are stuck to the ones matching last user interactions.

@grant77 Do you have any idea to make it work both ways?

I'm trying a different way with 2 formControls: one handling the clean data and another containing all user inputs.
I have synchronized values, errors, disabled state and touched status when needed.
That's a bit overloaded, but it seems to work 😬

@crisbeto crisbeto self-assigned this Jul 9, 2023
@crisbeto crisbeto removed the needs: discussion Further discussion with the team is needed before proceeding label Jul 9, 2023
crisbeto added a commit to crisbeto/material2 that referenced this issue Jul 9, 2023
…panel

Adds the `requireSelection` input to the autocomplete, which when enabled will clear the input value if the user doesn't select an option from the list.

Fixes angular#3334.
crisbeto added a commit to crisbeto/material2 that referenced this issue Jul 9, 2023
…panel

Adds the `requireSelection` input to the autocomplete, which when enabled will clear the input value if the user doesn't select an option from the list.

Fixes angular#3334.
crisbeto added a commit to crisbeto/material2 that referenced this issue Jul 11, 2023
…panel

Adds the `requireSelection` input to the autocomplete, which when enabled will clear the input value if the user doesn't select an option from the list.

Fixes angular#3334.
crisbeto added a commit that referenced this issue Jul 11, 2023
…panel (#27423)

Adds the `requireSelection` input to the autocomplete, which when enabled will clear the input value if the user doesn't select an option from the list.

Fixes #3334.
stephenrca pushed a commit to stephenrca/components that referenced this issue Aug 2, 2023
…panel (angular#27423)

Adds the `requireSelection` input to the autocomplete, which when enabled will clear the input value if the user doesn't select an option from the list.

Fixes angular#3334.
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Aug 11, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: material/autocomplete feature This issue represents a new feature or feature request rather than a bug or bug fix P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent
Projects
None yet
Development

Successfully merging a pull request may close this issue.