Если вы начинаете работать с angularjs, вам было довольно просто получить доступ к DOM и управлять им там. У вас был доступ к узлу DOM через element, введенный в link функцию директивы.

function link(scope, element, attrs) {
}

Или через angular.element, который был встроенным подмножеством jQuery в AngularJS. Но у этого подхода были свои недостатки. Это сделало ваш код тесно связанным с API браузера.

Новый Angular (начиная со 2) работает на нескольких платформах: мобильных, веб-воркерах и т. Д. Таким образом, они представили ряд API-интерфейсов для работы в качестве уровня абстракции между вашим кодом и API-интерфейсами платформы. Эти API-интерфейсы представлены в виде различных ссылочных типов, таких как ElementRef, TemplateRef, ViewRef, ComponentRef и ViewContainerRef.

В этом блоге мы увидим несколько примеров того, как эти ссылочные типы могут использоваться для управления DOM в angular. Но перед этим давайте посмотрим, как получить доступ к этим ссылочным типам в компоненте / директиве.

DOM запросы

Angular предоставляет два способа запроса / доступа к различным ссылочным типам внутри компонента / директивы. Эти

  • ViewChild / ViewChildren
  • ContentChild / ContentChildren

ViewChild / ViewChildren

Это декораторы, которые могут использоваться в компоненте / директиве как @ViewChild (возвращает одну ссылку) или @ViewChildren (возвращает список ссылок в форме QueryList). Они будут назначать значения ссылочных типов из шаблона полям компонентов, к которым они применяются. Основное использование следующее:

@ViewChild(selector, {read: ReferenceType}) fieldName;

Селектор может быть строкой, представляющей ссылочную переменную шаблона, или класс компонента / директивы, или TemplateRef, или поставщик, определенный в дереве дочерних компонентов.

@ViewChild("myElem") template: ElementRef;

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

@ViewChild("myContainer", {read: ViewContainerRef}) container: ViewContainerRef;

ContentChild / ContentChildren

Использование очень похоже на использование ViewChild / ViewChildren. Единственное отличие состоит в том, что он запрашивает <ng-content> спроецированных элементов компонента, а @ViewChild запрашивает внутри шаблона компонента. Это будет лучше объяснено на примерах в следующих разделах.

Доступ к DOM через ElementRef

ElementRef - это очень простой уровень абстракции для элемента DOM в Angular. Это угловая оболочка вокруг собственного элемента.

Вы можете получить ElementRef в компоненте или директиве следующими способами:

Внедрение зависимости

К основному элементу компонента или директивы можно получить доступ через прямой DI в конструкторе.

@Component({
  selector: 'app-test',
  template: '<div>I am a test component</div>'
})
export class TestComponent implements OnInit {

  constructor(private element: ElementRef) { }

  ngOnInit() {
    console.log(this.element.nativeElement);
  }

}
/*
* Output: 
*   <app-test>
*     <div>I am a test component</div>
*   </app-test>
* */

Использование переменных ViewChild и шаблонов

@Component({
  selector: 'app-test',
  template: `
    <div #child1>First Child</div>
    <div>Second Child</div>
  `
})
export class TestComponent implements OnInit {

  @ViewChild("child1") firstChild: ElementRef;

  constructor() { }

  ngOnInit() {
    console.log(this.firstChild.nativeElement);
  }

}

/*
* Output: <div>First Child</div>
* */

Использование ContentChild

Работает аналогично @ViewChild, но для <ng-content> проецируемых элементов.

// Child Component
@Component({
  selector: "component-a",
  template: `<ng-content></ng-content>`
})
export class ComponentA {
  @ContentChild("contentChild") contentChild: ElementRef;
  
  ngOnInit() {
    console.log(this.contentChild.nativeElement);
  }
}
// Parent Component
@Component({
  selector: 'app-test',
  template: `
    <component-a>
      <div #contentChild>Content Child 1</div>
      <div>Content Child 2</div>
    </component-a>
  `
})
export class TestComponent implements OnInit {}
/*
* Output: <div>Content Child 1</div>
* */

Выглядит довольно просто, что вы можете легко получить доступ к элементу DOM через ElementRef, а затем манипулировать DOM, обратившись к nativeElement. Что-то вроде этого:

@Component({
  selector: 'app-test-component',
  template: `
    <div class="header">I am a header</div>
    <div class="body" #body>
    </div>
    <div class="footer">I am a footer</div>
  `
})
export class TestComponent implements AfterContentInit {
  @ViewChild("body") bodyElem: ElementRef;

  ngAfterContentInit(): void {
    this.bodyElem.nativeElement.innerHTML = `<div>Hi, I am child added by directly calling the native APIs.</div>`;
  }

}

Однако прямое использование ElementRef не одобряется Angular Team, потому что он напрямую предоставляет доступ к DOM, что может сделать ваше приложение уязвимым для XSS-атак. Это также создает тесную связь между вашим приложением и уровнями рендеринга, что затрудняет запуск приложения на нескольких платформах.

Итак, как нам с этим бороться? Ответ: Просмотры.

В Angular все есть "представление"

Представление - это самый маленький строительный блок пользовательского интерфейса углового приложения. У каждого компонента свое представление. Вы можете рассматривать его как группу элементов, которые можно создавать и уничтожать вместе.

Представления можно разделить на два типа:

  • Встроенные представления - созданы из шаблонов
  • Представления хоста - созданы из компонентов

Отображение представления в пользовательском интерфейсе можно разбить на два этапа:

  1. Создание представления из шаблона или компонента
  2. Визуализация представления в контейнер представления

Встроенные представления

Встроенные представления создаются из шаблонов, определенных с помощью элемента <ng-template>.

Создание встроенного представления

Сначала необходимо получить доступ к шаблону внутри компонента как TemplateRef с использованием @ViewChild и ссылочной переменной шаблона. Затем встроенное представление может быть создано из TemplateRef путем передачи контекста привязки данных.

const viewRef = this.template.createEmbeddedView({
  name: "View 1"
});

Этот контекст используется шаблоном in<ng-template>.

<ng-template #template let-viewName="name">
  <div>Hi, My name is {{viewName}}. I am a view created from a template</div>
</ng-template>

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

const viewRef = this.template.createEmbeddedView({
  $implicit: "View 1"
});

В этом случае вы можете пропустить присвоение значений шаблонным переменным.

<ng-template #template let-viewName>
  <div>Hi, My name is {{viewName}}. I am a view created from a template</div>
</ng-template>

Визуализация встроенного представления

До сих пор мы создали только экземпляр ViewRef. Это представление все еще не отображается в пользовательском интерфейсе. Чтобы увидеть его в пользовательском интерфейсе, нам нужен заполнитель (контейнер представления) для его рендеринга. Этот заполнитель предоставляется ViewContainerRef.

Любой элемент может служить контейнером представления, однако <ng-container> - лучший кандидат, поскольку он отображается как комментарий и не оставляет никаких избыточных элементов в html DOM.

@Component({
  selector: 'app-test-component',
  template: `
    <div class="header">I am a header</div>
    <div class="body">
      <ng-container #container></ng-container>
    </div>
    <div class="footer">I am a footer</div>

    <ng-template #template let-viewName="name">
      <div>Hi, My name is {{viewName}}. I am a view created from a template</div>
    </ng-template>
  `,
})
export class TestComponent implements AfterContentInit {

  @ViewChild("template") template: TemplateRef;
  @ViewChild("container", {read: ViewContainerRef}) container: ViewContainerRef;

  constructor() { }

  ngAfterContentInit(): void {
    const viewRef = this.template.createEmbeddedView({
      name: "View 1"
    });
    this.container.insert(viewRef);
  }
}

Оба элемента <ng-container> и <ng-template> будут отображаться как комментарии, оставляя HTML DOM аккуратным и чистым.

Вышеупомянутый двухэтапный процесс создания представления и добавления его в контейнер можно дополнительно сократить, используя метод createEmbeddedView, доступный в самом ViewContainerRef.

this.container.createEmbeddedView(this.template, {
  name: "View 1"
});

Это можно еще больше упростить, переместив всю логику создания представления из класса компонента в шаблон с помощью ngTemplateOutlet и ngTemplateOutletContext.

@Component({
  selector: 'app-test-component',
  template: `
    <div class="header">I am a header</div>
    <div class="body">
      <ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{name: 'View 1'}"></ng-container>
    </div>
    <div class="footer">I am a footer</div>

    <ng-template #template let-viewName="name">
      <div>Hi, My name is {{viewName}}. I am a view created from a template</div>
    </ng-template>
  `
})
export class TestComponent {}

Просмотры хоста

Host Views очень похожи на Embedded View. Единственное отличие состоит в том, что Host Views создаются из компонентов, а не из шаблонов.

Создание основного представления

Чтобы создать основной вид, сначала вам нужно создать ComponentFactory компонента, который вы хотите визуализировать, используя ComponentFactoryResolver.

constructor(
  private componentFactoryResolver: ComponentFactoryResolver
) {
  this.someComponentFactory = this.componentFactoryResolver.resolveComponentFactory(SomeComponent);
}

Затем создается динамический экземпляр компонента путем передачи Injector экземпляра в фабрику. Каждый компонент должен быть привязан к экземпляру Injector. Вы можете использовать инжектор родительского компонента для динамически создаваемых компонентов.

const componentRef = this.someComponentFactory.create(this.injector);
const viewRef = componentRef.hostView;

Визуализация вида хоста

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

@Component({
  selector: 'app-test-component',
  template: `
    <div class="header">I am a header</div>
    <div class="body">
      <ng-container #container></ng-container>
    </div>
    <div class="footer">I am a footer</div>
  `,
})
export class TestComponentComponent implements AfterContentInit {

  @ViewChild("container", {read: ViewContainerRef}) container: ViewContainerRef;

  private someComponentFactory: ComponentFactory<SomeComponent>;

  constructor(
    private componentFactoryResolver: ComponentFactoryResolver,
    private injector: Injector
  ) {
    this.someComponentFactory = this.componentFactoryResolver.resolveComponentFactory(SomeComponent);
  }

  ngAfterContentInit(): void {
    const componentRef = this.someComponentFactory.create(this.injector);
    const viewRef = componentRef.hostView;
    this.container.insert(viewRef);
  }
}

Или путем прямого вызова createComponent метода ViewContainerRef и передачи экземпляра фабрики компонентов.

this.container.createComponent(this.someComponentFactory);

Теперь, как и во встроенном представлении, мы также можем перенести всю логику создания основного представления в самом шаблоне с помощью ngComponentOutlet.

@Component({
  selector: 'app-test-component',
  template: `
    <div class="header">I am a header</div>
    <div class="body">
      <ng-container [ngComponentOutlet]="comp"></ng-container>
    </div>
    <div class="footer">I am a footer</div>
  `
})
export class TestComponent {
  comp = SomeComponent;
}

Не забудьте сохранить ссылку на класс компонента в поле родительского компонента. В шаблоне есть доступ только к полям компонентов.

Резюме

Здесь мы подошли к концу. Подведем итог тому, что мы поняли до сих пор.

  • Мы можем получить доступ к DOM в Angular, используя различные ссылочные типы, такие как ElementRef, TemplateRef, ViewRef, ComponentRef и ViewContainerRef.
  • Эти ссылочные типы можно запросить из шаблонов, используя @ViewChild и @ContentChild.
  • Доступ к собственному элементу DOM браузера можно получить через ElementRef. Однако не рекомендуется напрямую манипулировать этим элементом из соображений безопасности.
  • Понятие просмотров.
  • Как создать и отобразить встроенный вид.
  • Как создать и отобразить компонентный вид.

Итак, на сегодня все, что нужно для понимания манипуляций с DOM в Angular. Это был мой первый блог. Пожалуйста, оставьте мне отзыв в комментариях.