web-dev-qa-db-ja.com

オートコンプリートの実装

Angular 2用の適切なオートコンプリートコンポーネントを見つけるのに問題があります。キーラベルオブジェクトのリストを渡してinputフィールドにNiceオートコンプリートを設定できるものなら何でも。

KendoはまだAngular 2をサポートしていません、そしてそれは我々が主に内部的に使用しているものです。 Angular MaterialがAngular 2をまだサポートしていないようです。

誰かが私を正しい方向に向けさせたり、彼らが何を使っているのかを私に知らせてください。

これが私がこれまでに作ったものです。それはかなり悪いです、そして私は素敵に見える何かを見つけたいのですが。

import {Component, EventEmitter, Input, Output} from 'angular2/core';
import {Control} from 'angular2/common';
import {Observable} from 'rxjs/Observable';
import {SimpleKeyValue} from '../models/simple-key-value'
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

@Component({
selector: 'general-typeahead',
template: ` <div>
            <div class="input-group">
            <input type="text" [ngFormControl] = "term" class="form-control" placeholder={{placeHolder}} >
            </div>
            <ul>
                <li class="item" *ngFor="#item of matchingItems" (click)="selectItem(item)">
                    {{item.value}}
                </li>
            </ul>              
</div>`
})

export class GeneralTypeahead {

  matchingItems: Array<SimpleKeyValue>;
  term = new Control();

  @Input() allItems: Array<SimpleKeyValue>;
  @Input() placeHolder: string;
  @Output() onSelectItem = new EventEmitter<SimpleKeyValue>();

  constructor() {
    this.term.valueChanges
        .distinctUntilChanged()
        .debounceTime(200)
        .subscribe((term : string) => this.matchingItems = this.allItems.filter(sl => sl.value.toLowerCase().indexOf(term.toLowerCase()) > -1));
  }

  selectItem(sl: SimpleKeyValue) {
    this.onSelectItem.emit(sl);
  }
}
55
Mort

更新:この答えは ng2-completer Angular 2オートコンプリートコンポーネントの開発につながりました。これはAngular 2の既存のオートコンプリートコンポーネントのリストです。

  1. ng2-completer
  2. ng2-auto-complete
  3. ng2-typeahead

アイディアを思いついたことに対して信用は@ dan-cancroに行きます

独自のディレクティブを作成したい人のためのオリジナルの答えを守る:

オートコンプリートリストを表示するには、最初に attribute directive が必要です。これは入力テキストに基づいて候補のリストを返し、それからそれらをドロップダウンで表示します。ディレクティブにはリストを表示するための2つのオプションがあります。

  1. NativeElementへの参照を取得してDOMを直接操作する
  2. DynamicComponentLoaderを使ってリストコンポーネントを動的にロードする

DOMを直接操作してそれらを迂回するのではなく、角度のある2つのコアメカニズムを使用するため、2つ目の方法がより良い選択であると私には思えます。

これは指令コードです。

"use strict";
import {Directive, DynamicComponentLoader, Input, ComponentRef, Output, EventEmitter, OnInit, ViewContainerRef} from "@angular/core";
import {Promise} from "es6-promise";
import {AutocompleteList} from "./autocomplete-list";

@Directive({
    selector: "[ng2-autocomplete]", // The attribute for the template that uses this directive
    Host: {
        "(keyup)": "onKey($event)" // Liten to keyup events on the Host component
    }
})
export class AutocompleteDirective implements OnInit {
    // The search function should be passed as an input
    @Input("ng2-autocomplete") public search: (term: string) => Promise<Array<{ text: string, data: any }>>;
    // The directive emits ng2AutocompleteOnSelect event when an item from the list is selected
    @Output("ng2AutocompleteOnSelect") public selected = new EventEmitter();

    private term = "";
    private listCmp: ComponentRef<AutocompleteList> = undefined;
    private refreshTimer: any = undefined;
    private searchInProgress = false;
    private searchRequired = false;

    constructor( private viewRef: ViewContainerRef, private dcl: DynamicComponentLoader) { }
    /**
     * On key event is triggered when a key is released on the Host component
     * the event starts a timer to prevent concurrent requests
     */
    public onKey(event: any) {
        if (!this.refreshTimer) {
            this.refreshTimer = setTimeout(
            () => {
                if (!this.searchInProgress) {
                    this.doSearch();
                } else {
                    // If a request is in progress mark that a new search is required
                    this.searchRequired = true;
                }
            },
            200);
        }
        this.term = event.target.value;
        if (this.term === "" && this.listCmp) {
            // clean the list if the search term is empty
            this.removeList();
        }
    }

    public ngOnInit() {
        // When an item is selected remove the list
        this.selected.subscribe(() => {
            this.removeList();
        });
    }

    /**
     * Call the search function and handle the results
     */
    private doSearch() {
        this.refreshTimer = undefined;
        // if we have a search function and a valid search term call the search
        if (this.search && this.term !== "") {
            this.searchInProgress = true;
            this.search(this.term)
            .then((res) => {
                this.searchInProgress = false;
                // if the term has changed during our search do another search
                if (this.searchRequired) {
                    this.searchRequired = false;
                    this.doSearch();
                } else {
                    // display the list of results
                    this.displayList(res);
                }
            })
            .catch(err => {
                console.log("search error:", err);
                this.removeList();
            });
        }
    }

    /**
     * Display the list of results
     * Dynamically load the list component if it doesn't exist yet and update the suggestions list
     */
    private displayList(list: Array<{ text: string, data: any }>) {
        if (!this.listCmp) {
            this.dcl.loadNextToLocation(AutocompleteList, this.viewRef)
            .then(cmp => {
                // The component is loaded
                this.listCmp = cmp;
                this.updateList(list);
                // Emit the selectd event when the component fires its selected event
                (<AutocompleteList>(this.listCmp.instance)).selected
                    .subscribe(selectedItem => {

                    this.selected.emit(selectedItem);
                });
            });
        } else {
            this.updateList(list);
        }
    }

    /**
     * Update the suggestions list in the list component
     */
    private updateList(list: Array<{ text: string, data: any }>) {
        if (this.listCmp) {
            (<AutocompleteList>(this.listCmp.instance)).list = list;
        }
    }

    /**
     * remove the list component
     */
    private removeList() {
        this.searchInProgress = false;
        this.searchRequired = false;
        if (this.listCmp) {
            this.listCmp.destroy();
            this.listCmp = undefined;
        }
    }
}

ディレクティブはドロップダウンコンポーネントを動的にロードします。これは、ブートストラップ4を使用したそのようなコンポーネントのサンプルです。

"use strict";
import {Component, Output, EventEmitter} from "@angular/core";

@Component({
    selector: "autocomplete-list",
    template: `<div class="dropdown-menu  search-results">
                    <a *ngFor="let item of list" class="dropdown-item" (click)="onClick(item)">{{item.text}}</a>
               </div>`, // Use a bootstrap 4 dropdown-menu to display the list
    styles: [".search-results { position: relative; right: 0; display: block; padding: 0; overflow: hidden; font-size: .9rem;}"]
})
export class AutocompleteList  {
    // Emit a selected event when an item in the list is selected
    @Output() public selected = new EventEmitter();

    public list;

    /**
     * Listen for a click event on the list
     */
    public onClick(item: {text: string, data: any}) {
        this.selected.emit(item);
    }
}

別のコンポーネントでディレクティブを使用するには、そのディレクティブをインポートする必要があります。そのディレクティブをcomponentsディレクティブに含めて、選択のための検索機能とイベントハンドラを指定します。

 "use strict";
import {Component} from "@angular/core";

import {AutocompleteDirective} from "../component/ng2-autocomplete/autocomplete";

@Component({
    selector: "my-cmp",
    directives: [AutocompleteDirective],
    template: `<input class="form-control" type="text" [ng2-autocomplete]="search()" (ng2AutocompleteOnSelect)="onItemSelected($event)" autocomplete="off">`
})
export class MyComponent  {

    /**
     * generate a search function that returns a Promise that resolves to array of text and optionally additional data
     */  
    public search() {
        return (filter: string): Promise<Array<{ text: string, data: any }>> => {
            // do the search
            resolve({text: "one item", data: null});
        };
    }

    /**
     * handle item selection
     */  
    public onItemSelected(selected: { text: string, data: any }) {
        console.log("selected: ", selected.text);
    }
}

更新:Angular 2と互換性のあるコードrc.1

121
Ofer Herman

PrimeNGには、テンプレート作成や複数選択などの高度な機能を備えたネイティブのオートコンプリートコンポーネントがあります。

http://www.primefaces.org/primeng/#/autocomplete

20
Cagatay Civici

typeahead.jsが使えると思います。そのためのTypeScript定義があります。開発にTypeScriptを使っているのであれば、それは使いやすいでしょう。

2
Manoj

私はあなたがすでにいくつかの答えを持っていることを知っています、しかし私は私たちのチームが材料を使っているので重いライブラリやブートストラップに関連するものに依存したくないという同様の状況にありました。スタイル、あなたは私の オートコンプリート を使用することができますまたは少なくともあなたはあなたにいくつかの導きを与えるために一見を与えることができます.

1
kEpEx

まだ誰も言及していないものを追加したいのですが:ng2-input-autocomplete

NPM: https://www.npmjs.com/package/ng2-input-autocomplete

GitHub: https://github.com/liuy97/ng2-input-autocomplete#readme

0
alchi baucha

私はこの答えの中のいくつかのアイデアに基づいて、かなりシンプルで再利用可能で機能的なAngular2オートコンプリートコンポーネントを構築しました。それは決して包括的ではありませんが、あなたがあなた自身のものを作ることにしたならば役に立つかもしれません。

コンポーネント:

import { Component, Input, Output, OnInit, ContentChild, EventEmitter, HostListener } from '@angular/core';
import { Observable } from "rxjs/Observable";
import { AutoCompleteRefDirective } from "./autocomplete.directive";

@Component({
    selector: 'autocomplete',
    template: `
<ng-content></ng-content>
<div class="autocomplete-wrapper" (click)="clickedInside($event)">
    <div class="list-group autocomplete" *ngIf="results">
        <a [routerLink]="" class="list-group-item" (click)="selectResult(result)" *ngFor="let result of results; let i = index" [innerHTML]="dataMapping(result) | highlight: query" [ngClass]="{'active': i == selectedIndex}"></a>
    </div>
</div>
    `,
    styleUrls: ['./autocomplete.component.css']
})
export class AutoCompleteComponent implements OnInit {

    @ContentChild(AutoCompleteRefDirective)
    public input: AutoCompleteRefDirective;

    @Input() data: (searchTerm: string) => Observable<any[]>;
    @Input() dataMapping: (obj: any) => string;
    @Output() onChange = new EventEmitter<any>();

    @HostListener('document:click', ['$event'])
    clickedOutside($event: any): void {
        this.clearResults();
    }

    public results: any[];
    public query: string;
    public selectedIndex: number = 0;
    private searchCounter: number = 0;

    ngOnInit(): void {
        this.input.change
            .subscribe((query: string) => {
                this.query = query;
                this.onChange.emit();
                this.searchCounter++;
                let counter = this.searchCounter;

                if (query) {
                    this.data(query)
                        .subscribe(data => {
                            if (counter == this.searchCounter) {
                                this.results = data;
                                this.input.hasResults = data.length > 0;
                                this.selectedIndex = 0;
                            }
                        });
                }
                else this.clearResults();
            });

        this.input.cancel
            .subscribe(() => {
                this.clearResults();
            });

        this.input.select
            .subscribe(() => {
                if (this.results && this.results.length > 0)
                {
                    this.selectResult(this.results[this.selectedIndex]);
                }
            });

        this.input.up
            .subscribe(() => {
                if (this.results && this.selectedIndex > 0) this.selectedIndex--;
            });

        this.input.down
            .subscribe(() => {
                if (this.results && this.selectedIndex + 1 < this.results.length) this.selectedIndex++;
            });
    }

    selectResult(result: any): void {
        this.onChange.emit(result);
        this.clearResults();
    }

    clickedInside($event: any): void {
        $event.preventDefault();
        $event.stopPropagation();
    }

    private clearResults(): void {
        this.results = [];
        this.selectedIndex = 0;
        this.searchCounter = 0;
        this.input.hasResults = false;
    }
}

コンポーネントCSS:

.autocomplete-wrapper {
    position: relative;
}

.autocomplete {
    position: absolute;
    z-index: 100;
    width: 100%;
}

ディレクティブ

import { Directive, Input, Output, HostListener, EventEmitter } from '@angular/core';

@Directive({
    selector: '[autocompleteRef]'
})
export class AutoCompleteRefDirective {
    @Input() hasResults: boolean = false;
    @Output() change = new EventEmitter<string>();
    @Output() cancel = new EventEmitter();
    @Output() select = new EventEmitter();
    @Output() up = new EventEmitter();
    @Output() down = new EventEmitter();

    @HostListener('input', ['$event'])
    oninput(event: any) {
        this.change.emit(event.target.value);
    }

    @HostListener('keydown', ['$event'])
    onkeydown(event: any)
    {
        switch (event.keyCode) {
            case 27:
                this.cancel.emit();
                return false;
            case 13:
                var hasResults = this.hasResults;
                this.select.emit();
                return !hasResults;
            case 38:
                this.up.emit();
                return false;
            case 40:
                this.down.emit();
                return false;
            default:
        }
    }
}

ハイライトパイプ:

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

@Pipe({
    name: 'highlight'
})

export class HighlightPipe implements PipeTransform {
    transform(value: string, args: any): any {
        var re = new RegExp(args, 'gi');

        return value.replace(re, function (match) {
            return "<strong>" + match + "</strong>";
        })

    }
}

実装:

import { Component } from '@angular/core';
import { Observable } from "rxjs/Observable";
import { Subscriber } from "rxjs/Subscriber";

@Component({
    selector: 'home',
    template: `
<autocomplete [data]="getData" [dataMapping]="dataMapping" (onChange)="change($event)">
    <input type="text" class="form-control" name="AutoComplete" placeholder="Search..." autocomplete="off" autocompleteRef />
</autocomplete>
    `
})
export class HomeComponent {

    getData = (query: string) => this.search(query);

    // The dataMapping property controls the mapping of an object returned via getData.
    // to a string that can be displayed to the use as an option to select.
    dataMapping = (obj: any) => obj;

    // This function is called any time a change is made in the autocomplete.
    // When the text is changed manually, no object is passed.
    // When a selection is made the object is passed.
    change(obj: any): void {
        if (obj) {
            // You can do pretty much anything here as the entire object is passed if it's been selected.
            // Navigate to another page, update a model etc.
            alert(obj);
        }
    }

    private searchData = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];

    // This function mimics an Observable http service call.
    // In reality it's probably calling your API, but today it's looking at mock static data.
    private search(query: string): Observable<any>
    {
        return new Observable<any>((subscriber: Subscriber<any>) => subscriber
            .next())
            .map(o => this.searchData.filter(d => d.indexOf(query) > -1));
    }
}
0
James Law

Anuglar2オートコンプリート用のモジュールを作成しました。このモジュールでは、配列、またはURL npmリンクを使用できます。 ang2-autocomplete

angle-materialのオートコンプリートで利用可能なビルドインコンポーネント

確認するには、 をクリックしてください

注:このオートコンプリートのフィルタロジックを要件に合わせてカスタマイズすることもできます。

0
Ashish Sharma