实现ngbTypeahead在用户离开字段后自动填充AJAX结果
问题:ng-bootstrap Typeahead AJAX搜索自动填充异常(Tab键导致请求取消)
我在项目中使用ng-bootstrap的ngbTypeahead组件实现了AJAX搜索功能,但遇到一个问题:当用户在搜索结果返回前按下Tab键离开输入框时,AJAX请求会被取消,无法执行自动填充逻辑。
需要实现的自动填充规则:
- 仅返回一个结果时,自动选中该结果;
- 返回多个结果时,若搜索文本匹配某结果的
Code或Name(例如搜索“Cani”匹配Code为“CANI”的结果),自动选中该结果; - 无结果或无匹配项时,清空输入框。
当前代码使用switchMap操作符,怀疑是它导致了AJAX请求被取消,以下是现有代码:
Typescript 代码
AnimalSource = (text$: Observable<string>) => text$.pipe( debounceTime(200), distinctUntilChanged(), switchMap(searchText => { if (searchText === '') return []; return this.SearchAnimals(searchText); }) ); SearchAnimals(query: string): Observable<Animal[]> { const formParams: FormData = new FormData(); formParams.append('Query', query); return this.httpClient.post(AnimalSearchUrl, formParams) .pipe(map((data: IAnimalSearchResponse) => { if (data && data.Results?.length) return data.Results; return []; })); }
HTML 代码
<input type="text" autocomplete="off" class="form-control" container="body" name="Animal" (selectItem)="ChooseAnimal($event)" [(ngModel)]="Criteria.Animal" [ngbTypeahead]="AnimalSource" [resultTemplate]="AnimalSearchListTemplate" /> <ng-template #AnimalSearchListTemplate let-result="result"> <p class="mb-1">{{ result.Code }} / {{ result.Name }}</p> </ng-template>
示例数据
[ { "Code": "CANI", "Name": "Canis familiaris" }, { "Code": "CANIL", "Name": "Canis lupus" } ]
解决方案
问题根源
switchMap的特性是:每当源Observable(这里是输入框的文本流text$)发出新值或完成时,会自动取消之前订阅的内部Observable(AJAX请求)。当用户按下Tab键离开输入框,text$流会完成,导致正在进行的AJAX请求被取消,无法返回结果执行自动填充逻辑。
修改方案
- 替换
switchMap为mergeMap:mergeMap不会取消已发起的AJAX请求,即使text$流完成,请求仍会继续执行并返回结果。 - 添加自动填充逻辑处理:在AJAX请求返回后,根据规则自动选中结果或清空输入框,同时处理多请求返回的顺序问题。
修改后的代码
更新后的AnimalSource及新增处理方法
private lastSearchText = ''; AnimalSource = (text$: Observable<string>) => text$.pipe( debounceTime(200), distinctUntilChanged(), // 记录最后一次搜索文本,避免旧请求结果干扰 tap(searchText => this.lastSearchText = searchText), mergeMap(searchText => { if (searchText === '') { this.Criteria.Animal = ''; return []; } return this.SearchAnimals(searchText).pipe( tap(results => { // 仅处理最后一次搜索的结果 if (searchText === this.lastSearchText) { this.handleAutoFill(searchText, results); } }) ); }) ); // 自动填充逻辑处理 private handleAutoFill(searchText: string, results: Animal[]) { let selectedAnimal: Animal | null = null; // 规则1:仅返回一个结果时自动选中 if (results.length === 1) { selectedAnimal = results[0]; } // 规则2:多个结果时匹配Code或Name(不区分大小写) else if (results.length > 1) { const lowerSearch = searchText.toLowerCase(); selectedAnimal = results.find(animal => animal.Code.toLowerCase() === lowerSearch || animal.Name.toLowerCase().includes(lowerSearch) ) || null; } // 执行选中或清空操作 if (selectedAnimal) { this.Criteria.Animal = selectedAnimal; // 手动触发选择事件,保持原有逻辑一致 this.ChooseAnimal({ item: selectedAnimal }); } else { this.Criteria.Animal = ''; } } // 原SearchAnimals方法保持不变 SearchAnimals(query: string): Observable<Animal[]> { const formParams: FormData = new FormData(); formParams.append('Query', query); return this.httpClient.post(AnimalSearchUrl, formParams) .pipe(map((data: IAnimalSearchResponse) => { if (data && data.Results?.length) return data.Results; return []; })); }
说明
- 使用
mergeMap确保AJAX请求不会因输入框失去焦点而被取消; - 通过
lastSearchText变量过滤旧请求结果,避免多个请求返回时的顺序问题; handleAutoFill方法严格按照设定的三个规则处理结果,同时手动触发ChooseAnimal事件,保证原有业务逻辑不受影响。
内容的提问来源于stack exchange,提问作者Dan Atkinson
相关产品推荐
相关产品推荐

