От автора: одним из моих последних заданий было разработать общий метод, чтобы сделать компоненты и шаблоны с функцией поиска в приложении. В этой статье я расскажу вам о процессе и идеях, лежащих в основе решений, которые я создал.
Как всегда, просто чтобы понять, как будет реализован в Angular поиск, давайте сначала посмотрим красивую визуализацию конечного результата:
И разметка для кода:
<input [formControl]="searchControl"> <searchable-container [searchTerm]="searchControl.value"> <ul> <li searchable="Javascript" searchableHighlight></li> <li searchable="Angular" searchableHighlight></li> <li searchable="Typescript" searchableHighlight></li> <li searchable="RxJS" searchableHighlight></li> <li searchable="Akita" searchableHighlight></li> </ul> </searchable-container>
Создание Контейнера
Моей первой мыслью было использовать что-то вроде ViewChildren или ContentChildren, чтобы захватить searchable элементы, но у этого есть один большой недостаток.
В случае ViewChildren мы ограничены текущим представлением, а в случае ContentChildren — текущим разделом ng-content. Да, я знаю, что мы можем использовать опцию потомков ContentChildren, но нам нужна гибкость. Мы хотим дать пользователям возможность добавлять директивы searchable в любом месте иерархии шаблонов.
Для достижения этой цели мы использовали мощную функцию Angular - Element Injector. Как вы, возможно, знаете, мы, как и сервис, можем запросить Angular предоставить нам директиву через внедрение зависимостей. Например:
<parent> <div child>Element Injector</div> </parent>
@Component({ selector: 'parent', template: '<ng-content></ng-content>' }) export class ParentComponent {} @Directive({ selector: '[child]' }) export class ChildDirective { constructor(@Optional() private parent: ParentComponent) { if(!parent) { // use default or throw an error } } }
Затем Angular будет искать начальный элемент текущего хоста ParentComponent, пока не достигнет корневого компонента. Если Angular не сможет его найти, он выдаст ошибку, поэтому мы используем декоратор @Optional.
Возвращаемся к нашему коду. Мы будем использовать эту функцию для регистрации каждой директивы searchable в нашем компоненте SearchableContainer.
@Component({ selector: 'searchable-container', template: ` <ng-content></ng-content> ` changeDetection: ChangeDetectionStrategy.OnPush }) export class SearchableContainerComponent { private searchables: SearchableDirective[] = []; private _term = ''; @Input('searchTerm') set term(searchTerm: string) { this._term = searchTerm || ''; this.search(this._term); } get term() { return this._term; } register(searchable: SearchableDirective) { this.searchables.push(searchable); } unregister(searchable: SearchableDirective) { this.searchables = this.searchables.filter(current => current !== searchable); } }
Мы определяем объект input, который принимает текущий поисковый термин, и вызываем вместе с ним метод search() (вскоре мы продемонстрируем реализацию этого метода). Мы также предоставляем методы register и директивы unregister для поиска. Давайте перейдем к SearchableDirective.
Создание директивы Searchable
@Directive({ selector: '[searchable]' }) export class SearchableDirective { token = ''; @Input() set searchable(value: string) { this.token = value; } constructor(@Optional() private container: SearchableContainerComponent, private host: ElementRef) { if (!container) { throw new Error(`Missing <dato-searchable-container> wrapper component`); } } ngOnInit() { this.container.register(this); } hide() { this.host.nativeElement.classList.add('hide'); } show() { this.host.nativeElement.classList.remove('hide'); } ngOnDestroy() { this.container.unregister(this); } }
Как показано выше, мы указываем Angular предоставить нам SearchableContainer и зарегистрировать текущий экземпляр. Мы ожидаем, что компонент SearchableContainer будет представлен, поэтому мы выдадим ошибку, если это не так.
Мы также предоставляем методы для скрытия и отображения собственного элемента хоста. В этом случае я решил использовать стратегию CSS, а не использовать структурные директивы, потому что операции создания DOM очень затратны, и я не хочу создавать и удалять элементы DOM каждый раз, когда пользователь что-то ищет.
Да, я знаю, что могу использовать метод insert() ViewContainerRef и оставить ссылку на представление, чтобы сохранить элементы, но это кажется излишним для моих потребностей.
Если по какой-то причине вам нужна эта возможность, вы всегда можете перейти к структурным директивам. Теперь вернемся к реализации метода search():
search(searchTerm: string) { this.handleSearchables(searchTerm); } private handleSearchables(searchTerm: string) { for (const searchable of this.searchables) { if (!searchTerm) { searchable.show(); } else { if (this.match(searchable)) { searchable.show(); } else { searchable.hide(); } } } } private match(searchable: SearchableDirective) { return searchable.token.toLowerCase().indexOf(this._term.toLowerCase()) > -1; }
Когда мы получаем новый критерий поиска, нам нужно перебрать директивы searchable , проверить, есть ли совпадение, и запустить соответствующий метод.
На данный момент у нас есть основной рабочий поиск. Но дизайнеры всегда хотят большего. Нам нужно выделить соответствующий термин. У нас в приложении уже есть выделенный канал, который делает именно это, и он выглядит примерно так:
<input [formControl]="searchControl"> <searchable-container [searchTerm]="searchControl.value"> <ul> <li searchable="Javascript"> {{ Javascript | highlight: searchControl.value } </li> <li searchable="Angular"> {{ Angular | highlight: searchControl.value } </li> ... </ul> </searchable-container>
Хотя это работает, это не мое любимое решение. Оно добавляет детали к шаблону, а затем мне нужно повторить себя и добавить значение поиска для каждого элемента. Короче это не DRY.
Вот я и подумал об отношениях. Маркер зависит от родителя, SearchContainerа также зависит от того Searchable, который может быть его родителем или находиться в том же хосте. Так почему бы снова не использовать дерево инжекторов элементов Angular и не очистить его?
<searchable-container [searchTerm]="searchControl.value"> <ul> <li searchable="Javascript" searchableHighlight></li> <li searchable="Angular"> Learn <span searchableHighlight></span> // it can be anywhere </li> ... </ul> </searchable-container>
Создание директивы SearchableHighlight
@Directive({ selector: '[searchableHighlight]' }) export class SearchableHighlightDirective { constructor(@Optional() private container: SearchableContainerComponent, @Optional() private searchable: SearchableDirective, private sanitizer: DomSanitizer, private host: ElementRef) { if (!searchable) { throw new Error(`Missing [searchable] directive`); } } ngOnInit() { this.container.register(this, { highlight: true }); } ngOnDestroy() { this.container.unregister(this, { highlight: true }); } get token() { return this.searchable.token; } highlight(token: string, searchTerm: string) { this.host.nativeElement.innerHTML = this.sanitizer.sanitize(SecurityContext.HTML, this.resolve(token, searchTerm)); } resolve(token: string, searchTerm: string) { ...removed for brevity, you can see it in the full demo } }
Как я упоминал ранее, эта директива зависит от SearchableContainer и SearchableDirective, поэтому мы указываем Angular предоставить нам обе. Мы помечаем их как Optional() и выдаем ошибку, если не можем найти ни одной из них, потому что они необходимы.
Мы регистрируем экземпляр в контейнере, поэтому можем управлять им точно так же, как мы это делали с SearchableDirective. Метод highlight() принимает для поиска маркеры и текущее условие поиска и устанавливает принимающий элемент innerHTML после того, как я санировал его по соображениям безопасности.
Метод resolve() возвращает совпадающий термин обернутый в span, чтобы мы могли применить к нему стили CSS.
Давайте посмотрим на изменения SearchableContainer:
@Component({ selector: 'searchable-container', template: ` <ng-content></ng-content> ` }) export class SearchableContainerComponent { private searchables: SearchableDirective[] = []; private searchablesHighlight: SearchableHighlightDirective[] = []; ... search(searchTerm: string) { this.handleSearchables(searchTerm); this.handleHighlighters(searchTerm); } register(searchable, { highlight }) { // add the instance } unregister(searchable, { highlight }) { // remove the instance } ngAfterContentInit() { this.search(this.term); } private match(searchable: SearchableDirective) { return searchable.token.toLowerCase().indexOf(this._term.toLowerCase()) > -1; } private handleSearchables(searchTerm: string) { ..same } private handleHighlighters(searchTerm: string) { for (const searchableHighlight of this.searchablesHighlight) { searchableHighlight.highlight(searchableHighlight.token, searchTerm); } } }
Мы добавили массив для хранения директив SearchableHighlight. Когда мы получаем новый поисковый термин, мы обрабатываем его через цикл и вызываем метод highlight(), передающий маркер и поисковый термин.
Обработка количества результатов
Давайте в конце добавим возможность просмотра количества результатов. Сначала мы добавим в наш компонент SearchableContainer счетчик:
@Component({...}) export class SearchableContainerComponent { private _count = 0; get count() { return this._count; } set count(count: number) { this._count = count; } ... private handleSearchables(searchTerm: string) { let count = 0; for (const searchable of this.searchables) { if (!searchTerm) { searchable.show(); count++; } else { if (this.match(searchable)) { searchable.show(); count++; } else { searchable.hide(); } } } this.count = count; } ... }
Я не думаю, что есть необходимость объяснять приведенный выше код. Это простой счетчик, который отслеживает длину директив Searchables.
Теперь нам нужно ввести это в представление. Мы собираемся использовать функцию Angular, с которой не все знакомы — exportAs.
Я уже написал специальную статью по этой теме, но вкратце это свойство exportAs позволяет нам предоставлять директиву public API для шаблона. Теперь мы можем получить доступ в нашем шаблоне к экземпляру SearchableContainer:
@Component({ selector: 'searchable-container', template: ` <ng-content></ng-content> `, exportAs: 'searchableContainer', <======== changeDetection: ChangeDetectionStrategy.OnPush }) export class SearchableContainerComponent { ... }
Мы создаем локальную переменную с именем container, которая является ссылкой на экземпляр SearchableContainer.
Заключение
Фух … это было небыстро. Мы узнали, как можно использовать инжектор элементов Angular, чтобы очистить код и сделать его многократно используемым. Затем мы рассмотрели декоратор Optional() и функцию exportAs.
Конечно, это не конец. В нашем приложении код более оптимизирован, и мы поддерживаем «более поздние поступления». (подсказка: вы знаете, когда был добавлен новый поиск).
Цель состояла в том, чтобы вдохновить вас идеей создания компонентов с возможностью поиска в Angular. Вы можете взять код отсюда:
Автор: Netanel Basal
Источник: https://netbasal.com/
Редакция: Команда webformyself.