Главная » Статьи » Создание в Angular шаблонов с функцией поиска

Создание в Angular шаблонов с функцией поиска

Создание в Angular шаблонов с функцией поиска

От автора: одним из моих последних заданий было разработать общий метод, чтобы сделать компоненты и шаблоны с функцией поиска в приложении. В этой статье я расскажу вам о процессе и идеях, лежащих в основе решений, которые я создал.

Как всегда, просто чтобы понять, как будет реализован в 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.