Встроенные директивы предназначены для отображения контента по условию (*ngIf, *ngSwitch), повторения контента для каждого элемента в массиве (*ngFor), для применения стилей к html-элементам (ngStyle) или назначения им классов (ngCLass). Существуют и менее распространенные директивы, их мы рассмотрим ниже. Также пользователь может создавать свои директивы при необходимости.

Директивы обеспечивают выполнение самых распространенных и фундаментальных задач разработки веб-приложений и закладывают основу для адаптации контента, отображаемого для пользователя, на основании данных в приложении.

Директива *ngFor

Начнем с этой директивы для того, чтобы отобразить наши демо данные из условной «базы данных» в таблице. Для этого идеально подходит встроенная директива *ngFor.

Для начала нам нужно изменить шаблон. Давайте удалим блок form-inline, который мы добавляли в предыдущем разделе (он нужен был только для демонстрации). Также добавим директиву *ngFor второй строке в таблице, т.к. первая строка это заголовок он будет одним и неизменным. Нам нужно, чтобы для каждого товара повторялась именно строка таблицы и в ней выводились данные для каждого из них.

Листинг 5.1 | Файл: src/app/all-products/all-products.component.html
<div class="container">
  <div class="row">
    <h3 class="mr-3 mt-2">{{ title }}</h3>
    <table class="table table-striped text-center mt-3">
        <thead class="thead-dark">
          <tr>
            <th>ID</th>
            <th>Название</th>
            <th>Цена</th>
            <th>Кол-во</th>
          </tr>
        </thead>
        <tr *ngFor="let product of products">
        	<td>{{ product.id }}</td>
        	<td>{{ product.name }}</td>
        	<td>${{ product.price }}</td>
        	<td>{{ product.quantity }}</td>
        </tr>
      </table> <!-- /table -->
  </div>
</div>

Теперь более подробно о том, что мы сделали.

Тегу tr мы добавляем директиву *ngFor (синтаксис именно такой, со *), в директиве мы указываем, что на каждой итерации переменная product будет принимать значение следующего элемента в массиве products. Массив products был определен в нашем сервисе src/app/shared/data.service.ts и его значения получены в компоненте src/app/all-servers/all-servers.component.ts. Элементами данного массива являются объекты, в каждом из которых заданы свойства id, name, price, quantity.

Далее, при помощи строковой интерполяции, мы в каждой ячейке выводим значение необходимого свойства ({{products.name}}). Таким образом после изменения кода у вас должны вывестись все 4 товара по порядку.

Директива *ngFor имеет несколько разных значений, которые могут быть присвоены переменным.

Значение Тип данных Описание
index (number) Это порядковый номер текущего элемента. Отсчет начинается с 0
odd (boolean) Вернет true, если текущий элемент нечетный
even (boolean) Вернет true, если текущий элемент четный
first (boolean) Вернет true, если текущий элемент является первым
last (boolean) Вернет true, если текущий элемент является последним

Директива *ngIf

Следующей наиболее используемой встроенной директивой является *ngIf. Она позволяет вносить какие-либо изменения в шаблон при выполнении заданных условий. Давайте рассмотрим пример как мы можем ее использовать.

Скажем, мы хотим выводить товары с остатком (свойство quantity) больше 3.

Для этого внесем изменения в наш шаблон:

Листинг 5.2 | Файл: src/app/all-products/all-products.component.html
...
    <table class="table table-striped text-center mt-3">
      <thead class="thead-dark">
        <tr>
          <th>ID</th>
          <th>Название</th>
          <th>Цена</th>
          <th>Кол-во</th>
        </tr>
      </thead>
      <ng-container *ngFor="let product of products">
        <tr *ngIf="product.quantity > 3">
          <td>{{ product.id }}</td>
          <td>{{ product.name }}</td>
          <td>${{ product.price }}</td>
          <td>{{ product.quantity }}</td>
        </tr>
      </ng-container>
    </table> <!-- /.table -->
...

Итак, тут вы видите, что я добавил новый тег ng-container и прописал ему директиву *ngFor, а тегу tr *ngIf. Это сделано потому, что в Angular (начиная со второй версии, если не ошибаюсь) нельзя использовать две директивы в одном теге. Т.е. невозможно прописать их обе тегу tr, это вызовет ошибку. Было бы проще, если бы нам нужно было вывести данные не в таблице, а в обычных тегах div, тогда мы могли бы просто добавить один лишний тег только для условия ngIf. В случае с таблицами пришлось изменить шаблон именно на такой.

После того как мы определяем переменную product, в которую, напомню, входит объект товара, мы с помощью условия *ngIf="product.quantity > 3" выводим только те товары, кол-во которых больше 3. Вот таким образом работает данная директива.

Также условие можно расширить, к примеру, написать
*ngIf="product.quantity > 3 && product.price == 500"
Т.е. будут выведены товары, если выполнены оба условия (остаток больше 3 и цена равная 500).

*ngIf…else и *ngIf…then…else

В Angular есть возможность использовать if...else конструкцию. Мы рассмотрим их на примерах, не относящихся к нашей практической части. Синтаксис в таком случае будет следующий

<div *ngIf="isLoggedIn; else loggedOut">
  Добро пожаловать!
</div>

<ng-template #loggedOut>
  Пожалуйста, авторизуйтесь.
</ng-template>

В данном случае будет проверенна переменная isLoggedIn, и если она вернет true, то будет выведен блок div. В этом случае ng-template даже не будет отображаться в DOM. Если же isLoggedIn вернет false, тогда будет отображен шаблон #loggedOut.

Еще один вариант записи

<ng-container
  *ngIf="isLoggedIn; then loggedIn; else loggedOut">
</ng-container>

<ng-template #loggedIn>
  <div>
    Добро пожаловать!
  </div>
</ng-template>
<ng-template #loggedOut>
  <div>
    Пожалуйста, авторизуйтесь.
  </div>
</ng-template>

Эта запись использует следующую логику: ngIf = expression ? then : else;, т.е. если isLoggedIn=true, тогда будет загружен шаблон #loggedIn, если false — #loggedOut

Директивы ngClass и ngStyle

Давайте начнем с директивы ngClass. Синтаксис данной директивы такой — [ngClass]="выражение" и в зависимости от результата выполнения данного выражения, директива может принимать:

  • Строку — имена классов, разделенные пробелами. Например, "col-md-6 text-center mb-2"
  • Массив — каждый элемент массива назначается как отдельный класс. Например, ["col-md-6", "text-center", "mb-2"]
  • Объект (или правильнее сказать тип Map) — каждое свойство объекта задается как новый класс. В качестве значения свойства записывается выражение, которое должно вернуть true, чтобы данный класс был добавлен элементу.

Для начала проверим первый вариант и будем выводить класс для заголовка таблицы с помощью директивы ngClass. Для этого изменим шаблон, и в качестве выражения передадим функцию headClass(), которая просто будет возвращать строку с названием класса.

Листинг 5.3 | Файл: src/app/all-products/all-products.component.html
...
<table class="table table-striped text-center mt-3">
  <thead [ngClass]="headClass()">
...
Листинг 5.4 | Файл: src/app/all-servers/all-servers.component.ts
...
  editTitle() {
  	this.title = 'Новый тайтл';
  }

  headClass() {
  	return 'thead-dark';
  }
}

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

Функция у нас получилась самая примитивная, но она могла бы возвращать какое-либо значение в зависимости от выполнения условий. Например, если сегодня четный день недели тогда return false, в противном случае return "thead-dark". Вообщем все, что вы можете сделать при помощи обычного JavaScript, просто тут показано как применить это к Angular.

После этого проверьте изменения, заголовок у таблицы должен остаться черного цвета. Если посмотреть с помощью консоли разработчика в браузере, то увидим следующее
ngCLass

Также можно вернуть массив классов

Листинг 5.5 | Файл: src/app/all-products/all-products.component.html
...
<table [ngClass]="tableClass()">
  <thead [ngClass]="headClass()">
...
Листинг 5.6 | Файл: src/app/all-servers/all-servers.component.ts
...
  headClass() {
  	return 'thead-dark';
  }

  tableClass() {
  	return ['table', 'table-striped', 'text-center', 'mt-3'];
  }
}

Теперь давайте рассмотрим наиболее сложный вариант использования ngClass, когда в качестве выражения передается map. Основное отличие map и объекта заключается в том, что в качестве пары 'ключ': значение для map можно передать любой тип данных (в данном случае в качестве значения свойства мы будем передавать условие, которое нам будет возвращать true или false).

Давайте изменим наш шаблон (обратите внимание, что я изменил условие в строке 14, чтобы выводились все товары):

Листинг 5.7 | Файл: src/app/all-products/all-products.component.html
...
<ng-container *ngFor="let product of products">
  <tr *ngIf="product.quantity > 0">
    <td>{{ product.id }}</td>
    <td>{{ product.name }}</td>
    <td>${{ product.price }}</td>
    <td><span class="badge" [ngClass]="quantityBagde(product.quantity)">{{ product.quantity }}</span></td>
  </tr>
</ng-container>
...

И добавим новый метод в класс нашего компонента:

Листинг 5.8 | Файл: src/app/all-servers/all-servers.component.ts
...
  tableClass() {
  	return ['table', 'table-striped', 'text-center', 'mt-3'];
  }

  quantityBagde(quantity) {
  	return {
  		'badge-success': quantity >= 3,
  		'badge-danger': quantity < 3
  	}
  }

В последней ячейке мы добавляем тег span с классом badge, а затем добавляем директиву [ngClass], которая в зависимости от количества товаров будет нам возвращать нужны класс — bg-success если больше или равно 3 и bg-danger если 2 и меньше. Сохраните изменения и проверьте, что у вас получилось.

Директива [ngStyle] позволяет задать несколько свойств при помощи объекта-карты. Качестве ключа могут передаваться два типа: просто css-свойство или с единицами измерения. Мы рассмотрим оба варианта для демонстрации. Внесем изменения в шаблон:

Листинг 5.9 | Файл: src/app/all-products/all-products.component.html
...
<ng-container *ngFor="let product of products">
        <tr *ngIf="product.quantity > 0">
          <td>{{ product.id }}</td>
          <td>{{ product.name }}</td>
          <td [ngStyle]="getStyles(product.price)">${{ product.price }}</td>
          <td><span class="badge" [ngClass]="quantityBagde(product.quantity)">{{ product.quantity }}</span></td>
        </tr>
      </ng-container>
...

И добавим метода для нашего компонента:

Листинг 5.10 | Файл: src/app/all-servers/all-servers.component.ts
...
  getStyles(price) {
  	return {
  		fontSize: '1.1rem',
  		'letterSpacing.px': 1,
  		backgroundColor: price > 300 ? 'green' : 'red'
  	}
  }
...

Мы применили стилевые свойства к ячейке с ценой. Изменили размера шрифта, отступы между буквами/цифрами и в зависимости от стоимости меняем фоновый цвет ячейки.

Директива ngSwitch

Директива [ngSwitch] используется для выбора элементов, которые будут включены в документ HTML, на основании сравнения выражения с результатами отдельных выражений, определяемых в директивах *ngSwitchCase. Если ни одно из значений *ngSwitchCase не совпадает, используется элемент, к которому применена директива *ngSwitchDefault

Давайте изменим наш шаблон

Листинг 5.11 | Файл: src/app/all-products/all-products.component.html
...
<ng-container *ngFor="let product of products">
  <tr *ngIf="product.quantity > 0">
    <td>{{ product.id }}</td>
    <td>{{ product.name }}</td>
    <td [ngStyle]="getStyles(product.price)">${{ product.price }}</td>
    <td [ngSwitch]="product.quantity">
      <span class="badge" [ngClass]="quantityBagde(product.quantity)">{{ product.quantity }}</span><br>
      <span *ngSwitchCase="1">Остался последний</span>
      <span *ngSwitchCase="2">Осталось всего два</span>
      <span *ngSwitchCase="3">Осталось еще три</span>
      <span *ngSwitchCase="5">Товара достаточно</span>
      <span *ngSwitchDefault>Значение по умолчанию</span>
    </td>
  </tr>
</ng-container>
...

Тут важно отметить, что мы использовали переменную которую определяли в шаблоне (product.quantity), но можно указать также переменную или метод, которые были определены в классе компонента. Просто я выбрал этот вариант для удобства.

*ngSwitchDefault сработает в том случае, если нет ни одного *ngSwitchCase удовлетворяющего условию (как в случае с товаром с id=1)

Также, обратите внимание, что директива [ngSwitch] пишется в квадратных скобках, а *ngSwitchCase и *ngSwitchDefault просто со звездочкой.

Значения, которые указываются в *ngSwitchCase являются выражениями, т.е. могут быть переданы не только строки, но также переменные или методы. Если вы хотите сравнить *ngSwitchCase со строкой, тогда после двойных кавычек нужно указать еще одинарные — *ngSwitchCase="'string'". Если же вы укажите *ngSwitchCase="string", то Angular будет искать переменную string в компоненте.

Директива ngTemplateOutlet

Данная директива используется для повторения блока с контентом в разных местах шаблона.
К примеру, у нас есть блок рекламы который мы хотим вывести перед началом статьи, и после окончания. Этот код одинаков. В таком случае мы можем использовать директиву ngTemplateOutlet

<!-- Определяем шаблон -->
<ng-template #advertisement>
  <div>--- Код рекламы ---</div>
</ng-template>

<!-- 1-ый вывод блока -->
<ng-template [ngTemplateOutlet]="advertisement"></ng-template>

<div id="content">
  Статья
</div>

<!-- 2-ой вывод блока -->
<ng-template [ngTemplateOutlet]="advertisement"></ng-template>

Сначала мы определяем шаблон с помощью ссылочной переменной #advertisement (имя вы можете выбрать произвольное), затем в том месте где вы хотите использовать данный шаблон просто тегу ng-template добавляете директиву [ngTemplateOutlet]="advertisement"

Ваши вопросы и комментарии: