Как остановить выбор автозаполнения раскрывающегося списка материалов для запуска другого поискового запроса в Angular 8/9? - PullRequest
5 голосов
/ 26 мая 2020

Резюме: у меня есть выпадающий поиск по полю, который вы вводите. Когда я выбираю из раскрывающегося списка, который успешно устанавливает поле для объекта, который я получил при поиске, но, к сожалению, регистрирует ЭТО изменение и отправляет весь объект для поиска.

HTML:

<form [formGroup]="myForm" novalidate>
  <mat-form-field>
    <input matInput 
    placeholder="SKU / Item Number" 
    [matAutocomplete]="auto" 
    formControlName='itemName'>
  </mat-form-field> 
  <mat-autocomplete #auto="matAutocomplete" [displayWith]="parseDropDownSelection">
      <mat-option 
            *ngFor="let row of searchQueryResult" 
            [value]="row" 
            (onSelectionChange)="onItemSelection($event)">
        <span>{{ row.Name }}</span>
      </mat-option>
  </mat-autocomplete>   
</form>

Настройка:

import {FieldSearchServiceItem}     from './../services/field-search.service';

constructor(
    private dialog: MatDialog,
    private formBuilder: FormBuilder,
    private http: HttpClient,
    private appServiceItem: FieldSearchServiceItem,    
    )   {}

ngOnInit ()

ngOnInit(){
    this.myForm = this.formBuilder.group
        ({
        itemName: '',
        });

this.myForm
  .get('itemName')
  .valueChanges
  .pipe(
    debounceTime(200),
    switchMap(value => this.appServiceItem.search({name: value}, 1))
    )
  .subscribe(serviceResult => this.searchQueryResult = serviceResult.qResult);
}

Сервис:

@Injectable()
export class FieldSearchServiceItem 
  {
  constructor(private http: HttpClient) {}
  search(filter: {name: string} = {name: ''}, page = 1): Observable<apiQueryResponseForItem> 
    {
    var queryResponse;
    return this.http.get<apiQueryResponseForItem>(`example.com/search/item/${filter.name}`)
    .pipe(
      tap((response: apiQueryResponseForItem) => 
        {
        response.qResult = response.qResult
          .map(unit => new ItemUnit(unit.Id, unit.Name))
        return response;
        })
      );
    }
  }

Определения класса:

export class ItemUnit
    {
    constructor
        (
        public Id:number,
        public Name:string,
        )   {}
    }

export interface apiQueryResponseForItem
    {
    qSuccess: boolean;
    qResult: ItemUnit[];        
    }

Я видел другие ответы, в которых решение заключалось в использовании emitEvent: false при установке значения, например:

this.myForm.get('itemName').patchValue('theDataThatCameBackFromSeach', {emitEvent:false})

Это имеет смысл ... но я чувствую это решение не соответствует этому подходу наблюдаемых / инъекционных / материалов ... в основном потому, что я не использую вызов, который выполняет .setValue () или .patchValue (), я предполагаю, что где-то в Материальный материал, который это обрабатывает.

Сервер, в конце концов, видит такие вызовы:

http://example.com/search/item/a                   (when the letter a is typed)
http://example.com/search/item/[object%20Object]   (after clicking the dropdown, JS tries to search 'object' after clumsily falling back to string representation )

Мой onItemSelection() в настоящее время не задействован, он работает, но ничего не делает, кроме сброса на консоль. .qResult, возвращаемый моей службой searchQueryResult, содержит {Id:number,Name:string}. Как я могу сделать так, чтобы действие настройки поля автозаполнения по-прежнему выполняло свою работу по настройке поля, но не создавало событие изменения, когда оно это делает, при этом соблюдая onItemSelection(), чтобы я мог получить свой другая обработка сделана?

Ответы [ 4 ]

2 голосов
/ 31 мая 2020

Сделал простое приложение в Stackblitz , можете проверить. Надеюсь, я правильно понял вашу проблему.

Основная идея - когда пользователь заполняет ввод формы, мы получаем строку, когда автозаполнение заполняет ее, мы получаем ItemUnit. Таким образом, мы можем решить, делать http-запрос или нет.

2 голосов
/ 29 мая 2020

Я когда-то сталкивался с той же проблемой go, и это последнее решение, которое я использовал для нескольких входов в моей системе.

Шаги:

  1. Поймите, что на самом деле здесь два события. Something Typed и Value Selected .
  2. Создайте два наблюдаемых объекта для двух событий и обработайте их отдельно.
class Component {
  ngOnInit() {

    /*       SEPARATE THE EVENTS      */

    // If it's an object, it's been selected.
    // I also don't allow selecting `null` but it's up to you.
    const itemSelected$ = this.myForm.get('itemName').valueChanges.pipe(
      filter(val => typeof val === 'object' && val !== null),
    );
    // If it's a string, it's been typed into the input
    const itemTyped$ = this.myForm.get('itemName').valueChanges.pipe(
      filter(val => typeof val === 'string'),
    );

    /*       HANDLE ITEM SELECTED     */

    itemSelected$.subscribe(item => {
      // If you want, you can also handle "fake" items here.
      // I use this trick to show a placeholder like "Create New Item" in the dropdown
      if (item.id === 0 && item.name === 'Create New Item') {
        this.createNewItem().subscribe(
          newItem => this.myForm.get('itemName').setValue(newItem),
        );
        return;
      }
      // I use this in a custom input component (with ControlValueAccessor)
      // So this is where I notify the parent
      this.onChange(item);
    });

    /*       HANDLE ITEM TYPED        */

    const searchQueryResult$ = itemTyped$.pipe(
      debounce(200),
      tap(value => {/* you could handle starting a loading spinner or smth here */}),
      switchMap(name => this.appServiceItem.search({name}, 1)),
    );

    // now you can either use searchQueryResult$ with async pipe:
    // this.filteredResults$ = searchQueryResult$;
    // or subscribe to it and update a field in your component class:
    searchQueryResult$.subscribe(result => this.searchQueryResult = result);
    // If you subscribe, don't forget to clean up subscriptions onDestroy
  }
}

I взял на себя смелость добавить несколько предложений и уловок, но вы поняли общую идею - создайте две отдельные взаимоисключающие наблюдаемые и обрабатывайте их отдельно.

0 голосов
/ 30 мая 2020

ОБНОВЛЕНИЕ

Поскольку другие ответы используют проверку типа для решения нежелательного триггера после выбора - этого также можно достичь с помощью следующей гораздо более простой и короткой версии (I ' m с использованием loda sh для функции isString):

ngOnInit() {
    this.myForm = this.formBuilder.group
        ({ itemName: '' });

    this.searchQueryResult$ = this.myForm
      .get('itemName')
      .valueChanges.pipe(
        debounceTime(200),
        filter(isString),
        switchMap(value => this.appServiceItem.search({name: value}, 1)),
        map(({qResult}) => qResult)
      )
  }

Исходный ответ

Форма valueChanges observable всегда будет выдавать новое значение, при выборе варианта из mat-autocomplete. Это работает должным образом, поскольку mat-autocomplete обновляет значение поля ввода. Для вашего конкретного варианта использования c API mat-autocomplete не предоставляет встроенного способа. Поэтому, чтобы предотвратить такое поведение, вы можете использовать Subject:

Шаблон:

<form [formGroup]="myForm" novalidate>
  <mat-form-field>
<input
      matInput
      placeholder="SKU / Item Number" 
      formControlName="itemName"
      [matAutocomplete]="auto"
      (keyup)="serchTermChanged$.next($event.target.value)"
    />
  <mat-autocomplete #auto="matAutocomplete" [displayWith]="displayName">
      <mat-option 
            *ngFor="let row of searchQueryResult$ | async" 
            [value]="row">
        <span>{{ row.Name }}</span>
      </mat-option>
  </mat-autocomplete>  
<mat-form-field> 
</form>

Компонент:

@Component({...})
export class SomeComponent implements OnInit {
  searchTermChanged$ = new Subject();

  myForm: FormGroup;
  searchQueryResult$: Observable<any[]>;

  constructor(
    private dialog: MatDialog,
    private formBuilder: FormBuilder,
    private appServiceItem: FieldSearchServiceItem,    
  ){}

  ngOnInit() {
    this.myForm = this.formBuilder.group
        ({ itemName: '' });

    this.searchQueryResult$ = this.searchTermChanged$.pipe(
      debounceTime(200),
      switchMap(value => this.appServiceItem.search({name: value}, 1)),
      map(({qResult}) => qResult)
    )
  }

  displayName(option) {
    return option.Name;
  }
}

Таким образом, это не вызовет новый запрос после выбрав опцию из mat-autocomplete. А также вы можете подписаться на наблюдаемую службу, которая возвращается оператором switchMap в шаблоне через конвейер asyn c. Таким образом, вам не нужно беспокоиться об отказе от подписки, когда компонент будет уничтожен.

Кроме того, вот Stackblitz для вашего варианта использования.

0 голосов
/ 26 мая 2020

Допустим, это был ваш интерфейс

interface MyClass {
  id: number,
  name: string
}

Изменения внутреннего значения имеют проверку, что

if(!(value instanceof MyClass)) { //callApi }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...