От автора: одним из моих последних заданий было создать настраиваемые раскрывающиеся Angular компоненты, которые поддерживают как одиночный, так и множественный выбор. Как всегда, я поделюсь с вами некоторыми советами в надежде, что вы узнаете новые вещи.
Начнем с подготовки основы с двумя ключевыми компонентами — нам нужны компоненты dato-select и dato-option.
1 2 3 4 5 6 7 8 9 10 |
@Component({ selector: 'dato-option', template: ` <div class="dato-option" [ngClass]="..."> <ng-content></ng-content> </div> `, }) export class DatoOptionComponent implements OnInit { } |
1 2 3 4 5 6 7 8 9 10 |
@Component({ selector: 'dato-select', template: `<ng-content></ng-content>` }) export class DatoSelectComponent { @ContentChildren(DatoOptionComponent) options: QueryList<DatoOptionComponent>; ngAfterContentInit() { } } |
Совет 1
Поскольку это раскрывающийся список, который поддерживает множественный выбор, мне нужно добавить галочку, которая указывает, выбран ли параметр.
Один из способов добиться этого — добавить type input() и в соответствии с его значением показать и скрыть разметку.
1 2 3 4 5 6 7 8 9 10 11 12 |
@Component({ selector: 'dato-option', template: ` <div class="dato-option" [ngClass]="..."> <input type="checkbox" *ngIf="isMulti"> <ng-content></ng-content> </div> `, }) export class DatoOptionComponent { @Input() isMulti = false; } |
Да, это сработает, но недостатком этого подхода является то, что мы добавили еще одну проверку для каждого параметра в Angular, поэтому в раскрывающемся меню с 10 000 опциями мы добавили еще 10 000 проверок, и это только один ngIf; обычно он будет сопровождаться ngClass, ngStyle и т. д.
В моем случае производительность очень важна, поэтому я решил разделить multi-option на другой компонент, который наследует single-option, чтобы избежать избыточных проверок.
Кроме того, все еще есть части разметки, которые имеют общий характер, поэтому я буду использовать строки шаблонов, чтобы сохранить мой код DRY.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
export function getOptionTemplate(isMulti = false) { return ` <div class="dato-option"> ${isMulti ? '<input type="checkbox">' : ''} <ng-content></ng-content> </div> ` } @Component({ selector: 'dato-option', template: getOptionTemplate(), }) export class DatoOptionComponent { } |
1 2 3 4 5 6 |
@Component({ selector: 'dato-option-multi', template: getOptionTemplate(true) }) export class DatoOptionMultiComponent extends DatoOptionComponent { } |
Совет 2
Предыдущее изменение вызвало новую проблему. Предположим, я хочу использовать раскрывающееся multi-select с multi-select:
1 2 3 4 5 6 7 |
@Component({ selector: 'dato-select', template: `<ng-content></ng-content>` }) export class DatoSelectComponent { @ContentChildren(DatoOptionComponent) options: QueryList<DatoOptionComponent>; } |
1 2 3 |
<dato-select> <dato-option-multi *ngFor="let option of options">{{option}}</dato-option-multi> </dato-select> |
Это не сработает, так как DatoOptionComponent ContentChildren ищет DatoOptionComponent. Если нам нужна ссылка на option-multi компоненты, нам нужно добавить другой запрос ContentChildren.
1 2 |
@ContentChildren(DatoOptionComponent) options: QueryList<DatoOptionComponent>; @ContentChildren(DatoOptionMultiComponent) optionsMulti: QueryList<DatoOptionMultiComponent>; |
Но теперь мы должны поддерживать оба. К счастью, мы можем решить это более элегантно с помощью Angular DI.
1 2 3 4 5 6 7 |
@Component({ selector: 'dato-option-multi', template: getOptionTemplate(true), providers: [{provide: DatoOptionComponent, useExisting: DatoOptionMultiComponent}] }) export class DatoOptionMultiComponent extends DatoOptionComponent { } |
Мы можем сказать Angular — когда вам нужен DatoOptionComponent, используйте DatoOptionMultiComponent.
Совет 3
Я еще не был доволен. Я хотел использовать тот же селектор — dato-option — для одиночных и нескольких выпадающих компонентов. Поэтому я изменил селектор с multi-option, чтобы быть тем же.
1 2 3 4 5 6 |
@Component({ selector: 'dato-option', ... }) export class DatoOptionMultiComponent extends DatoOptionComponent { } |
Но теперь у меня проблема. Angular не допускает дублирования селекторов и выдает ошибку:
Более одного компонента соответствует этому элементу. Убедитесь, что селектор только одного компонента может соответствовать данному элементу.
Способ, которым я решил это, заключается в том, что Angular поддерживает :not CSS-селектор. (наряду с другими полезными селекторами)
1 2 3 4 5 6 |
@Component({ selector: 'dato-option:not([multi])', ... }) export class DatoOptionComponent implements OnInit { } |
1 2 3 4 5 6 |
@Component({ selector: 'dato-option[multi]', ... }) export class DatoOptionMultiComponent extends DatoOptionComponent { } |
Теперь мы можем придерживаться одного и того же селектора и добавлять атрибут, когда нам нужна multi-option. Это кажется более уместным, чем введение совершенно нового селектора.
Совет 4
В раскрывающемся списке есть окно поиска, которое по умолчанию выполняет внутренний поиск. Нам также нужна опция для поиска на стороне сервера.
Один из способов добиться этого — создать как input() и output():
1 2 3 4 5 6 7 8 9 |
@Component({ selector: 'dato-select', template: `<ng-content></ng-content>` }) export class DatoSelectComponent { @ContentChildren(DatoOptionComponent) options: QueryList<DatoOptionComponent>; @Input() internalSearch = true; @Output() search = new EventEmitter(); } |
Но кажется, что ввод избыточен и его можно избежать; Мы можем договориться о том, что кто-то слушает событие search и основывается на том, чтобы решить, нужно ли активировать внутренний поиск или нет.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Component({ selector: 'dato-select', template: `<ng-content></ng-content>` }) export class DatoSelectComponent { @ContentChildren(DatoOptionComponent) options: QueryList<DatoOptionComponent>; @Output() search = new EventEmitter(); private internalSearch; ngOnInit() { this.internalSearch = this.search.observers.length === 0; } } |
Каждый эмитент событий ведет список своих наблюдателей. Если у нас есть хотя бы один наблюдатель, это означает, что кто-то подписался.
Я хочу воспользоваться этой возможностью, чтобы упомянуть, что мы недавно выступили с Akita, которая предлагает простое и эффективное управление состоянием для приложений с Angular системами. Проверьте это здесь: Представляем Akita: новый шаблон управления состоянием для приложений Angular.
Автор: Netanel Basal
Источник: //netbasal.com/
Редакция: Команда webformyself.