1

I'm trying to display different value depends on focus/blur autocomplete input.

Let's say we have suggestions of items where each item has id and desc. I want to filter items by id and by desc. When I select one of them and input is blur there should be id and desc displayed (e.g. 1 - item01).

When focus is back on input it should display only id (without desc) and if any suggestions with that id(like if 10 is there it should suggest 1010 | desc ). Right now I have to forcefully erase the desc to get back to id. That shouldn't be the case.

Thanks for any suggestions.

You can try to edit this example

See expected behavior on image:

enter image description here

Community
  • 1
  • 1
TadeM
  • 81
  • 1
  • 8
  • So you want the description to be erased automatically on re-focusing and only the id should be there ? – Rex5 Jul 30 '19 at 14:08
  • Try to use pipe. I've made a sample which could give you a kick off https://stackblitz.com/edit/angular-mssmik – Sergey Jul 30 '19 at 18:27

2 Answers2

4

Control Value Accessor (CVA)

The ControlValueAccessor interface is what you are looking for.

Why? This interface decouples the DOM from the Angular Form allowing the display drop down and input to differ from the value that is actually used by the form.

You can implement a custom input as a separate component* and pass a FormControl in.

The following is an untested Working Stackblitz

*Edit 3 - I believe it may also possible to implement this as a directive. See - Angular 2 Directive implementing ControlValueAccessor doesn't update 'touched' property on change


Outside the black box

Your app.component.html will end up looking like.

<form class="example-form">
  <app-auto-special [users]="options" [formControl]="myControl"></app-auto-special>
</form>

app-auto-special acts like a blackbox where it cares only about the User id.

We can patchValue or setValue and it will do it's thing (call writeValue internally). If we interact with this component we get User id's for the FormControl value.

Edit - nothing is stopping you passing the whole User object around instead. I'm assumed OP wanted id based on the question.

Edit 2 - Example passing User object instead Working Stackbliz


Inside the black box

We need to register the app-auto-special component as a provider for the controlValueAccessor using NG_VALUE_ACCESSOR. This is done via:

  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => AutoSpecialComponent),
    multi: true
  }]

So inside the black box we implement the interface comprising of 4 methods:

  1. writeValue(obj: any): void
  2. registerOnChange(fn: any): void
  3. registerOnTouched(fn: any): void
  4. setDisabledState(isDisabled: boolean)?: void

This usually means the following boilerplate:

export class AutoSpecialComponent implements ControlValueAccessor {
  public _value: number;
  public disabled: boolean;
  onChanged: any = () => {};
  onTouched: any = () => {};

  /*
  * ControlValueAccessor boilerplate
  *
  */
  writeValue(value): void {
    this._value = value
  }
  registerOnChange(fn: any): void {
    this.onChanged = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled
  }

}

We make a copies of the functions provided by registerOnChange and registerOnTouched and call these copies (this.onChanged and this.onTouched) when we want to update the formControl value or it's on touched property. setDisabledState is optional and writeValue is called on initialization or when we call patchValue or setValue from the parent.

To set the FormControl we call this.onChanged(some_value); and we can hook into various events input, focusin, blur, optionSelected and decide what happens separately to:

  • The FormControl value
  • Which options to show
  • What display string should be in the input

This answer comes with the caveat that this is one of the first CVA implementations I've done so I'm shaky on the foundations.


Additional benefits

  • Unit testing - isolated DOM display versus form
  • Separation of logic - parent no longer saturated

Resources

Learn more from the following youtube video The Control Value Accessor | Jennifer Wadella

This is accompanied by the following slides

Andrew Allen
  • 6,512
  • 5
  • 30
  • 73
  • This has potential if reuse is needed, but would have to be modified quite a bit to allow for using different data sets and formatting. Otherwise this is probably heavy-handed. It also assumes that form control values can be just the 'id' - not the entire 'User' object that the OP uses. And it needs work as it isn't working properly - focus, select, blur, focus results in the display text being just the id. – G. Tranter Jul 30 '19 at 21:30
  • @G.Tranter per OP...When focus is back on input it should display only id. And absolutely nothing is stopping you from passing the User to the formcontrol – Andrew Allen Jul 30 '19 at 21:32
  • My bad - I realise now what you are saying, onBlur should should show id and descr. This is now fixed. – Andrew Allen Jul 30 '19 at 21:56
  • https://stackblitz.com/edit/angular-gz8jd5?file=src/app/app.component.ts – Sergey Jul 31 '19 at 15:00
  • @Sergey - still needs the fix I mentioned before you deleted your answer (it was a great answer) - you can't type into the field. Fixed: https://stackblitz.com/edit/angular-kcb71e?file=src/app/input-focus.pipe.ts. – G. Tranter Jul 31 '19 at 16:01
  • @Sergey please repost my updated answer below as your own and I will delete mine. – G. Tranter Jul 31 '19 at 16:31
2

Updated:

Building on a deleted answer from user @Sergey, all you need to do is to add a pipe and a template reference to the input element. No other changes are necessary:

Stackblitz

HTML:

<input type="text" placeholder="Assignee" aria-label="Assignee" 
  matInput [formControl]="myControl" [matAutocomplete]="auto"
  #input [value]="myControl.value | inputFocus:input.ownerDocument.activeElement === input">

Pipe:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'inputFocus'
})
export class InputFocusPipe implements PipeTransform {

  transform(value: any, focused: boolean): any {
    if (!value || typeof value === 'string') { 
      return value; 
    }

    return focused ? value.id : value.id + ' | ' + value.description;
  }

}
G. Tranter
  • 16,766
  • 1
  • 48
  • 68