angular 12 全局搜索组件

Fork Me On Github

一个全局的 SearchService

ts
                                                                                                                        
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
    providedIn: 'root',
})
export class SearchService {

    /**
     * 输入文字发送改变
     */
    static EVENT_CHANGE = 'change';
    /**
     * 确认搜索
     */
    static EVENT_CONFIRM = 'confirm';

    /**
     * 根据文字设置搜索建议
     */
    static EVENT_CHANGE_SUGGEST = 'suggest';

    private eventPair: {
        [trigger: string]: string;
    } = {
        [SearchService.EVENT_CHANGE]: SearchService.EVENT_CHANGE_SUGGEST
    };

    private listeners: {
        [key: string]: Function[];
    } = {};

    constructor(
    ) {
    }

    public on(event: 'change', cb: (keywords: string) => void|boolean|Observable<any[]>): this;
    public on(event: 'confirm', cb: (keywords: any) => void|false): this;
    public on(event: 'suggest', cb: (items: any[]) => void): this;
    public on(event: string, cb: (...items: any[]) => void|boolean|Observable<any>): this;
    public on(event: string, cb: any) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, event)) {
            this.listeners[event] = [];
        }
        this.listeners[event].push(cb);
        return this;
    }

    public emit(event: 'change', keywords: string): this;
    public emit(event: 'confirm', keywords: any): this;
    public emit(event: 'suggest', items: any[]): this;
    public emit(event: string, ...items: any[]): this;
    public emit(event: string, ...items: any[]) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, event)) {
            return this;
        }
        const pair = this.eventPair[event];
        const listeners = this.listeners[event];
        for (let i = listeners.length - 1; i >= 0; i--) {
            const cb = listeners[i];
            const res = cb(...items);
            //  允许事件不进行传递
            if (res === false) {
                break;
            }
            if (!res || !pair) {
                continue;
            }
            // 接受订阅
            if (res instanceof Observable) {
                res.subscribe(data => {
                    this.emit(pair, data);
                });
                continue;
            }
            this.emit(pair, res);
        }
        return this;
    }

    public off(...events: string[]): this;
    public off(event: string, cb: Function): this;
    public off(...events: any[]) {
        if (events.length == 2 && typeof events[1] === 'function') {
            return this.offListener(events[0], events[1]);
        }
        for (const event of events) {
            delete this.listeners[event];
        }
        return this;
    }

    /**
     * 移除搜索框页面的接受事件
     */
    public offTrigger() {
        return this.off(SearchService.EVENT_CHANGE_SUGGEST);
    }

    /**
     * 移除搜索结果页面的接受事件
     */
    public offReceiver() {
        return this.off(SearchService.EVENT_CHANGE, SearchService.EVENT_CONFIRM);
    }

    private offListener(event: string, cb: Function): this {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, event)) {
            return this;
        }
        const items = this.listeners[event];
        for (let i = items.length - 1; i >= 0; i--) {
            if (items[i] === cb) {
                items.splice(i, 1);
            }
        }
        return this;
    }

}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120

注册全局 service

ts
          
export class ThemeModule {
    static forRoot(): ModuleWithProviders<ThemeModule> {
        return {
            ngModule: ThemeModule,
            providers: [
                SearchService,
            ]
        };
    }
}
12345678910
ts
      
@NgModule({
    imports: [
        ThemeModule.forRoot(),
    ]
})
export class AppModule { }
123456

添加搜索框

html
             
<div class="dialog-search" [ngClass]="{inputting: suggestText.length > 0}" [hidden]="!panelVisible">
    <i class="iconfont icon-close dialog-close" (click)="close()"></i>
    <div class="dialog-body">
        <div class="search-input">
            <i class="iconfont icon-search input-search"></i>
            <input type="text" placeholder="请输入关键字,按回车 / Enter 搜索" autocomplete="off" [(ngModel)]="suggestText" (keydown)="suggestKeyPress($event)" (ngModelChange)="onSuggestChange()">
            <i class="iconfont icon-close input-clear" (click)="tapClear()"></i>
        </div>
        <ul class="search-suggestion">
            <li *ngFor="let item of suggestItems;let i = index" [ngClass]="{active: i === suggestIndex}" (click)="tapItem(item)"><span>{{ i + 1 }}</span>{{ formatTitle(item) }}</li>
        </ul>
    </div>        
</div>
12345678910111213
ts
                                                                                                        
import { Component, OnDestroy, OnInit } from '@angular/core';
import { SearchService } from '../../theme/services';

@Component({
  selector: 'app-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit, OnDestroy {

    public panelVisible = false;
    public suggestItems: any[] = [];
    public suggestText = '';
    public suggestIndex = -1;
    private asyncHandle = 0;

    constructor(
        private searchService: SearchService,
    ) { }

    ngOnInit() {
        this.searchService.on('suggest', items => {
            this.suggestIndex = -1;
            this.suggestItems = items;
        });
    }

    ngOnDestroy() {
        this.searchService.offTrigger();
    }

    public formatTitle(item: any) {
        if (typeof item !== 'object') {
            return item;
        }
        // 这里只接受 title 或 name 属性进行显示
        return item.title || item.name;
    }

    public suggestKeyPress(event: KeyboardEvent) {
        if (event.key === 'Enter') {
            this.searchService.emit('confirm', this.suggestIndex >= 0 ? this.suggestItems[this.suggestIndex] : this.suggestText);
            this.close();
            return;
        }
        if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
            this.suggestIndex = -1;
            return;
        }
        if (this.suggestItems.length < 0) {
            return;
        }
        let i = this.suggestIndex;
        if (event.key === 'ArrowDown') {
            i = i < this.suggestItems.length - 1 ? i + 1 : 0;
        } else if (event.key === 'ArrowUp') {
            i = (i < 1 ? this.suggestItems.length: i) - 1;
        }
        this.suggestIndex = i;
        this.suggestText = this.formatTitle(this.suggestItems[this.suggestIndex]);
    }

    public onSuggestChange() {
        if (this.suggestIndex >= 0) {
            return;
        }
        this.asyncSuggest();
    }

    public tapItem(item: any) {
        this.searchService.emit('confirm', item);
        this.close();
    }

    public tapClear() {
        this.suggestText = '';
        this.suggestItems = [];
    }

    public open() {
        this.panelVisible = true;
    }

    public close() {
        this.panelVisible = false;
    }

    private asyncSuggest() {
        if (this.asyncHandle) {
            clearTimeout(this.asyncHandle);
        }
        this.suggestIndex = -1;
        this.asyncHandle = window.setTimeout(() => {
            this.asyncHandle = 0;
            this.suggestIndex = -1;
            if (this.suggestText.length < 1) {
                this.suggestItems = [];
                return;
            }
            this.searchService.emit('change', this.suggestText);
        }, 300);
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104

搜索页面

其他页面,根据搜索关键词跳转搜索页面或详情页。

ts
                            
export class BlogComponent implements OnInit, OnDestroy {

    constructor(
        private searchService: SearchService,
        private service: BlogService,
        private router: Router,
        private route: ActivatedRoute,
    ) {
    }

    ngOnInit() {
        this.searchService.on('change', keywords => {
            return this.service.suggesttion({keywords});
        }).on('confirm', res => {
            if (typeof res === 'object') {
                this.router.navigate([res.id], {relativeTo: this.route});
                return;
            }
            this.router.navigate(['./'], {relativeTo: this.route, queryParams: {
                keywords: res
            }});
        });
    }

    ngOnDestroy() {
        this.searchService.offReceiver();
    }
}
12345678910111213141516171819202122232425262728

如果当前就是搜索页面,那么可以不用跳转

ts
                
private searchFn = res => {
    if (typeof res === 'object') {
        return;
    }
    this.queries.keywords = res;
    this.tapRefresh();
    // 阻止事件传递
    return false;
};
ngOnInit() {
    this.searchService.on('confirm', this.searchFn);
}

ngOnDestroy() {
    this.searchService.off('confirm', this.searchFn);
}
12345678910111213141516

注意

搜索事件并不会自动清除,需要添加 ngOnDestroy 进行清除

点击查看全文
标签: angular
0 304 0
在 angular 项目中实现对页面的访问控制
按下回车键,焦点移动到下一个表单或提交表单
使用ng-template 显示tree结构数据
使用 ViewContainerRef.createComponent 替代 ComponentFactoryResolver
angular 12使用 KaTex 显示 AsciiMath 格式的公式