Components: Feature: Possibility to make the [displayWith] of Autocomplete more flexible

Created on 29 May 2017  路  27Comments  路  Source: angular/components

Bug, feature request, or proposal:

Feature Request.

What is the expected behavior?

I expect [displayWith] to give me back the whole object so I can treat what property I can return to display (doesn't it make sense)?

What is the current behavior?

Actually [displayWith] is too limited.

Ex:

I have the following variable that gets the content from API:

this.myObs = this.<service>.<fetch>();

That's the content coming from back-end:

[
  { id: 1, name: 'anyName' },
  { id: 2, name: 'xxx' }
];

In template I have:

<md-input-container>
  <input mdInput placeholder="State" (keyup)="onKeyUp($event)" [mdAutocomplete]="auto" [formControl]="objCtrl">
</md-input-container>

<md-autocomplete #auto="mdAutocomplete" [displayWith]="displayFn.bind(this)">
  <md-option *ngFor="let obj of myObs | async" [value]="obj.id">
    {{ obj.name }}
  </md-option>
</md-autocomplete>

Since, I'm binding the md-option to id, [displayWith] just gives me the value of id while I was expecting at least the full object so I can return the name, for example.

Workarounds:

1- Subscribe to the observables and store in arrays.

Problem 1: You'll have to loop through the array everytime that an option is selected to return the desired property.

Problem 2: Imagine that I have more than 10 autocomplete (my case), I'll have to create 10 variables and subscribe them all while I could simply use the async pipe (as I'm doing) and, consequently I wouldn't need to subscribe in nothing.

2 - Bind the full object to <md-option>, so [displayWith] will give me back the whole object.

Problem: If your API expects a single property (in my case back-end expects only the id), before submits the form, you'll have to change the whole content of form that was stored as object to id.

What are the steps to reproduce?

Providing a Plunker (or similar) is the best way to get the team to see your issue.
Plunker template: https://goo.gl/DlHd6U

#### What is the use-case or motivation for changing an existing behavior?
-

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

Material 2.0.0-beta.6

Is there anything else we should know?

The workarounds can be used? Ofc they can, however it'd be really better if Material2 could provide a better solution for this.

P3 materiaautocomplete feature needs discussion

Most helpful comment

I agree that it this issue needs some love. I'm not sure why it doesn't just behave like the mat-select does and default to display the value that's in the selected panel. I've been able to use function composition to get the desired behavior here by passing the options into the displayWith function and finding the selected option by id ( which is the selected options [value]), then returning its name.

here's the important stuff in the component.ts

options: User[] = [
  {name: 'Mary', id: 1},
  {name: 'Shelley', id: 2},
  {name: 'Igor', id: 3}
];
// we expect the options to be passed in from the template
// then we return a callback function that will expect an id which is passed in by the autocomplete event logic
displayFn(options: User[]): (id: number) => string {
  return (id: number) => { 
    const correspondingOption = Array.isArray(options) ? options.find(option => option.id === id) : null;
    return correspondingOption ? correspondingOption.name : '';
  }
}

and the important piece in the template

<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn(options)">
  <mat-option *ngFor="let option of filteredOptions | async" [value]="option.id">
    {{option.name}}
  </mat-option>
</mat-autocomplete>

Additionally, if you throw a console.log in the function, you'll see that this is running a lot. So, a few of these on a form could impact performance.

All 27 comments

@dev054 Thanks for raising this issue.

There may be two simple options:

  1. MD takes care of everything for us, we give it a _displayFn_ and an object instance. In this case, MD calls the _displayFn_ with our instance object. We format the display and pass it back. Inturn MD will populate the innerHTML of <md-option></md-option>(s) with the returned display names. The [value] remains what we set it to.
<md-input-container>
  <input mdInput placeholder="ComplexObject"  [mdAutocomplete]="auto" [formControl]="objCtrl">
</md-input-container>

<md-autocomplete #auto="mdAutocomplete" [displayWith]="displayFn.bind(this)">
  <md-option *ngFor="let obj of myObs | async" [instance]="obj" [value]="obj.id"></md-option>
</md-autocomplete>
  1. MD expect us to do it all. We have a simple name and just go about displaying it. The good old way.
<md-input-container>
  <input mdInput placeholder="ComplexObject"  [mdAutocomplete]="auto" [formControl]="objCtrl">
</md-input-container>

<md-autocomplete #auto="mdAutocomplete">
  <md-option *ngFor="let obj of myObs | async"  [value]="obj.id">{{obj.name}}</md-option>
</md-autocomplete>

@dev054 - the plunker seems to be unrelated. Removing it or providing a relevant one may help others.

Hi,

I am interested in that issue as well, as I have the same needs that @dev054 . (In the single ID asked from the server and the multiples autocomplete in 1 form.)

Is there any news on what is intended for that ?

Isn't there a way to use the same template in the md-option to show the selected value, instead of stripping everything and put a single string ? (in case of using the md-icon for example, the selected value shows the code of the icon and not the icon itself).

this whole displayFN doesn't make sense to me. Why not just use displayValue and bind to a value in the component. (e.g. [displayValue]="myDisplayValue")
Then using (onSelectChange) one can update displayValue if they like. My problem is that i need to update displayValue when the user navigates e.g. browser back and forward buttons. There isn't a way to do this with displayFN since displayFN only gets called when you select a new item in the autocomplete. Unfortunately, my autocomplete value also updates if the URL of the page changes.

Wouldn't it makes more sense if the [displayWith] gave us the whole option (of <md-option>) instead of only the [value]? I think It'd solve all problems.

@jelbourn

I have the same issue. In my case, I also need to restore the display from saved values..

I can see why displayWith is taking in value rather than the whole object. It's because value is what the form control stores, and when you revisit your "draft" form it has to reconstruct the "display" from something, and in this case it is your stored value.

The workaround is to tell your OnSubmit to take in ID of your object, otherwise you will have to do some dirty hacks (e.g. storing display locally and reconstruct display by querying your db matching the id when visit your "draft").

Alternatively, what if when displayWith isn't defined, the trigger displays the MdOption's viewValue instead of value?

That, or as @rafaelss95 suggested, the MdOption is passed to displayWith, so the user can choose.

My usecase is slightly different. My values are Observables and the options are rendered as

<md-option *ngFor="let newUser of users" [value]="newUser" disabled={{!(newUser|async).name}}> {{(newUser|async)?.userName}} - {{(newUser|async)?.name}} </md-option>

My problem is that a displayWith() function has no way of synchronously returning a value and I don't see an option of having the displayWith value rendered as interpolation, e.g. {{(newUser|async)?.userName}}

Hi,

To aid discussion and provide a working reference implementation, I've posted a plnkr. Unfortunately I don't know what's wrong with the typical plnkr template itself (known issue apparently), but at least you can see the code itself here:

http://plnkr.co/edit/rw8OYeXtRTPnYfLRsqtv

I understand the original argument about wanting to have the autocomplete work with the full object. Given the amount of component code in the plnkr that has to used just for one autocomplete, and the complexity, I agree it would be nice to reduce some of this if possible, especially when making forms with numerous autocompletes. That said, I believe the plnkr demonstrates that it is possible to get all the typical desired functionality working.

I would strongly advise that whatever solution is under consideration retains some of the existing design in terms of binding [value] to the object's value (a.k.a. id), NOT to the entire object. The reason becomes clear when you try to persist the data or patch the form on reload - using id is simplest.

I also believe it's easy enough in the status quo to work with the entire object in the component code as it is, relying on a mapping function like that used by displayWith to convert from value to viewValue as necessary.

So in summary, I slightly disagree with @dev054 in terms of what the concerns ought to be and how we tackle them, because I see value in the existing design, but I don't disagree in spirit that it would be nice to improve the autocomplete's ability to rely on less code if we provide the full object as an option.

Regards,

  • S. Arora

p.s. @RAlfoeldi -- the plunkr contains a way to pre-empt the need for interpolation by doing the string manipulation in the loadCompanies() method.

@sarora2073 : Thanks for the p.s. but what the plunkr does is cache the values and use the cached values in displayWith(). That works... but isn't really what I would like to do as both the list of values (actually only the ids of the values) as well as the values themselves are Observables, i.e.: getCompanies() returns an Observable of Observables.

@RAlfoeldi - Not sure I follow your objective in using autocomplete exactly. Common use-case is to load a single collection (view / viewValue) and bind those to the drop-down, then allow autocomplete to filter that single collection based on user entry, and in this use-case you can leverage displayWith to map selected value to corresponding viewValue.

Having said that, the displayWith function can handle any kind of return value though..it could perform additional operations e.g. a second lookup on another collection, if that's what you're trying to achieve perhaps. i.e. there's no built-in constraints inherently in that function..just takes an input (selected value) and returns an output to display. btw, I find it's much easier to work with displayWith if you bind the md-option to the [value]="newUser.value" (assuming that 'newUser' is the object you created). Binding the entire object sounds trickier to me at least.

Additionally, I was suggesting you do something similar to the loadCompanies() function in the plunker to pre-load the data you retrieve and get the data structure you want up-front (it can be 'value' / 'viewValue' and additional fields with interpolated values if that helps), then use displayWith to just return any of those fields depending on what is selected..that might be better for performance / separation-of-concern angle, and avoid having to do a second data lookup in the displayWith function, but if you really need to do a lookup from a second data source on the fly (i.e. in displayWith) you can. Hope that helps.

@sarora2073:

there's no built-in constraints inherently in that function

Not quite. displayWith() requires an immediate, synchronous return value. As I only have Observables to work with, that is something I cannot do without resorting to caching values - which I really don't want to do - at least not at this level.

And thanks for the suggestions with the preloading and the separation of concerns idea... Sounds like a good idea and like what we've built: a dedicated service returns an Observable of Observables matching the current input of the autoselect. Once lists of possible values are retrieved from the server they are centrally cached, once retrieved values are centrally cached; but - as we're dealing with Observables - both are returned - and potentially updated - asynchronously. Works like a charm in a fairly large system (updates pushed from the server to multiple clients right into the UI - no extra code required) - except for with displayWith().

In other words... a fully asynchronous solution using Observables would greatly appreciate interpolation in displayWith().

@ anybody else: our application does not handle 'data' until the very last possible moment. This is by design and fits well with reactive concepts. (As soon as you hold on to the value of an Observable, you're stuck with it - so we don't.) We use interpolation for all rendering of data and up till now this has worked everywhere - except for displayWith(). But this is getting slightly off topic...

A while ago @willshowell asked me to look at this discussion so i'm going to chime in again with my 2 cents on where I think this whole discussion landed...hoping to help bring some closure.

This argument for binding to the "whole object", and allowing the displayWith function to work with that was proposed to reduce the amount of code, improve performance, and to maintain use of Observables throughout. Personally I don't "get" what kind of magical code could accomplish the latter without presuming too much about the use case, and so given that that appears to be the main argument I'd suggest closing this issue absent a clear design path forward.

The current design for autocomplete, verbose though it may be, maintains some important benefits which I'd would advocate to retain:

  1. By explicit binding to the id (not the whole object) we can determine what id value to persist to the db. Storing / patching the whole object doesn't sound like a best practice.
  2. Explicit definition of the displayWith function provides some implementation flexibility as well.

Hope that helps.

Edit: I've been trying to find a way to make autocomplete work with dynamic forms and after much consideration of various use cases and this discussion, i think there might be a good case to be made for removing displayWith altogether. Will open a new issue on that front.

+1 for @RAlfoeldi 's request.
We're loading an entity with a country relation, so the entity has a entity.country = 'IE'. On the autocomplete we want to display the country name, 'Ireland'. Caching all possible autocomplete values is not an option (~200 countries might be ok, but not when we're drilling down to destination codes).
Our "displaySelection" function receives the country code 'IE', which is absolutely fine (there's no alternative, really). So we need a mechanism with which our "countryService" can look up the country code 'IE', return 'Ireland' (Observable), and thus update the display value.

Our current solution is to hold a "current selected option":

private selectedOption = { key: '', label: '' };

Then use the (optionSelected) on the mat-autocomplete to:

onOptionSelected( evt: MatAutocompleteSelectedEvent ) {
    this.selectedOption.key = evt.option.value;
    this.selectedOption.label = evt.option.viewValue;
}

And, finally, in the display method:

displaySelection( key ) {
    if ( !key ) {
        return '';
    }

    if ( key === this.selectedOption.key && this.selectedOption.label ) {
        return this.selectedOption.label;
    }

    this.dynSearchService.lookup( key, this.config.lookupApi )
        .subscribe( c => {
            this.selectedOption.key = key;
            this.selectedOption.label = this.getLabel( c );
            this.displayCtrl.setValue( this.selectedOption.key );
        } );

    return '...';
}

which will return the current label, if present. Otherwise, it will issue a lookup to the server, and on return update the internal "selected option" and then update the input control associated with the mat-autocomplete.

Would also be great if we could return observables or promisesm, not just synchronously return a string .

I have an asynchronous array as options for my autocomplete.
ideally [displayWith] should be able to consume an observable, but I figured out a workaround which doesn't require that my class holds a synchronous list just for displayFn to get the display value.

<mat-autocomplete #auto="matAutocomplete" [displayWith]="diplayFn()">

diplayFn() {
    const itemList: Item[] = [];
    getItems().subscribe(items => itemList.push(...items));
    return (id?: number) => id? itemList.find(i => i.id === id).name: null;
  }

Like some of you, I needed a solution where:

  1. I can display selected city name but store city code.
  2. I get my list of cities from an observable.

After reading this thread to get some inspiration, this is the solution I came up with:

<mat-form-field *ngIf="filteredCities$ | async as filteredCities">
  <mat-label>City</mat-label>
  <input matInput formControlName="city" [matAutocomplete]="city" required>
  <mat-autocomplete #city="matAutocomplete" autoActiveFirstOption [displayWith]="cityDisplayWith.bind(filteredCities)">
    <mat-option *ngFor="let city of filteredCities" [value]="city.code">
      <span>{{ city.name }}</span>
    </mat-option>
  </mat-autocomplete>
</mat-form-field>

cityDisplayWith (cityCode: string) {
  if (cityCode) {
    const cities = this as any as { code: string, name: string }[]
    return cities.find(city => city.code === cityCode).name
  } else {
    // when cityCode is '', display ''
    return ''
  }
}

The trick is to async resolve filteredCities$ earlier and bind filteredCities to displayWith function and <mat-option>.

Hope this helps.

I think this request is really important for us developers and should not be left in ostracism. The solutions above work, but feels like jerry-rig.

have little bit of different issue but got here. I have a FormGroup address with 8 formcontrols. Region, Municipality, Settlement, Address - ID and Name controls. For example RegionID, RegionName, MunicipalityID, ..., AddressName. For each address level I have mat auto-complete which returns list of addresses matching that address level. first I need to choose Region -> Municipality -> Settlement -> Address, but they all return same object type (address with all 8 fields, for low levels such as region other values are null) from database. I need to bind each input to either formControlName for matching Name or bind all of the inputs to same address FormGroup. the problem is when I try to edit same form (meaning the data should be prefilled in the form), I don't get preselected items.

<mat-form-field class="locationDate">
  <mat-label>{{ 'Region' | translate }}</mat-label>
  <mat-icon matPrefix>terrain</mat-icon>
  <input type="text" aria-label="Number" matInput 
    [formGroup]="form"
    (keyup)="getRegions($event.target.value)"
    [matAutocomplete]="region" 
    (blur)="onTouched()">

  <mat-autocomplete #region="matAutocomplete" 
  [displayWith]="displayRegion">
    <mat-option *ngFor="let region of regions" 
    [value]="region">
      {{region.regionName}}
    </mat-option>
  </mat-autocomplete>
</mat-form-field>

here is displayRegion

  displayRegion(region?: any): string | undefined {
    return region ? region.regionName : undefined;
  }

but the problem is I can't bind formControlName to regionName, because my value is the whole object including IDs and Names. this works fine but I have problem when trying to see already existing form values. better displaywith would have probably solved my issue.

I agree that it this issue needs some love. I'm not sure why it doesn't just behave like the mat-select does and default to display the value that's in the selected panel. I've been able to use function composition to get the desired behavior here by passing the options into the displayWith function and finding the selected option by id ( which is the selected options [value]), then returning its name.

here's the important stuff in the component.ts

options: User[] = [
  {name: 'Mary', id: 1},
  {name: 'Shelley', id: 2},
  {name: 'Igor', id: 3}
];
// we expect the options to be passed in from the template
// then we return a callback function that will expect an id which is passed in by the autocomplete event logic
displayFn(options: User[]): (id: number) => string {
  return (id: number) => { 
    const correspondingOption = Array.isArray(options) ? options.find(option => option.id === id) : null;
    return correspondingOption ? correspondingOption.name : '';
  }
}

and the important piece in the template

<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn(options)">
  <mat-option *ngFor="let option of filteredOptions | async" [value]="option.id">
    {{option.name}}
  </mat-option>
</mat-autocomplete>

Additionally, if you throw a console.log in the function, you'll see that this is running a lot. So, a few of these on a form could impact performance.

@nayfin You save my life. It works perfectly. I am very nervous about the documents of angular material. Thanks again.

Btw, I come from Saigon -VietNam.

Why this isn't documented in https://material.angular.io/components/autocomplete ??

@nayfin
function that returns a function, what a legend

I need the displayWith works with observables due to my using ngx-translate for getting i18n labels. How could I do that safely if I can't use the async pipe inside the property? I do need this functionality

@nayfin awesome work dude, love your solution.
i also was looking for this functionality in official documentation, expecting it to work by default. Sadly found nothing.
I had to always reformat form.value mat-autocomplete value before submiting to backend. That's very frustrating.
You solution should be in official docs

After trying different stuff with no success, finally the @nayfin solution made the job. This should be part of the official docs.

Was this page helpful?
0 / 5 - 0 ratings