Структурная директива Angular с HostBinding

В проекте нужна структурная директива, которая по условию изменяет состояние элемента, на который накинута.

Покажу на примере простенькой директивы shimmer:

@Directive({
    selector: '[shimmer]'
})
export class ShimmerDirective {

    private _shimmer = false;

    @HostBinding('class.shimmer')
    public get shimmerBinding(): boolean {
        return this._shimmer;
    }

    @Input()
    public set shimmer(value: boolean ) {
        this._shimmer = value;
    }

    constructor(
        private templateRef: TemplateRef<any>,
        private viewContainer: ViewContainerRef
    ) {
        this.viewContainer.createEmbeddedView(this.templateRef);
    }
}

Использую эту директиву вот так:

<button (click)="loading=!loading">
    переключить
</button>
{{ loading }}
<p *shimmer="loading">проверка шиммера</p>

По идее, после нажатия на кнопку "переключить", на элемент p должен навеситься класс shimmer, но этого не происходит:

активное состояние директивы

введите сюда описание изображения

Выбрана именно структурная директива не просто так, нужна дополнительная обрабатываемая логика, так что способ изменить структурную директиву на атрибутивную не подходит


Ответы (1 шт):

Автор решения: Pavel Mayorov

Любая структурная директива - это просто замаскированный ng-template, то есть ваша разметка эквивалентна вот такой:

<button (click)="loading=!loading">
    переключить
</button>
{{ loading }}
<ng-template [shimmer]="loading">
    <p>проверка шиммера</p>
</ng-template>

Легко видеть, что любые ваши хост-биндинги тут будут применяться именно к ng-template, а не к тому элементу на который вы "вешаете" структурную директиву.


Теперь что делать нужно в таком случае. А нужно получить сохранить ViewRef и добавить класс напрямую в rootNodes через Renderer2:

    private viewRef: EmbeddedViewRef;

    constructor(
        templateRef: TemplateRef<any>,
        viewContainer: ViewContainerRef,
        private renderer: Renderer2,
    ) {
        this.viewRef = this.viewContainer.createEmbeddedView(this.templateRef);
    }

    @Input()
    public set shimmer(value: boolean ) {
        this._shimmer = value;
        
        for (const node of this.viewRef.rootNodes) {
            if (value) {
                this.renderer.addClass(node, 'shimmer');
            } else {
                this.renderer.removeClass(node, 'shimmer');
            }
        }
    }    

Ну и всё-таки рассмотрите возможность сделать свою директиву обычной, а не структурной. Помните, что даже обычная директива имеет доступ к элементу на который её навесили через ElementRef.

→ Ссылка