Недавно появилась задача в одном достаточно крупном проекте ограничивать UI и функционал пользователей, в зависимости от их ролей. К этому моменту приложение уже разрослось на 100+ компонент, и это при том что основные базовые компоненты вынесены в отдельную репу и ставятся пакетом. То есть примерно в каждом из 100+, вероятно придется вносить некоторые правки, связанные с правами доступа.

Первой мыслью было пройтись по всем компонентам, заинжектить через DI UserService, прокинуть в шаблон контекст пользователя, и там с этим как-то разбираться. Однако, на счастье, я не успел этим заняться, так как нашел другое, как мне кажется более удачное решение.

exportAs

Я подумал о том, как можно сразу в шаблонах получить userContext, без необходимости инжектировать сервис или токен с пользователем в контроллерах компонент. На счастье вспомнилась весьма удобная штучка: свойство директив (ну и компонент, как наследников, естественно) - exportAs.

Что же дает exportAs?

Вот что нам сообщает документация:

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

Как же этим воспользоваться?

Допустим, у нас есть сервис, который авторизовался и хранит данные о пользователе.

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private user$: User;

  set user(user: User) {
    this.user$ = user;
  }

  get user() {
    return this.user$;
  }

  constructor(
    private _http: HttpClientWrapperService
  ) {
  }
  
  ...
}

И есть класс, описывающий пользователя

export class User {
  id: number;
  name: string;
  claims: Claim[];

  get roles() {
    return this.claims?.map(_ => _.code);
  }

  get isAdmin() {
    return this.roles?.indexOf('ADMIN') >= 0;
  }

  get isUser() {
    return this.isAdmin || this.roles?.indexOf('USER') >= 0;
  }

  get isGuest() {
    return this.isAdmin || this.isUser || this.roles?.indexOf('GUEST') >= 0;
  }
  ...
}

Что нам нужно - создать директиву, которая будет иметь доступ к пользователю, например такую:

import {Directive} from '@angular/core';
import {UserService} from '../../service/user.service';

@Directive({
  selector: '[appUserContext]',
  exportAs: 'userContext'
})
export class UserContextDirective {
  get user() {
    return this._user?.user;
  }

  constructor(private _user: UserService) {
  }
}

И использовать ее по надобности в шаблонах приложения. Например так, если добавлять у удалять объект может только администратор.

<div class="column content-block" appUserContext #user="userContext">
	<app-toolbar>
		<ng-container *ngIf="user.user?.isAdmin">
			<button app-button (click)="add()">
				<app-icon [name]="'add'"></teta-icon>
			</button>
			<button app-button (click)="delete()">
				<teta-icon [name]="'delete'"></teta-icon>
			</button>
		</ng-container>
	</app-toolbar>
</div>

Также данная функция может использована для получения доступа к директивам и компонентам в шаблоне, без необходимости использовать ViewChild там, где для этого нет большой необходимости.

Спасибо за внимание, надеюсь эта информация окажется для вас полезной и сэкономит кому-то время.

Комментарии (5)


  1. Hrodvitnir
    16.04.2022 05:35
    +2

    Хорошая статья, спасибо.

    Действительно забыл про этот функционал.


    1. Vahman Автор
      16.04.2022 10:44
      +1

      Спасибо. Трудно назвать это статьёй, скорее заметка. На самом деле прямо в процессе работы решил написать, так как функция, кажется не очень популярная, но весьма полезная.


  1. Alfinity
    18.04.2022 09:33
    +1

    Почему бы не создать структурную директиву? Это избавило бы разработчика, как минимум, от объявления переменной и улучшило читаемость кода.


    1. Vahman Автор
      18.04.2022 10:01

      На самом деле, структурная директива тоже есть. Но она не всегда решает поставленную задачу полностью. Если нужно именно скрыть часть UI, то да, но помимо этого есть необходимость определять input свойства дочерних компонент, не скрывая их целиком.

      <app-table [editable]="user.isEditor"></app-table>


      1. Alfinity
        18.04.2022 12:02

        Хороший пример, спасибо! Но я, пожалуй, воспользуюсь сервисом без директивы-прослойки в виду большей очевидности, подхода. имхо